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, ]); } }