FinancialDashboardService.php 7.8 KB

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