FinancialDashboardService.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. * - "Saldo Bancário" = entradas (to_account) menos saídas (from_account) por conta.
  14. */
  15. class FinancialDashboardService
  16. {
  17. private const SETTLED = [
  18. ReceivableStatus::PAID->value,
  19. ReceivableStatus::CANCELLED->value,
  20. ];
  21. public function overview(): array
  22. {
  23. $now = Carbon::now();
  24. $monthStart = $now->copy()->startOfMonth();
  25. $monthEnd = $now->copy()->endOfMonth();
  26. $prevStart = $now->copy()->subMonthNoOverflow()->startOfMonth();
  27. $prevEnd = $now->copy()->subMonthNoOverflow()->endOfMonth();
  28. $revenueCurrent = $this->monthlyRevenue($monthStart, $monthEnd);
  29. $revenuePrev = $this->monthlyRevenue($prevStart, $prevEnd);
  30. $tbrCurrent = $this->tbrTotal($monthStart, $monthEnd);
  31. $tbrPrev = $this->tbrTotal($prevStart, $prevEnd);
  32. return [
  33. 'marketing_fund' => [
  34. 'value' => $this->marketingFund(),
  35. 'percentage' => 0,
  36. ],
  37. 'monthly_revenue' => [
  38. 'value' => $revenueCurrent,
  39. 'percentage' => $this->variation($revenueCurrent, $revenuePrev),
  40. ],
  41. 'tbr_total' => [
  42. 'value' => $tbrCurrent,
  43. 'percentage' => $this->variation($tbrCurrent, $tbrPrev),
  44. ],
  45. 'accounts_payable' => $this->pendingPayables(),
  46. 'accounts_receivable' => $this->pendingReceivables(),
  47. 'bank_balance_total' => $this->bankBalanceTotal(),
  48. 'last_transactions' => $this->lastTransactions(),
  49. 'franchise_performance' => $this->franchisePerformance($monthStart, $monthEnd),
  50. ];
  51. }
  52. private function monthlyRevenue(Carbon $start, Carbon $end): float
  53. {
  54. return (float) DB::table('student_contract_installments')
  55. ->whereNull('deleted_at')
  56. ->where('status', ReceivableStatus::PAID->value)
  57. ->whereBetween('payment_date', [$start, $end])
  58. ->sum('paid_value');
  59. }
  60. private function tbrTotal(Carbon $start, Carbon $end): float
  61. {
  62. return (float) DB::table('franchisee_account_receives')
  63. ->whereNull('deleted_at')
  64. ->whereBetween('due_date', [$start, $end])
  65. ->sum('value');
  66. }
  67. private function marketingFund(): float
  68. {
  69. return (float) DB::table('unit_financials')
  70. ->whereNull('deleted_at')
  71. ->sum('marketing_fund');
  72. }
  73. private function pendingPayables(): array
  74. {
  75. $row = DB::table('financial_account_payables')
  76. ->whereNull('deleted_at')
  77. ->whereNotIn('status', self::SETTLED)
  78. ->selectRaw('COALESCE(SUM(price + fine + fees), 0) AS total, COUNT(*) AS qty')
  79. ->first();
  80. return ['value' => (float) $row->total, 'count' => (int) $row->qty];
  81. }
  82. private function pendingReceivables(): array
  83. {
  84. $row = DB::table('financial_account_receives')
  85. ->whereNull('deleted_at')
  86. ->whereNotIn('status', self::SETTLED)
  87. ->selectRaw('COALESCE(SUM(price + fine + fees), 0) AS total, COUNT(*) AS qty')
  88. ->first();
  89. return ['value' => (float) $row->total, 'count' => (int) $row->qty];
  90. }
  91. private function bankBalanceTotal(): float
  92. {
  93. // Saldo da rede = créditos (chegam no to_account) menos débitos (saem do from_account),
  94. // somados por conta. No modelo de partida dobrada atual (from/to obrigatórios) as
  95. // transferências internas se anulam; o valor reflete apenas fluxos externos. O cálculo
  96. // de saldo real por banco será consolidado no #CB018 (Tesouraria), que definirá
  97. // saldo de abertura / contas de origem externas.
  98. $credits = (float) DB::table('treasury_launches')
  99. ->whereNull('deleted_at')
  100. ->sum('amount');
  101. $debits = (float) DB::table('treasury_launches')
  102. ->whereNull('deleted_at')
  103. ->sum('amount');
  104. return $credits - $debits;
  105. }
  106. private function lastTransactions(int $limit = 8): array
  107. {
  108. return DB::table('treasury_launches')
  109. ->whereNull('deleted_at')
  110. ->orderByDesc('launch_date')
  111. ->orderByDesc('id')
  112. ->limit($limit)
  113. ->get(['id', 'description', 'amount', 'is_reconciled'])
  114. ->map(fn ($t) => [
  115. 'id' => $t->id,
  116. 'description' => $t->description,
  117. 'value' => (float) $t->amount,
  118. 'status' => $t->is_reconciled ? 'Pago' : 'Pendente',
  119. ])
  120. ->all();
  121. }
  122. private function franchisePerformance(Carbon $start, Carbon $end): array
  123. {
  124. $revenue = DB::table('student_contract_installments')
  125. ->whereNull('deleted_at')
  126. ->where('status', ReceivableStatus::PAID->value)
  127. ->whereBetween('payment_date', [$start, $end])
  128. ->groupBy('unit_id')
  129. ->selectRaw('unit_id, SUM(paid_value) AS total')
  130. ->pluck('total', 'unit_id');
  131. $expense = DB::table('financial_account_payables')
  132. ->whereNull('deleted_at')
  133. ->where('status', ReceivableStatus::PAID->value)
  134. ->whereBetween('payment_date', [$start, $end])
  135. ->groupBy('unit_id')
  136. ->selectRaw('unit_id, SUM(price + fine + fees) AS total')
  137. ->pluck('total', 'unit_id');
  138. return DB::table('units')
  139. ->whereNull('deleted_at')
  140. ->get(['id', 'fantasy_name'])
  141. ->map(function ($unit) use ($revenue, $expense) {
  142. $rev = (float) ($revenue[$unit->id] ?? 0);
  143. $exp = (float) ($expense[$unit->id] ?? 0);
  144. return [
  145. 'id' => $unit->id,
  146. 'franchise' => $unit->fantasy_name,
  147. 'revenue' => $rev,
  148. 'expense' => $exp,
  149. 'performance' => $rev > 0 ? round((($rev - $exp) / $rev) * 100, 1) : 0,
  150. ];
  151. })
  152. ->all();
  153. }
  154. private function variation(float $current, float $previous): float
  155. {
  156. if ($previous <= 0) {
  157. return $current > 0 ? 100.0 : 0.0;
  158. }
  159. return round((($current - $previous) / $previous) * 100, 1);
  160. }
  161. }