FinancialDashboardService.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\ReceivableStatus;
  4. use Illuminate\Support\Carbon;
  5. use Illuminate\Support\Facades\DB;
  6. /**
  7. * Agrega os indicadores financeiros consolidados da rede (visão do Franqueador).
  8. *
  9. * Premissas de cálculo (#CB017 — confirmar com o negócio se necessário):
  10. * - "Receita de Mensalidade" = parcelas de aluno pagas no mês (paid_value).
  11. * - "TBR Total" = TBR faturada à franquia no mês (franchisee_account_receives.value).
  12. * - "Fundo de Marketing" = soma do fundo configurado por unidade (unit_financials).
  13. * - "Contas a Receber" = TBR pendente das franquias (franchisee_account_receives).
  14. * - "Saldo Bancário" = entradas (to_account) menos saídas (from_account) por conta.
  15. * - "Desempenho por Franquia": a "Despesa" de cada unidade é o total que ela deve
  16. * pagar ao franqueador (TBR) acumulado até o mês corrente (due_date <= fim do mês).
  17. */
  18. class FinancialDashboardService
  19. {
  20. private const SETTLED = [
  21. ReceivableStatus::PAID->value,
  22. ReceivableStatus::CANCELLED->value,
  23. ];
  24. public function overview(): array
  25. {
  26. $now = Carbon::now();
  27. $monthStart = $now->copy()->startOfMonth();
  28. $monthEnd = $now->copy()->endOfMonth();
  29. $prevStart = $now->copy()->subMonthNoOverflow()->startOfMonth();
  30. $prevEnd = $now->copy()->subMonthNoOverflow()->endOfMonth();
  31. $revenueCurrent = $this->monthlyRevenue($monthStart, $monthEnd);
  32. $revenuePrev = $this->monthlyRevenue($prevStart, $prevEnd);
  33. $tbrCurrent = $this->tbrTotal($monthStart, $monthEnd);
  34. $tbrPrev = $this->tbrTotal($prevStart, $prevEnd);
  35. return [
  36. 'marketing_fund' => [
  37. 'value' => $this->marketingFund(),
  38. 'percentage' => 0,
  39. ],
  40. 'monthly_revenue' => [
  41. 'value' => $revenueCurrent,
  42. 'percentage' => $this->variation($revenueCurrent, $revenuePrev),
  43. ],
  44. 'tbr_total' => [
  45. 'value' => $tbrCurrent,
  46. 'percentage' => $this->variation($tbrCurrent, $tbrPrev),
  47. ],
  48. 'accounts_payable' => $this->pendingPayables(),
  49. 'accounts_receivable' => $this->pendingReceivables(),
  50. 'bank_balance_total' => $this->bankBalanceTotal(),
  51. 'last_transactions' => $this->lastTransactions(),
  52. 'franchise_performance' => $this->franchisePerformance($monthStart, $monthEnd),
  53. ];
  54. }
  55. private function monthlyRevenue(Carbon $start, Carbon $end): float
  56. {
  57. return (float) DB::table('student_contract_installments')
  58. ->whereNull('deleted_at')
  59. ->where('status', ReceivableStatus::PAID->value)
  60. ->whereBetween('payment_date', [$start, $end])
  61. ->sum('paid_value');
  62. }
  63. private function tbrTotal(Carbon $start, Carbon $end): float
  64. {
  65. return (float) DB::table('franchisee_account_receives')
  66. ->whereNull('deleted_at')
  67. ->whereBetween('due_date', [$start, $end])
  68. ->sum('value');
  69. }
  70. private function marketingFund(): float
  71. {
  72. return (float) DB::table('unit_financials')
  73. ->whereNull('deleted_at')
  74. ->sum('marketing_fund');
  75. }
  76. private function pendingPayables(): array
  77. {
  78. $row = DB::table('financial_account_payables')
  79. ->whereNull('deleted_at')
  80. ->whereNotIn('status', self::SETTLED)
  81. ->selectRaw('COALESCE(SUM(price + fine + fees), 0) AS total, COUNT(*) AS qty')
  82. ->first();
  83. return ['value' => (float) $row->total, 'count' => (int) $row->qty];
  84. }
  85. private function pendingReceivables(): array
  86. {
  87. // Recebíveis do franqueador = TBR cobrada das franquias ainda em aberto.
  88. $row = DB::table('franchisee_account_receives')
  89. ->whereNull('deleted_at')
  90. ->whereNotIn('status', self::SETTLED)
  91. ->selectRaw('COALESCE(SUM(value + fees - discount - paid_value), 0) AS total, COUNT(*) AS qty')
  92. ->first();
  93. return ['value' => (float) $row->total, 'count' => (int) $row->qty];
  94. }
  95. private function bankBalanceTotal(): float
  96. {
  97. // Saldo da rede = créditos (chegam no to_account) menos débitos (saem do from_account),
  98. // somados por conta. No modelo de partida dobrada atual (from/to obrigatórios) as
  99. // transferências internas se anulam; o valor reflete apenas fluxos externos. O cálculo
  100. // de saldo real por banco será consolidado no #CB018 (Tesouraria), que definirá
  101. // saldo de abertura / contas de origem externas.
  102. $credits = (float) DB::table('treasury_launches')
  103. ->whereNull('deleted_at')
  104. ->sum('amount');
  105. $debits = (float) DB::table('treasury_launches')
  106. ->whereNull('deleted_at')
  107. ->sum('amount');
  108. return $credits - $debits;
  109. }
  110. private function lastTransactions(int $limit = 8): array
  111. {
  112. return DB::table('treasury_launches')
  113. ->whereNull('deleted_at')
  114. ->orderByDesc('launch_date')
  115. ->orderByDesc('id')
  116. ->limit($limit)
  117. ->get(['id', 'description', 'amount', 'is_reconciled'])
  118. ->map(fn ($t) => [
  119. 'id' => $t->id,
  120. 'description' => $t->description,
  121. 'value' => (float) $t->amount,
  122. 'status' => $t->is_reconciled ? 'Pago' : 'Pendente',
  123. ])
  124. ->all();
  125. }
  126. private function franchisePerformance(Carbon $start, Carbon $end): array
  127. {
  128. $revenue = DB::table('student_contract_installments')
  129. ->whereNull('deleted_at')
  130. ->where('status', ReceivableStatus::PAID->value)
  131. ->whereBetween('payment_date', [$start, $end])
  132. ->groupBy('unit_id')
  133. ->selectRaw('unit_id, SUM(paid_value) AS total')
  134. ->pluck('total', 'unit_id');
  135. // Despesa = total que a unidade deve pagar ao franqueador (TBR) acumulado
  136. // até o mês corrente (toda cobrança com vencimento até o fim do mês).
  137. $expense = DB::table('franchisee_account_receives')
  138. ->whereNull('deleted_at')
  139. ->where('due_date', '<=', $end)
  140. ->groupBy('unit_id')
  141. ->selectRaw('unit_id, SUM(value) AS total')
  142. ->pluck('total', 'unit_id');
  143. return DB::table('units')
  144. ->whereNull('deleted_at')
  145. ->get(['id', 'fantasy_name'])
  146. ->map(function ($unit) use ($revenue, $expense) {
  147. $rev = (float) ($revenue[$unit->id] ?? 0);
  148. $exp = (float) ($expense[$unit->id] ?? 0);
  149. return [
  150. 'id' => $unit->id,
  151. 'franchise' => $unit->fantasy_name,
  152. 'revenue' => $rev,
  153. 'expense' => $exp,
  154. 'performance' => $rev > 0 ? round((($rev - $exp) / $rev) * 100, 1) : 0,
  155. ];
  156. })
  157. ->all();
  158. }
  159. private function variation(float $current, float $previous): float
  160. {
  161. if ($previous <= 0) {
  162. return $current > 0 ? 100.0 : 0.0;
  163. }
  164. return round((($current - $previous) / $previous) * 100, 1);
  165. }
  166. }