- Removido README.md padrão do Laravel (backend) - Removidos scripts de deploy (não mais necessários) - Atualizado copilot-instructions.md para novo fluxo - Adicionada documentação de auditoria do servidor - Sincronizado código de produção com repositório Novo workflow: - Trabalhamos diretamente em /root/webmoney (symlink para /var/www/webmoney) - Mudanças PHP são instantâneas - Mudanças React requerem 'npm run build' - Commit após validação funcional
488 lines
16 KiB
PHP
Executable File
488 lines
16 KiB
PHP
Executable File
<?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,
|
|
]);
|
|
}
|
|
}
|