TbrCalculationClientScenariosTest.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <?php
  2. namespace Tests\Unit\Financeiro;
  3. use App\Models\City;
  4. use App\Models\Country;
  5. use App\Models\FranchiseeContract;
  6. use App\Models\InhabitantClassification;
  7. use App\Models\MunicipalitySize;
  8. use App\Models\State;
  9. use App\Models\Tbr;
  10. use App\Models\Unit;
  11. use App\Services\TbrCalculationService;
  12. use Carbon\Carbon;
  13. use Illuminate\Foundation\Testing\RefreshDatabase;
  14. use Illuminate\Support\Facades\Bus;
  15. use PHPUnit\Framework\Attributes\DataProvider;
  16. use Tests\TestCase;
  17. /**
  18. * Reproduz, mês a mês, os dois cenários que o cliente mandou em planilha Excel
  19. * ("Cenário 50K" e "Cenário 50K a 100K", unidade Foz do Iguaçu, TBR R$ 1.621,00).
  20. *
  21. * Esses cenários foram o que confirmou a regra correta de Royalties: o percentual
  22. * da faixa de porte (40%/60% para até 50k habitantes; 50%/75% para 50k-100k) JÁ é
  23. * o percentual final aplicado direto sobre a TBR — sem nenhuma taxa-base
  24. * intermediária vinda de outra configuração. Ex.: mês 4, faixa 40%, TBR 1.621,00
  25. * -> Royalties fixo = R$ 648,40 (0,40 x 1.621,00), batendo com a planilha.
  26. *
  27. * Cada linha testada é [mês de contrato, faturamento do mês, total esperado da
  28. * cobrança], exatamente como está na planilha do cliente.
  29. */
  30. class TbrCalculationClientScenariosTest extends TestCase
  31. {
  32. use RefreshDatabase;
  33. private const TBR_VALUE = 1621.00;
  34. private TbrCalculationService $service;
  35. private Unit $unitPequenoPorte;
  36. private Unit $unitMedioPorte;
  37. protected function setUp(): void
  38. {
  39. parent::setUp();
  40. Bus::fake();
  41. $this->service = new TbrCalculationService;
  42. $country = Country::create(['name' => 'Brasil', 'code' => 'BR']);
  43. $state = State::create(['name' => 'Paraná', 'code' => 'PR', 'country_id' => $country->id]);
  44. $city = City::create(['name' => 'Foz do Iguaçu', 'country_id' => $country->id, 'state_id' => $state->id]);
  45. $sizePP = MunicipalitySize::create(['acronym' => 'PP', 'description' => 'Até 50 mil habitantes']);
  46. $sizeMP = MunicipalitySize::create(['acronym' => 'MP', 'description' => 'De 50 mil a 100 mil habitantes']);
  47. // Faixas exatamente como no cenário do cliente: 40%/60% (PP) e 50%/75% (MP).
  48. $this->makeBrackets($sizePP->id, [[1, 3, 0.0], [4, 12, 0.40], [13, 60, 0.60]]);
  49. $this->makeBrackets($sizeMP->id, [[1, 3, 0.0], [4, 12, 0.50], [13, 60, 0.75]]);
  50. // TBR e percentuais fixos de FNM/Manutenção iguais em ambos os cenários da planilha.
  51. Tbr::create([
  52. 'year' => 2026, 'tbr_value' => self::TBR_VALUE,
  53. 'royalties_percentage' => 0.08, 'fnm_percentage' => 0.20, 'maintenance_percentage' => 0.30,
  54. ]);
  55. $this->unitPequenoPorte = $this->makeUnit('Foz do Iguaçu (até 50k)', $city, $state, '00000000000191');
  56. $this->unitMedioPorte = $this->makeUnit('Foz do Iguaçu (50k a 100k)', $city, $state, '00000000000272');
  57. $this->makeContract($this->unitPequenoPorte->id, $sizePP->id);
  58. $this->makeContract($this->unitMedioPorte->id, $sizeMP->id);
  59. }
  60. private function makeUnit(string $name, City $city, State $state, string $cnpj): Unit
  61. {
  62. return Unit::create([
  63. 'fantasy_name' => $name, 'social_reason' => $name.' LTDA',
  64. 'cnpj' => $cnpj,
  65. 'street' => 'Rua Teste', 'neighborhood' => 'Centro', 'postal_code' => '85850000',
  66. 'city_id' => $city->id, 'state_id' => $state->id,
  67. 'email' => strtolower(str_replace(' ', '.', $name)).'@teste.com', 'name_responsible' => 'Responsável Teste',
  68. ]);
  69. }
  70. /** @param array<int, array{0:int,1:?int,2:float}> $brackets [start, end, percentual] */
  71. private function makeBrackets(int $sizeId, array $brackets): void
  72. {
  73. foreach ($brackets as [$start, $end, $percentage]) {
  74. InhabitantClassification::create([
  75. 'municipality_size_id' => $sizeId, 'description' => "Faixa {$start}-{$end}",
  76. 'start' => $start, 'end' => $end, 'tbr_percentage' => $percentage, 'is_renewal' => false,
  77. ]);
  78. }
  79. }
  80. private function makeContract(int $unitId, int $municipalitySizeId): FranchiseeContract
  81. {
  82. return FranchiseeContract::create([
  83. 'unit_id' => $unitId, 'protocol' => $unitId, 'name' => 'Contrato de Franquia',
  84. 'description' => 'Contrato de teste', 'start_date' => '2026-01-01', 'end_date' => '2031-12-31',
  85. 'signature_date' => '2026-01-01', 'validity_months' => 60, 'invoice_due_date' => 10,
  86. 'municipality_size_id' => $municipalitySizeId, 'tbr_fixed_value' => self::TBR_VALUE,
  87. ]);
  88. }
  89. /** Competência (ano/mês) que faz o mês de contrato bater com $contractMonth, dado início em 01/2026. */
  90. private function referenceFor(int $contractMonth): array
  91. {
  92. $date = Carbon::create(2026, 1, 1)->addMonthsNoOverflow($contractMonth - 1);
  93. return [$date->year, $date->month];
  94. }
  95. #[DataProvider('cenario50kProvider')]
  96. public function test_cenario_50k_pequeno_porte(int $contractMonth, float $revenue, float $expectedTotal): void
  97. {
  98. [$year, $month] = $this->referenceFor($contractMonth);
  99. $result = $this->service->preview([
  100. 'unit_id' => $this->unitPequenoPorte->id,
  101. 'reference_year' => $year,
  102. 'reference_month' => $month,
  103. 'revenue_value' => $revenue,
  104. ]);
  105. $this->assertSame($contractMonth, $result['contract_month_reference']);
  106. $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
  107. }
  108. #[DataProvider('cenario50kA100kProvider')]
  109. public function test_cenario_50k_a_100k_medio_porte(int $contractMonth, float $revenue, float $expectedTotal): void
  110. {
  111. [$year, $month] = $this->referenceFor($contractMonth);
  112. $result = $this->service->preview([
  113. 'unit_id' => $this->unitMedioPorte->id,
  114. 'reference_year' => $year,
  115. 'reference_month' => $month,
  116. 'revenue_value' => $revenue,
  117. ]);
  118. $this->assertSame($contractMonth, $result['contract_month_reference']);
  119. $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
  120. }
  121. /** [mês de contrato, faturamento, total esperado] — planilha "Cenário 50K". */
  122. public static function cenario50kProvider(): array
  123. {
  124. $rows = [
  125. [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
  126. [4, 5000, 1458.90], [5, 6000, 1458.90], [6, 7000, 1458.90], [7, 8000, 1458.90],
  127. [8, 9000, 1530.50], [9, 10000, 1610.50], [10, 11000, 1690.50], [11, 12000, 1770.50], [12, 13000, 1850.50],
  128. [13, 14000, 1930.50], [14, 15000, 2010.50], [15, 16000, 2090.50],
  129. ];
  130. // A partir do mês 16, tanto Royalties quanto FNM já usam o percentual do
  131. // faturamento (8% + 2% = 10%), então o total é sempre faturamento x 10% + R$ 486,30 de manutenção.
  132. for ($month = 16; $month <= 60; $month++) {
  133. $revenue = 1000 * $month + 1000;
  134. $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
  135. }
  136. return self::keyed($rows);
  137. }
  138. /** [mês de contrato, faturamento, total esperado] — planilha "Cenário 50K a 100K". */
  139. public static function cenario50kA100kProvider(): array
  140. {
  141. $rows = [
  142. [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
  143. [4, 5000, 1621.00], [5, 6000, 1621.00], [6, 7000, 1621.00], [7, 8000, 1621.00],
  144. [8, 9000, 1621.00], [9, 10000, 1621.00],
  145. [10, 11000, 1690.50], [11, 12000, 1770.50], [12, 13000, 1850.50],
  146. [13, 14000, 2026.25], [14, 15000, 2026.25], [15, 16000, 2090.50],
  147. ];
  148. // A partir do mês 16 o comportamento é idêntico ao cenário PP (ambos já
  149. // totalmente no percentual do faturamento).
  150. for ($month = 16; $month <= 60; $month++) {
  151. $revenue = 1000 * $month + 1000;
  152. $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
  153. }
  154. return self::keyed($rows);
  155. }
  156. /** @param array<int, array{0:int,1:float,2:float}> $rows */
  157. private static function keyed(array $rows): array
  158. {
  159. $keyed = [];
  160. foreach ($rows as $row) {
  161. $keyed["mês {$row[0]}"] = $row;
  162. }
  163. return $keyed;
  164. }
  165. }