with(['installments' => function ($q) { $q->orderBy('installment_number'); }]); // Filtros opcionais if ($request->has('status')) { $query->where('status', $request->status); } if ($request->has('is_active')) { $query->where('is_active', $request->boolean('is_active')); } $accounts = $query->orderBy('name')->get(); // Calcular resumo $summary = [ 'total_principal' => $accounts->sum('principal_amount'), 'total_paid' => $accounts->sum('total_paid'), 'total_pending' => $accounts->sum('total_pending'), 'total_interest' => $accounts->sum('total_interest'), 'total_fees' => $accounts->sum('total_fees'), 'contracts_count' => $accounts->count(), 'active_contracts' => $accounts->where('status', 'active')->count(), ]; return response()->json([ 'success' => true, 'data' => $accounts, 'summary' => $summary, 'statuses' => LiabilityAccount::STATUSES, ]); } /** * Criar nova conta passivo manualmente */ public function store(Request $request): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:150', 'contract_number' => 'nullable|string|max:100', 'creditor' => 'nullable|string|max:150', 'description' => 'nullable|string', 'principal_amount' => 'required|numeric|min:0', 'currency' => 'nullable|string|size:3', 'color' => 'nullable|string|max:7', 'icon' => 'nullable|string|max:50', 'start_date' => 'nullable|date', ]); $validated['user_id'] = Auth::id(); $validated['total_pending'] = $validated['principal_amount']; $validated['status'] = LiabilityAccount::STATUS_ACTIVE; $account = LiabilityAccount::create($validated); return response()->json([ 'success' => true, 'message' => 'Conta passivo criada com sucesso', 'data' => $account, ], 201); } /** * Exibir uma conta passivo específica com todas as parcelas */ public function show(int $id): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id()) ->with(['installments' => function ($q) { $q->orderBy('installment_number'); }]) ->findOrFail($id); return response()->json([ 'success' => true, 'data' => $account, ]); } /** * Atualizar uma conta passivo */ public function update(Request $request, int $id): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($id); $validated = $request->validate([ 'name' => 'sometimes|required|string|max:150', 'contract_number' => 'nullable|string|max:100', 'creditor' => 'nullable|string|max:150', 'description' => 'nullable|string', 'currency' => 'nullable|string|size:3', 'color' => 'nullable|string|max:7', 'icon' => 'nullable|string|max:50', 'status' => ['sometimes', Rule::in(array_keys(LiabilityAccount::STATUSES))], 'is_active' => 'nullable|boolean', ]); $account->update($validated); return response()->json([ 'success' => true, 'message' => 'Conta passivo atualizada com sucesso', 'data' => $account->fresh(), ]); } /** * Excluir uma conta passivo */ public function destroy(int $id): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($id); $account->delete(); return response()->json([ 'success' => true, 'message' => 'Conta passivo excluída com sucesso', ]); } /** * Importar contrato de arquivo Excel */ public function import(Request $request): JsonResponse { $request->validate([ 'file' => 'required|file|mimes:xlsx,xls', 'name' => 'required|string|max:150', 'creditor' => 'nullable|string|max:150', 'contract_number' => 'nullable|string|max:100', 'currency' => 'nullable|string|size:3', 'description' => 'nullable|string', ]); try { DB::beginTransaction(); // Criar conta passivo $liabilityAccount = LiabilityAccount::create([ 'user_id' => Auth::id(), 'name' => $request->name, 'creditor' => $request->creditor, 'contract_number' => $request->contract_number, 'currency' => $request->currency ?? 'EUR', 'description' => $request->description, 'principal_amount' => 0, // Será calculado 'status' => LiabilityAccount::STATUS_ACTIVE, ]); // Processar arquivo Excel $file = $request->file('file'); $spreadsheet = IOFactory::load($file->getPathname()); $worksheet = $spreadsheet->getActiveSheet(); $rows = $worksheet->toArray(); // Pular cabeçalho $header = array_shift($rows); // Mapear colunas (baseado no formato do arquivo exemplo) // Colunas: Pago, Fecha, Cuota, Intereses, Capital, Estado $columnMap = $this->mapColumns($header); $installments = []; foreach ($rows as $row) { if (empty($row[$columnMap['installment_number']])) { continue; } $installmentNumber = (int) $row[$columnMap['installment_number']]; $dueDate = $this->parseDate($row[$columnMap['due_date']]); $installmentAmount = $this->parseAmount($row[$columnMap['installment_amount']]); $interestAmount = $this->parseAmount($row[$columnMap['interest_amount']]); $principalAmount = $this->parseAmount($row[$columnMap['principal_amount']]); $status = $this->parseStatus($row[$columnMap['status']]); // Calcular taxa extra (se cuota > capital + juros) $normalAmount = $principalAmount + $interestAmount; $feeAmount = max(0, $installmentAmount - $normalAmount); $installments[] = [ 'liability_account_id' => $liabilityAccount->id, 'installment_number' => $installmentNumber, 'due_date' => $dueDate, 'installment_amount' => $installmentAmount, 'principal_amount' => $principalAmount, 'interest_amount' => $interestAmount, 'fee_amount' => $feeAmount, 'status' => $status, 'paid_amount' => $status === 'paid' ? $installmentAmount : 0, 'paid_date' => $status === 'paid' ? $dueDate : null, 'created_at' => now(), 'updated_at' => now(), ]; } // Inserir parcelas LiabilityInstallment::insert($installments); // Recalcular totais $liabilityAccount->recalculateTotals(); DB::commit(); // Recarregar com parcelas $liabilityAccount = LiabilityAccount::with('installments')->find($liabilityAccount->id); return response()->json([ 'success' => true, 'message' => 'Contrato importado com sucesso', 'data' => $liabilityAccount, 'imported_installments' => count($installments), ], 201); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Erro ao importar arquivo: ' . $e->getMessage(), ], 422); } } /** * Mapear colunas do Excel para campos do sistema */ private function mapColumns(array $header): array { $map = [ 'installment_number' => 0, // Pago (número da parcela) 'due_date' => 1, // Fecha 'installment_amount' => 2, // Cuota 'interest_amount' => 3, // Intereses 'principal_amount' => 4, // Capital 'status' => 5, // Estado ]; // Tentar mapear automaticamente baseado nos nomes das colunas foreach ($header as $index => $columnName) { $columnName = strtolower(trim($columnName)); if (in_array($columnName, ['pago', 'numero', 'nº', 'n', 'parcela', 'installment'])) { $map['installment_number'] = $index; } elseif (in_array($columnName, ['fecha', 'date', 'data', 'vencimiento', 'due_date'])) { $map['due_date'] = $index; } elseif (in_array($columnName, ['cuota', 'quota', 'valor', 'amount', 'installment_amount'])) { $map['installment_amount'] = $index; } elseif (in_array($columnName, ['intereses', 'interest', 'juros'])) { $map['interest_amount'] = $index; } elseif (in_array($columnName, ['capital', 'principal', 'amortización', 'amortizacion'])) { $map['principal_amount'] = $index; } elseif (in_array($columnName, ['estado', 'status', 'situação', 'situacion'])) { $map['status'] = $index; } } return $map; } /** * Converter string de data para formato válido */ private function parseDate($value): string { if ($value instanceof \DateTime) { return $value->format('Y-m-d'); } // Se for número (Excel serial date) if (is_numeric($value)) { $date = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value); return $date->format('Y-m-d'); } // Tentar parsear como string try { return date('Y-m-d', strtotime($value)); } catch (\Exception $e) { return date('Y-m-d'); } } /** * Converter string de valor monetário para float */ private function parseAmount($value): float { if (is_numeric($value)) { return (float) $value; } // Remover símbolos de moeda e espaços $value = preg_replace('/[€$R\s]/', '', $value); // Converter vírgula para ponto (formato europeu) $value = str_replace(',', '.', $value); // Remover pontos de milhar if (substr_count($value, '.') > 1) { $parts = explode('.', $value); $last = array_pop($parts); $value = implode('', $parts) . '.' . $last; } return (float) $value; } /** * Converter status do Excel para status do sistema */ private function parseStatus($value): string { $value = strtolower(trim($value)); $paidStatuses = ['abonado', 'paid', 'pago', 'pagado', 'liquidado']; $pendingStatuses = ['pendiente', 'pending', 'pendente', 'a pagar']; $overdueStatuses = ['atrasado', 'overdue', 'vencido', 'mora']; if (in_array($value, $paidStatuses)) { return LiabilityInstallment::STATUS_PAID; } if (in_array($value, $overdueStatuses)) { return LiabilityInstallment::STATUS_OVERDUE; } return LiabilityInstallment::STATUS_PENDING; } /** * Obter parcelas de uma conta passivo */ public function installments(int $id): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($id); $installments = $account->installments() ->orderBy('installment_number') ->get(); return response()->json([ 'success' => true, 'data' => $installments, 'statuses' => LiabilityInstallment::STATUSES, ]); } /** * Atualizar status de uma parcela */ public function updateInstallment(Request $request, int $accountId, int $installmentId): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($accountId); $installment = LiabilityInstallment::where('liability_account_id', $account->id) ->findOrFail($installmentId); $validated = $request->validate([ 'status' => ['sometimes', Rule::in(array_keys(LiabilityInstallment::STATUSES))], 'paid_amount' => 'nullable|numeric|min:0', 'paid_date' => 'nullable|date', 'payment_account_id' => 'nullable|exists:accounts,id', 'notes' => 'nullable|string', ]); // Se marcar como pago if (isset($validated['status']) && $validated['status'] === 'paid') { $installment->markAsPaid( $validated['paid_amount'] ?? null, isset($validated['paid_date']) ? new \DateTime($validated['paid_date']) : null, $validated['payment_account_id'] ?? null ); } else { $installment->update($validated); $account->recalculateTotals(); } return response()->json([ 'success' => true, 'message' => 'Parcela atualizada com sucesso', 'data' => $installment->fresh(), ]); } /** * Obter resumo de todas as contas passivo */ public function summary(): JsonResponse { $accounts = LiabilityAccount::where('user_id', Auth::id()) ->where('is_active', true) ->get(); // Agrupar por moeda $byCurrency = $accounts->groupBy('currency')->map(function ($group) { return [ 'total_principal' => $group->sum('principal_amount'), 'total_paid' => $group->sum('total_paid'), 'total_pending' => $group->sum('total_pending'), 'total_interest' => $group->sum('total_interest'), 'remaining_balance' => $group->sum('remaining_balance'), 'contracts_count' => $group->count(), ]; }); // Próximas parcelas a vencer $upcomingInstallments = LiabilityInstallment::whereHas('liabilityAccount', function ($q) { $q->where('user_id', Auth::id())->where('is_active', true); }) ->where('status', 'pending') ->where('due_date', '>=', now()) ->where('due_date', '<=', now()->addDays(30)) ->with('liabilityAccount:id,name,currency') ->orderBy('due_date') ->limit(10) ->get(); // Parcelas atrasadas $overdueInstallments = LiabilityInstallment::whereHas('liabilityAccount', function ($q) { $q->where('user_id', Auth::id())->where('is_active', true); }) ->where('status', '!=', 'paid') ->where('due_date', '<', now()) ->with('liabilityAccount:id,name,currency') ->orderBy('due_date') ->get(); return response()->json([ 'success' => true, 'data' => [ 'by_currency' => $byCurrency, 'upcoming_installments' => $upcomingInstallments, 'overdue_installments' => $overdueInstallments, 'overdue_count' => $overdueInstallments->count(), ], ]); } /** * Conciliar uma parcela com uma transação existente * * Vincula uma parcela de conta passivo a uma transação já registrada */ public function reconcile(Request $request, int $accountId, int $installmentId): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($accountId); $installment = LiabilityInstallment::where('liability_account_id', $account->id) ->findOrFail($installmentId); $validated = $request->validate([ 'transaction_id' => 'required|exists:transactions,id', 'mark_as_paid' => 'nullable|boolean', ]); // Verificar se a transação pertence ao usuário $transaction = \App\Models\Transaction::where('user_id', Auth::id()) ->findOrFail($validated['transaction_id']); try { DB::beginTransaction(); // Atualizar parcela com referência à transação $installment->reconciled_transaction_id = $transaction->id; $installment->payment_account_id = $transaction->account_id; // Opcionalmente marcar como paga if ($request->boolean('mark_as_paid', true)) { $installment->status = LiabilityInstallment::STATUS_PAID; $installment->paid_amount = abs($transaction->amount); $installment->paid_date = $transaction->date; } $installment->save(); // Recalcular totais da conta passivo $account->recalculateTotals(); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Parcela conciliada com sucesso', 'data' => $installment->fresh()->load('liabilityAccount'), ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Erro ao conciliar: ' . $e->getMessage(), ], 422); } } /** * Remover conciliação de uma parcela */ public function unreconcile(int $accountId, int $installmentId): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($accountId); $installment = LiabilityInstallment::where('liability_account_id', $account->id) ->findOrFail($installmentId); if (!$installment->reconciled_transaction_id) { return response()->json([ 'success' => false, 'message' => 'Parcela não está conciliada', ], 422); } try { DB::beginTransaction(); // Calcular o sobrepagamento que foi registrado (paid_amount - installment_amount) $paidAmount = (float) $installment->paid_amount; $plannedAmount = (float) $installment->installment_amount; $overpaymentToRemove = max(0, $paidAmount - $plannedAmount); // Remover referência à transação $installment->reconciled_transaction_id = null; $installment->status = LiabilityInstallment::STATUS_PENDING; $installment->paid_amount = 0; $installment->paid_date = null; // Remover o cargo extra (sobrepagamento) que foi adicionado na conciliação if ($overpaymentToRemove > 0 && $installment->fee_amount >= $overpaymentToRemove) { $installment->fee_amount = $installment->fee_amount - $overpaymentToRemove; } $installment->save(); // Recalcular totais $account->recalculateTotals(); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Conciliação removida com sucesso', 'data' => $installment->fresh(), 'fee_removed' => $overpaymentToRemove > 0 ? $overpaymentToRemove : null, ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Erro ao remover conciliação: ' . $e->getMessage(), ], 422); } } /** * Buscar transações elegíveis para conciliação * * Retorna transações que podem ser vinculadas a uma parcela * Ordenadas por similaridade de valor com a parcela */ public function eligibleTransactions(Request $request, int $accountId, int $installmentId): JsonResponse { $account = LiabilityAccount::where('user_id', Auth::id())->findOrFail($accountId); $installment = LiabilityInstallment::where('liability_account_id', $account->id) ->findOrFail($installmentId); // Buscar transações dentro de uma janela de tempo (+/- 45 dias da data de vencimento) // Janela ampla para capturar pagamentos atrasados ou antecipados $startDate = (clone $installment->due_date)->subDays(45); $endDate = (clone $installment->due_date)->addDays(45); $installmentAmount = (float) $installment->installment_amount; // Usar effective_date se existir, senão planned_date $query = \App\Models\Transaction::where('user_id', Auth::id()) ->where(function ($q) use ($startDate, $endDate) { $q->whereBetween('effective_date', [$startDate, $endDate]) ->orWhere(function ($q2) use ($startDate, $endDate) { $q2->whereNull('effective_date') ->whereBetween('planned_date', [$startDate, $endDate]); }); }) ->where('type', 'debit') // Pagamentos são débitos (saídas) ->with('account:id,name,currency'); // Por padrão, filtrar por valores próximos (±20% do valor da parcela) // Permite encontrar transações mesmo com pequenas diferenças $minAmount = $installmentAmount * 0.8; $maxAmount = $installmentAmount * 1.2; // Se strict_amount = false ou não informado, ainda assim filtrar por faixa // Usa COALESCE para considerar amount ou planned_amount if (!$request->has('no_amount_filter')) { $query->whereRaw('COALESCE(amount, planned_amount) BETWEEN ? AND ?', [$minAmount, $maxAmount]); } // Se tiver filtro por conta específica if ($request->has('account_id')) { $query->where('account_id', $request->account_id); } // Busca por descrição if ($request->has('search')) { $query->where(function ($q) use ($request) { $q->where('description', 'like', '%' . $request->search . '%') ->orWhere('original_description', 'like', '%' . $request->search . '%'); }); } // Ordenar por similaridade de valor (mais próximo primeiro) e depois por data // ABS(COALESCE(amount, planned_amount) - valor_parcela) = diferença absoluta $query->orderByRaw("ABS(COALESCE(amount, planned_amount) - ?) ASC", [$installmentAmount]) ->orderByRaw('COALESCE(effective_date, planned_date) DESC'); $transactions = $query->limit(30)->get(); // Adicionar campo de diferença percentual para cada transação $transactions->transform(function ($transaction) use ($installmentAmount) { $transactionAmount = (float) ($transaction->amount ?? $transaction->planned_amount); $diff = abs($transactionAmount - $installmentAmount); $diffPercent = $installmentAmount > 0 ? ($diff / $installmentAmount) * 100 : 0; $transaction->amount_difference = round($diff, 2); $transaction->amount_difference_percent = round($diffPercent, 1); return $transaction; }); return response()->json([ 'success' => true, 'data' => $transactions, 'installment' => [ 'id' => $installment->id, 'installment_number' => $installment->installment_number, 'due_date' => $installment->due_date->format('Y-m-d'), 'installment_amount' => $installmentAmount, ], 'search_period' => [ 'start' => $startDate->format('Y-m-d'), 'end' => $endDate->format('Y-m-d'), ], 'amount_range' => [ 'min' => round($minAmount, 2), 'max' => round($maxAmount, 2), ], ]); } /** * Listar parcelas pendentes de conciliação */ public function pendingReconciliation(): JsonResponse { $installments = LiabilityInstallment::whereHas('liabilityAccount', function ($q) { $q->where('user_id', Auth::id())->where('is_active', true); }) ->whereNull('reconciled_transaction_id') ->where('status', '!=', 'cancelled') ->with('liabilityAccount:id,name,currency,creditor') ->orderBy('due_date') ->get(); return response()->json([ 'success' => true, 'data' => $installments, 'count' => $installments->count(), ]); } /** * Download template Excel para importação */ public function downloadTemplate() { $service = new \App\Services\LiabilityTemplateService(); $spreadsheet = $service->generateTemplate(); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); $filename = 'plantilla_importacion_pasivo.xlsx'; // Criar response com stream return response()->streamDownload(function () use ($writer) { $writer->save('php://output'); }, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', 'Cache-Control' => 'max-age=0', ]); } /** * Obter tipos de contrato disponíveis */ public function contractTypes(): JsonResponse { return response()->json([ 'success' => true, 'data' => LiabilityAccount::CONTRACT_TYPES, 'amortization_systems' => LiabilityAccount::AMORTIZATION_SYSTEMS, 'index_types' => LiabilityAccount::INDEX_TYPES, 'guarantee_types' => LiabilityAccount::GUARANTEE_TYPES, ]); } /** * Criar conta passivo com wizard (formulário completo) */ public function storeWithWizard(Request $request): JsonResponse { $validated = $request->validate([ // Dados básicos 'name' => 'required|string|max:150', 'contract_type' => ['required', \Illuminate\Validation\Rule::in(array_keys(LiabilityAccount::CONTRACT_TYPES))], 'creditor' => 'nullable|string|max:150', 'contract_number' => 'nullable|string|max:100', 'description' => 'nullable|string', 'currency' => 'nullable|string|size:3', 'color' => 'nullable|string|max:7', // Dados do contrato 'principal_amount' => 'required|numeric|min:0.01', 'annual_interest_rate' => 'nullable|numeric|min:0|max:100', 'monthly_interest_rate' => 'nullable|numeric|min:0|max:20', 'amortization_system' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(LiabilityAccount::AMORTIZATION_SYSTEMS))], 'total_installments' => 'nullable|integer|min:1|max:600', 'start_date' => 'required|date', 'first_due_date' => 'required|date', 'end_date' => 'nullable|date', // Opções básicas 'has_grace_period' => 'nullable|boolean', 'grace_period_months' => 'nullable|integer|min:0|max:12', 'include_insurance' => 'nullable|boolean', 'insurance_amount' => 'nullable|numeric|min:0', 'include_admin_fee' => 'nullable|boolean', 'admin_fee_amount' => 'nullable|numeric|min:0', // ============================================ // CAMPOS AVANÇADOS (opcionais) // ============================================ // Indexadores 'index_type' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(LiabilityAccount::INDEX_TYPES))], 'index_spread' => 'nullable|numeric|min:-50|max:50', 'total_effective_cost' => 'nullable|numeric|min:0|max:500', // Garantias 'guarantee_type' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(LiabilityAccount::GUARANTEE_TYPES))], 'guarantee_value' => 'nullable|numeric|min:0', 'guarantee_description' => 'nullable|string|max:500', 'guarantor_name' => 'nullable|string|max:150', // Penalidades 'late_fee_percent' => 'nullable|numeric|min:0|max:100', 'daily_penalty_percent' => 'nullable|numeric|min:0|max:10', 'grace_days_for_penalty' => 'nullable|integer|min:0|max:30', // Específicos por tipo 'asset_value' => 'nullable|numeric|min:0', 'asset_description' => 'nullable|string|max:300', 'residual_value' => 'nullable|numeric|min:0', 'admin_fee_percent' => 'nullable|numeric|min:0|max:50', 'reserve_fund_percent' => 'nullable|numeric|min:0|max:20', // Covenants e gestão 'covenants' => 'nullable|array', 'covenants.*.name' => 'required_with:covenants|string|max:100', 'covenants.*.condition' => 'required_with:covenants|string|max:50', 'covenants.*.value' => 'required_with:covenants|string|max:100', 'alert_days_before' => 'nullable|integer|min:0|max:60', 'internal_responsible' => 'nullable|string|max:150', 'internal_notes' => 'nullable|string', 'document_number' => 'nullable|string|max:100', 'registry_office' => 'nullable|string|max:200', // Parcelas (opcional - se não enviado, será calculado) 'installments' => 'nullable|array', 'installments.*.installment_number' => 'required|integer|min:1', 'installments.*.due_date' => 'required|date', 'installments.*.installment_amount' => 'required|numeric|min:0', 'installments.*.principal_amount' => 'nullable|numeric|min:0', 'installments.*.interest_amount' => 'nullable|numeric|min:0', 'installments.*.fee_amount' => 'nullable|numeric|min:0', 'installments.*.status' => 'nullable|string', ]); try { DB::beginTransaction(); // Calcular taxa mensal se não informada $monthlyRate = $validated['monthly_interest_rate'] ?? null; if (!$monthlyRate && isset($validated['annual_interest_rate'])) { $monthlyRate = $validated['annual_interest_rate'] / 12; } // Criar conta passivo com todos os campos $account = LiabilityAccount::create([ 'user_id' => Auth::id(), 'name' => $validated['name'], 'contract_type' => $validated['contract_type'], 'creditor' => $validated['creditor'] ?? null, 'contract_number' => $validated['contract_number'] ?? null, 'description' => $validated['description'] ?? null, 'currency' => $validated['currency'] ?? 'EUR', 'color' => $validated['color'] ?? null, 'principal_amount' => $validated['principal_amount'], 'annual_interest_rate' => $validated['annual_interest_rate'] ?? null, 'monthly_interest_rate' => $monthlyRate, 'amortization_system' => $validated['amortization_system'] ?? 'price', 'total_installments' => $validated['total_installments'] ?? null, 'start_date' => $validated['start_date'], 'first_due_date' => $validated['first_due_date'], 'end_date' => $validated['end_date'] ?? null, 'has_grace_period' => $validated['has_grace_period'] ?? false, 'grace_period_months' => $validated['grace_period_months'] ?? 0, 'status' => LiabilityAccount::STATUS_ACTIVE, // Campos avançados - Indexadores 'index_type' => $validated['index_type'] ?? 'fixed', 'index_spread' => $validated['index_spread'] ?? null, 'total_effective_cost' => $validated['total_effective_cost'] ?? null, // Campos avançados - Garantias 'guarantee_type' => $validated['guarantee_type'] ?? 'none', 'guarantee_value' => $validated['guarantee_value'] ?? null, 'guarantee_description' => $validated['guarantee_description'] ?? null, 'guarantor_name' => $validated['guarantor_name'] ?? null, // Campos avançados - Penalidades 'late_fee_percent' => $validated['late_fee_percent'] ?? null, 'daily_penalty_percent' => $validated['daily_penalty_percent'] ?? null, 'grace_days_for_penalty' => $validated['grace_days_for_penalty'] ?? 0, // Campos avançados - Específicos 'asset_value' => $validated['asset_value'] ?? null, 'asset_description' => $validated['asset_description'] ?? null, 'residual_value' => $validated['residual_value'] ?? null, 'admin_fee_percent' => $validated['admin_fee_percent'] ?? null, 'reserve_fund_percent' => $validated['reserve_fund_percent'] ?? null, // Campos avançados - Covenants e gestão 'covenants' => $validated['covenants'] ?? null, 'alert_days_before' => $validated['alert_days_before'] ?? 5, 'internal_responsible' => $validated['internal_responsible'] ?? null, 'internal_notes' => $validated['internal_notes'] ?? null, 'document_number' => $validated['document_number'] ?? null, 'registry_office' => $validated['registry_office'] ?? null, ]); // Se parcelas foram enviadas, criar diretamente if (!empty($validated['installments'])) { foreach ($validated['installments'] as $inst) { LiabilityInstallment::create([ 'liability_account_id' => $account->id, 'installment_number' => $inst['installment_number'], 'due_date' => $inst['due_date'], 'installment_amount' => $inst['installment_amount'], 'principal_amount' => $inst['principal_amount'] ?? 0, 'interest_amount' => $inst['interest_amount'] ?? 0, 'fee_amount' => $inst['fee_amount'] ?? 0, 'status' => $inst['status'] ?? 'pending', ]); } } else { // Gerar parcelas automaticamente $this->generateInstallments($account, $validated); } // Recalcular totais $account->recalculateTotals(); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Cuenta pasivo creada con éxito', 'data' => $account->load('installments'), ], 201); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Error al crear cuenta: ' . $e->getMessage(), ], 422); } } /** * Gerar parcelas automaticamente baseado no sistema de amortização */ private function generateInstallments(LiabilityAccount $account, array $data): void { $principal = $account->principal_amount; $monthlyRate = ($account->monthly_interest_rate ?? 0) / 100; $totalInstallments = $data['total_installments'] ?? 12; $amortizationSystem = $account->amortization_system ?? 'price'; $hasGracePeriod = $data['has_grace_period'] ?? false; $gracePeriodMonths = $data['grace_period_months'] ?? 0; $insuranceAmount = $data['insurance_amount'] ?? 0; $adminFeeAmount = $data['admin_fee_amount'] ?? 0; $firstDueDate = new \DateTime($data['first_due_date']); $remainingPrincipal = $principal; // Para sistema PRICE, calcular parcela fixa $fixedInstallment = 0; if ($amortizationSystem === 'price' && $monthlyRate > 0) { $fixedInstallment = $principal * ($monthlyRate * pow(1 + $monthlyRate, $totalInstallments)) / (pow(1 + $monthlyRate, $totalInstallments) - 1); } elseif ($amortizationSystem === 'price') { $fixedInstallment = $principal / $totalInstallments; } // Para sistema SAC, calcular amortização fixa $fixedAmortization = $principal / $totalInstallments; for ($i = 1; $i <= $totalInstallments; $i++) { $dueDate = clone $firstDueDate; $dueDate->modify('+' . ($i - 1) . ' months'); // Carência if ($hasGracePeriod && $i <= $gracePeriodMonths) { $interestAmount = $remainingPrincipal * $monthlyRate; $principalAmount = 0; $installmentAmount = $interestAmount + $insuranceAmount + $adminFeeAmount; } else { if ($amortizationSystem === 'price') { // Sistema PRICE - parcela fixa $interestAmount = $remainingPrincipal * $monthlyRate; $principalAmount = $fixedInstallment - $interestAmount; $installmentAmount = $fixedInstallment + $insuranceAmount + $adminFeeAmount; } elseif ($amortizationSystem === 'sac') { // Sistema SAC - amortização constante $principalAmount = $fixedAmortization; $interestAmount = $remainingPrincipal * $monthlyRate; $installmentAmount = $principalAmount + $interestAmount + $insuranceAmount + $adminFeeAmount; } else { // Americano - só juros, principal no final $interestAmount = $remainingPrincipal * $monthlyRate; $principalAmount = ($i === $totalInstallments) ? $remainingPrincipal : 0; $installmentAmount = $interestAmount + $principalAmount + $insuranceAmount + $adminFeeAmount; } $remainingPrincipal -= $principalAmount; } LiabilityInstallment::create([ 'liability_account_id' => $account->id, 'installment_number' => $i, 'due_date' => $dueDate->format('Y-m-d'), 'installment_amount' => round($installmentAmount, 2), 'principal_amount' => round(max(0, $principalAmount), 2), 'interest_amount' => round($interestAmount, 2), 'fee_amount' => round($insuranceAmount + $adminFeeAmount, 2), 'status' => 'pending', ]); } } }