FinancialDashboardService.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. 'stock_value' => $this->stockValue(),
  52. 'last_transactions' => $this->lastTransactions(),
  53. 'franchise_performance' => $this->franchisePerformance($monthStart, $monthEnd),
  54. ];
  55. }
  56. private function monthlyRevenue(Carbon $start, Carbon $end): float
  57. {
  58. return (float) DB::table('student_contract_installments')
  59. ->whereNull('deleted_at')
  60. ->where('status', ReceivableStatus::PAID->value)
  61. ->whereBetween('payment_date', [$start, $end])
  62. ->sum('paid_value');
  63. }
  64. private function tbrTotal(Carbon $start, Carbon $end): float
  65. {
  66. return (float) DB::table('franchisee_account_receives')
  67. ->whereNull('deleted_at')
  68. ->whereBetween('due_date', [$start, $end])
  69. ->sum('value');
  70. }
  71. private function marketingFund(): float
  72. {
  73. return (float) DB::table('unit_financials')
  74. ->whereNull('deleted_at')
  75. ->sum('marketing_fund');
  76. }
  77. private function pendingPayables(): array
  78. {
  79. $row = DB::table('company_account_payables')
  80. ->whereNull('deleted_at')
  81. ->whereNotIn('status', self::SETTLED)
  82. ->selectRaw('COALESCE(SUM(value + fine - discount - paid_value), 0) AS total, COUNT(*) AS qty')
  83. ->first();
  84. return ['value' => (float) $row->total, 'count' => (int) $row->qty];
  85. }
  86. private function pendingReceivables(): array
  87. {
  88. // Recebíveis do franqueador = TBR cobrada das franquias ainda em aberto.
  89. $row = DB::table('franchisee_account_receives')
  90. ->whereNull('deleted_at')
  91. ->whereNotIn('status', self::SETTLED)
  92. ->selectRaw('COALESCE(SUM(value + fees - discount - paid_value), 0) AS total, COUNT(*) AS qty')
  93. ->first();
  94. return ['value' => (float) $row->total, 'count' => (int) $row->qty];
  95. }
  96. private function stockValue(): float
  97. {
  98. // Valor total do estoque = soma de (preço de venda x quantidade) de todos os
  99. // produtos. Mesma base exibida por produto na aba de Produtos (price_sale * quantity).
  100. return (float) DB::table('products')
  101. ->whereNull('deleted_at')
  102. ->selectRaw('COALESCE(SUM(price_sale * quantity), 0) AS total')
  103. ->value('total');
  104. }
  105. private function bankBalanceTotal(): float
  106. {
  107. // Saldo da rede = créditos (chegam no to_account) menos débitos (saem do from_account),
  108. // somados por conta. No modelo de partida dobrada atual (from/to obrigatórios) as
  109. // transferências internas se anulam; o valor reflete apenas fluxos externos. O cálculo
  110. // de saldo real por banco será consolidado no #CB018 (Tesouraria), que definirá
  111. // saldo de abertura / contas de origem externas.
  112. $credits = (float) DB::table('treasury_launches')
  113. ->whereNull('deleted_at')
  114. ->sum('amount');
  115. $debits = (float) DB::table('treasury_launches')
  116. ->whereNull('deleted_at')
  117. ->sum('amount');
  118. return $credits - $debits;
  119. }
  120. private function lastTransactions(int $limit = 8): array
  121. {
  122. return DB::table('treasury_launches')
  123. ->whereNull('deleted_at')
  124. ->orderByDesc('launch_date')
  125. ->orderByDesc('id')
  126. ->limit($limit)
  127. ->get(['id', 'description', 'amount', 'is_reconciled'])
  128. ->map(fn ($t) => [
  129. 'id' => $t->id,
  130. 'description' => $t->description,
  131. 'value' => (float) $t->amount,
  132. 'status' => $t->is_reconciled ? 'Pago' : 'Pendente',
  133. ])
  134. ->all();
  135. }
  136. private function franchisePerformance(Carbon $start, Carbon $end): array
  137. {
  138. $revenue = DB::table('student_contract_installments')
  139. ->whereNull('deleted_at')
  140. ->where('status', ReceivableStatus::PAID->value)
  141. ->whereBetween('payment_date', [$start, $end])
  142. ->groupBy('unit_id')
  143. ->selectRaw('unit_id, SUM(paid_value) AS total')
  144. ->pluck('total', 'unit_id');
  145. // Despesa = total que a unidade deve pagar ao franqueador (TBR) acumulado
  146. // até o mês corrente (toda cobrança com vencimento até o fim do mês).
  147. $expense = DB::table('franchisee_account_receives')
  148. ->whereNull('deleted_at')
  149. ->where('due_date', '<=', $end)
  150. ->groupBy('unit_id')
  151. ->selectRaw('unit_id, SUM(value) AS total')
  152. ->pluck('total', 'unit_id');
  153. return DB::table('units')
  154. ->whereNull('deleted_at')
  155. ->get(['id', 'fantasy_name'])
  156. ->map(function ($unit) use ($revenue, $expense) {
  157. $rev = (float) ($revenue[$unit->id] ?? 0);
  158. $exp = (float) ($expense[$unit->id] ?? 0);
  159. return [
  160. 'id' => $unit->id,
  161. 'franchise' => $unit->fantasy_name,
  162. 'revenue' => $rev,
  163. 'expense' => $exp,
  164. 'performance' => $rev > 0 ? round((($rev - $exp) / $rev) * 100, 1) : 0,
  165. ];
  166. })
  167. ->all();
  168. }
  169. private function variation(float $current, float $previous): float
  170. {
  171. if ($previous <= 0) {
  172. return $current > 0 ? 100.0 : 0.0;
  173. }
  174. return round((($current - $previous) / $previous) * 100, 1);
  175. }
  176. }