webmoney/backend/app/Http/Controllers/Api/RecurringTemplateController.php
2025-12-16 18:04:59 +00:00

488 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\RecurringTemplate;
use App\Models\RecurringInstance;
use App\Models\Transaction;
use App\Services\RecurringService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
class RecurringTemplateController extends Controller
{
private RecurringService $recurringService;
public function __construct(RecurringService $recurringService)
{
$this->recurringService = $recurringService;
}
/**
* Lista todos os templates de recorrência do usuário
*/
public function index(Request $request): JsonResponse
{
$query = RecurringTemplate::where('user_id', Auth::id())
->with(['account', 'category', 'costCenter'])
->withCount(['instances', 'pendingInstances', 'paidInstances']);
// Filtros
if ($request->has('is_active')) {
$query->where('is_active', $request->boolean('is_active'));
}
if ($request->has('frequency')) {
$query->where('frequency', $request->frequency);
}
if ($request->has('type')) {
$query->where('type', $request->type);
}
if ($request->has('account_id')) {
$query->where('account_id', $request->account_id);
}
if ($request->has('category_id')) {
$query->where('category_id', $request->category_id);
}
// Ordenação
$sortBy = $request->get('sort_by', 'name');
$sortDir = $request->get('sort_dir', 'asc');
$query->orderBy($sortBy, $sortDir);
$templates = $query->paginate($request->get('per_page', 20));
return response()->json($templates);
}
/**
* Exibe um template específico
*/
public function show(RecurringTemplate $recurringTemplate): JsonResponse
{
$this->authorize('view', $recurringTemplate);
$recurringTemplate->load([
'account',
'category',
'costCenter',
'sourceTransaction',
'instances' => fn($q) => $q->orderBy('due_date', 'asc'),
]);
return response()->json($recurringTemplate);
}
/**
* Cria um novo template de recorrência manualmente
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'frequency' => 'required|in:' . implode(',', array_keys(RecurringTemplate::FREQUENCIES)),
'frequency_interval' => 'nullable|integer|min:1|max:12',
'day_of_month' => 'nullable|integer|min:1|max:31',
'day_of_week' => 'nullable|integer|min:0|max:6',
'start_date' => 'required|date',
'end_date' => 'nullable|date|after:start_date',
'max_occurrences' => 'nullable|integer|min:1|max:999',
'account_id' => 'required|exists:accounts,id',
'category_id' => 'nullable|exists:categories,id',
'cost_center_id' => 'nullable|exists:cost_centers,id',
'type' => 'required|in:income,expense',
'planned_amount' => 'required|numeric|min:0.01',
'transaction_description' => 'required|string|max:255',
'notes' => 'nullable|string|max:1000',
]);
$template = $this->recurringService->createTemplate(Auth::id(), $validated);
return response()->json([
'message' => __('Recurring template created successfully'),
'template' => $template,
], 201);
}
/**
* Cria um template a partir de uma transação existente
*/
public function createFromTransaction(Request $request): JsonResponse
{
$validated = $request->validate([
'transaction_id' => 'required|exists:transactions,id',
'frequency' => 'required|in:' . implode(',', array_keys(RecurringTemplate::FREQUENCIES)),
'name' => 'nullable|string|max:255',
'description' => 'nullable|string|max:1000',
'frequency_interval' => 'nullable|integer|min:1|max:12',
'day_of_month' => 'nullable|integer|min:1|max:31',
'day_of_week' => 'nullable|integer|min:0|max:6',
'start_date' => 'nullable|date',
'end_date' => 'nullable|date',
'max_occurrences' => 'nullable|integer|min:1|max:999',
]);
$transaction = Transaction::where('user_id', Auth::id())
->findOrFail($validated['transaction_id']);
$options = collect($validated)->except(['transaction_id', 'frequency'])->filter()->toArray();
$template = $this->recurringService->createFromTransaction(
$transaction,
$validated['frequency'],
$options
);
return response()->json([
'message' => __('Recurring template created from transaction'),
'template' => $template,
], 201);
}
/**
* Atualiza um template
*/
public function update(Request $request, RecurringTemplate $recurringTemplate): JsonResponse
{
$this->authorize('update', $recurringTemplate);
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
'description' => 'nullable|string|max:1000',
'frequency' => 'sometimes|in:' . implode(',', array_keys(RecurringTemplate::FREQUENCIES)),
'frequency_interval' => 'nullable|integer|min:1|max:12',
'day_of_month' => 'nullable|integer|min:1|max:31',
'day_of_week' => 'nullable|integer|min:0|max:6',
'end_date' => 'nullable|date',
'max_occurrences' => 'nullable|integer|min:1|max:999',
'account_id' => 'sometimes|exists:accounts,id',
'category_id' => 'nullable|exists:categories,id',
'cost_center_id' => 'nullable|exists:cost_centers,id',
'planned_amount' => 'sometimes|numeric|min:0.01',
'transaction_description' => 'sometimes|string|max:255',
'notes' => 'nullable|string|max:1000',
'is_active' => 'sometimes|boolean',
]);
$recurringTemplate->update($validated);
return response()->json([
'message' => __('Recurring template updated successfully'),
'template' => $recurringTemplate->fresh(['account', 'category', 'costCenter']),
]);
}
/**
* Remove permanentemente um template e TODAS as suas instâncias (hard delete)
*/
public function destroy(RecurringTemplate $recurringTemplate): JsonResponse
{
$this->authorize('delete', $recurringTemplate);
// HARD DELETE: Remover TODAS as instâncias (pagas, pendentes, canceladas, etc)
$recurringTemplate->instances()->forceDelete();
// HARD DELETE: Remover o template permanentemente
$recurringTemplate->forceDelete();
return response()->json([
'message' => __('Recurring template permanently deleted'),
]);
}
/**
* Pausa um template
*/
public function pause(RecurringTemplate $recurringTemplate): JsonResponse
{
$this->authorize('update', $recurringTemplate);
$template = $this->recurringService->pauseTemplate($recurringTemplate);
return response()->json([
'message' => __('Recurring template paused'),
'template' => $template,
]);
}
/**
* Reativa um template
*/
public function resume(RecurringTemplate $recurringTemplate): JsonResponse
{
$this->authorize('update', $recurringTemplate);
$template = $this->recurringService->resumeTemplate($recurringTemplate);
return response()->json([
'message' => __('Recurring template resumed'),
'template' => $template,
]);
}
/**
* Lista instâncias de um template
*/
public function instances(Request $request, RecurringTemplate $recurringTemplate): JsonResponse
{
$this->authorize('view', $recurringTemplate);
$query = $recurringTemplate->instances()
->with('transaction');
// Filtro por status
if ($request->has('status')) {
$query->where('status', $request->status);
}
// Filtro por período
if ($request->has('from_date')) {
$query->where('due_date', '>=', $request->from_date);
}
if ($request->has('to_date')) {
$query->where('due_date', '<=', $request->to_date);
}
$instances = $query->orderBy('due_date', 'asc')->get();
return response()->json($instances);
}
/**
* Lista todas as instâncias pendentes do usuário (dashboard)
*/
public function allPendingInstances(Request $request): JsonResponse
{
$query = RecurringInstance::where('user_id', Auth::id())
->where('status', RecurringInstance::STATUS_PENDING)
->with(['template', 'template.account', 'template.category']);
// Filtros
if ($request->has('type')) {
$query->whereHas('template', fn($q) => $q->where('type', $request->type));
}
if ($request->has('from_date')) {
$query->where('due_date', '>=', $request->from_date);
}
if ($request->has('to_date')) {
$query->where('due_date', '<=', $request->to_date);
}
// Ordenar por data de vencimento
$instances = $query->orderBy('due_date', 'asc')
->limit($request->get('limit', 50))
->get();
return response()->json($instances);
}
/**
* Lista instâncias vencidas
*/
public function overdueInstances(): JsonResponse
{
$instances = RecurringInstance::where('user_id', Auth::id())
->overdue()
->with(['template', 'template.account', 'template.category'])
->orderBy('due_date', 'asc')
->get();
return response()->json($instances);
}
/**
* Lista instâncias próximas do vencimento (próximos 7 dias)
*/
public function dueSoonInstances(Request $request): JsonResponse
{
$days = $request->get('days', 7);
$instances = RecurringInstance::where('user_id', Auth::id())
->dueSoon($days)
->with(['template', 'template.account', 'template.category'])
->orderBy('due_date', 'asc')
->get();
return response()->json($instances);
}
/**
* Marca uma instância como paga (cria transação)
*/
public function markAsPaid(Request $request, RecurringInstance $recurringInstance): JsonResponse
{
$this->authorize('update', $recurringInstance->template);
if ($recurringInstance->isPaid()) {
return response()->json([
'message' => __('This instance is already paid'),
], 422);
}
$validated = $request->validate([
'amount' => 'nullable|numeric|min:0.01',
'effective_date' => 'nullable|date',
'description' => 'nullable|string|max:255',
'notes' => 'nullable|string|max:1000',
]);
$instance = $this->recurringService->markAsPaid($recurringInstance, $validated);
return response()->json([
'message' => __('Instance marked as paid'),
'instance' => $instance,
]);
}
/**
* Concilia uma instância com uma transação existente
*/
public function reconcile(Request $request, RecurringInstance $recurringInstance): JsonResponse
{
$this->authorize('update', $recurringInstance->template);
if ($recurringInstance->isPaid()) {
return response()->json([
'message' => __('This instance is already reconciled'),
], 422);
}
$validated = $request->validate([
'transaction_id' => 'required|exists:transactions,id',
'notes' => 'nullable|string|max:1000',
]);
$transaction = Transaction::where('user_id', Auth::id())
->findOrFail($validated['transaction_id']);
$instance = $this->recurringService->reconcileWithTransaction(
$recurringInstance,
$transaction,
$validated['notes'] ?? null
);
return response()->json([
'message' => __('Instance reconciled with transaction'),
'instance' => $instance,
]);
}
/**
* Busca transações candidatas para conciliação
*/
public function findCandidates(Request $request, RecurringInstance $recurringInstance): JsonResponse
{
$this->authorize('view', $recurringInstance->template);
$daysTolerance = $request->get('days_tolerance', 7);
$candidates = $this->recurringService->findCandidateTransactions(
$recurringInstance,
$daysTolerance
);
return response()->json($candidates);
}
/**
* Pula uma instância
*/
public function skip(Request $request, RecurringInstance $recurringInstance): JsonResponse
{
$this->authorize('update', $recurringInstance->template);
$validated = $request->validate([
'reason' => 'nullable|string|max:255',
]);
$instance = $this->recurringService->skipInstance(
$recurringInstance,
$validated['reason'] ?? null
);
return response()->json([
'message' => __('Instance skipped'),
'instance' => $instance,
]);
}
/**
* Atualiza uma instância individual
*/
public function updateInstance(Request $request, RecurringInstance $recurringInstance): JsonResponse
{
$this->authorize('update', $recurringInstance->template);
if ($recurringInstance->isPaid()) {
return response()->json([
'message' => __('Cannot edit a paid instance'),
], 422);
}
$validated = $request->validate([
'planned_amount' => 'sometimes|numeric|min:0.01',
'due_date' => 'sometimes|date',
'notes' => 'nullable|string|max:1000',
]);
$recurringInstance->update($validated);
return response()->json([
'message' => __('Instance updated successfully'),
'instance' => $recurringInstance->fresh(['template', 'transaction']),
]);
}
/**
* Cancela uma instância
*/
public function cancel(Request $request, RecurringInstance $recurringInstance): JsonResponse
{
$this->authorize('update', $recurringInstance->template);
$validated = $request->validate([
'reason' => 'nullable|string|max:255',
]);
$instance = $this->recurringService->cancelInstance(
$recurringInstance,
$validated['reason'] ?? null
);
return response()->json([
'message' => __('Instance cancelled'),
'instance' => $instance,
]);
}
/**
* Retorna as frequências disponíveis
*/
public function frequencies(): JsonResponse
{
return response()->json(RecurringTemplate::FREQUENCIES);
}
/**
* Regenera instâncias para todos os templates ativos do usuário
*/
public function regenerateAll(): JsonResponse
{
$generated = $this->recurringService->regenerateAllForUser(Auth::id());
return response()->json([
'message' => __(':count instances generated', ['count' => $generated]),
'generated' => $generated,
]);
}
}