TbrCalculationClientScenariosTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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 quatro cenários que o cliente mandou em planilha Excel
  19. * — um por porte de município (até 50k, 50k-100k, 100k-200k, acima de 200k),
  20. * unidade Foz do Iguaçu, TBR R$ 1.621,00.
  21. *
  22. * Esses cenários foram o que confirmou a regra correta de Royalties: o percentual
  23. * da faixa de porte (40%/60%, 50%/75%, 75%/100%, 100%/150%) JÁ é o percentual
  24. * final aplicado direto sobre a TBR — sem nenhuma taxa-base intermediária vinda
  25. * de outra configuração. Ex.: mês 4, faixa 40%, TBR 1.621,00 -> Royalties fixo =
  26. * R$ 648,40 (0,40 x 1.621,00), batendo com a planilha.
  27. *
  28. * Os cenários de porte maior (100k-200k e acima de 200k) também confirmam que
  29. * Royalties e FNM são comparados de forma independente: nos meses 16-19 (GP) e
  30. * 16-29 (MGP) o FNM já vira "percentual do faturamento" enquanto os Royalties
  31. * continuam no fixo, exatamente como a planilha mostra.
  32. *
  33. * Cada linha testada é [mês de contrato, faturamento do mês, total esperado da
  34. * cobrança], exatamente como está na planilha do cliente.
  35. */
  36. class TbrCalculationClientScenariosTest extends TestCase
  37. {
  38. use RefreshDatabase;
  39. private const TBR_VALUE = 1621.00;
  40. private TbrCalculationService $service;
  41. private Unit $unitPequenoPorte;
  42. private Unit $unitMedioPorte;
  43. private Unit $unitGrandePorte;
  44. private Unit $unitMuitoGrandePorte;
  45. protected function setUp(): void
  46. {
  47. parent::setUp();
  48. Bus::fake();
  49. $this->service = new TbrCalculationService;
  50. $country = Country::create(['name' => 'Brasil', 'code' => 'BR']);
  51. $state = State::create(['name' => 'Paraná', 'code' => 'PR', 'country_id' => $country->id]);
  52. $city = City::create(['name' => 'Foz do Iguaçu', 'country_id' => $country->id, 'state_id' => $state->id]);
  53. $sizePP = MunicipalitySize::create(['acronym' => 'PP', 'description' => 'Até 50 mil habitantes']);
  54. $sizeMP = MunicipalitySize::create(['acronym' => 'MP', 'description' => 'De 50 mil a 100 mil habitantes']);
  55. $sizeGP = MunicipalitySize::create(['acronym' => 'GP', 'description' => 'De 100 mil a 200 mil habitantes']);
  56. $sizeMGP = MunicipalitySize::create(['acronym' => 'MGP', 'description' => 'Acima de 200 mil habitantes']);
  57. // Faixas exatamente como nos cenários do cliente: 40%/60% (PP), 50%/75% (MP),
  58. // 75%/100% (GP), 100%/150% (MGP).
  59. $this->makeBrackets($sizePP->id, [[1, 3, 0.0], [4, 12, 0.40], [13, 60, 0.60]]);
  60. $this->makeBrackets($sizeMP->id, [[1, 3, 0.0], [4, 12, 0.50], [13, 60, 0.75]]);
  61. $this->makeBrackets($sizeGP->id, [[1, 3, 0.0], [4, 12, 0.75], [13, 60, 1.00]]);
  62. $this->makeBrackets($sizeMGP->id, [[1, 3, 0.0], [4, 12, 1.00], [13, 60, 1.50]]);
  63. // TBR e percentuais fixos de FNM/Manutenção iguais em todos os cenários da planilha.
  64. Tbr::create([
  65. 'year' => 2026, 'tbr_value' => self::TBR_VALUE,
  66. 'royalties_percentage' => 0.08, 'fnm_percentage' => 0.20, 'maintenance_percentage' => 0.30,
  67. ]);
  68. $this->unitPequenoPorte = $this->makeUnit('Foz do Iguaçu (até 50k)', $city, $state, '00000000000191');
  69. $this->unitMedioPorte = $this->makeUnit('Foz do Iguaçu (50k a 100k)', $city, $state, '00000000000272');
  70. $this->unitGrandePorte = $this->makeUnit('Foz do Iguaçu (100k a 200k)', $city, $state, '00000000010316');
  71. $this->unitMuitoGrandePorte = $this->makeUnit('Foz do Iguaçu (acima 200k)', $city, $state, '00000000020460');
  72. $this->makeContract($this->unitPequenoPorte->id, $sizePP->id);
  73. $this->makeContract($this->unitMedioPorte->id, $sizeMP->id);
  74. $this->makeContract($this->unitGrandePorte->id, $sizeGP->id);
  75. $this->makeContract($this->unitMuitoGrandePorte->id, $sizeMGP->id);
  76. }
  77. private function makeUnit(string $name, City $city, State $state, string $cnpj): Unit
  78. {
  79. return Unit::create([
  80. 'fantasy_name' => $name, 'social_reason' => $name.' LTDA',
  81. 'cnpj' => $cnpj,
  82. 'street' => 'Rua Teste', 'neighborhood' => 'Centro', 'postal_code' => '85850000',
  83. 'city_id' => $city->id, 'state_id' => $state->id,
  84. 'email' => strtolower(str_replace(' ', '.', $name)).'@teste.com', 'name_responsible' => 'Responsável Teste',
  85. ]);
  86. }
  87. /** @param array<int, array{0:int,1:?int,2:float}> $brackets [start, end, percentual] */
  88. private function makeBrackets(int $sizeId, array $brackets): void
  89. {
  90. foreach ($brackets as [$start, $end, $percentage]) {
  91. InhabitantClassification::create([
  92. 'municipality_size_id' => $sizeId, 'description' => "Faixa {$start}-{$end}",
  93. 'start' => $start, 'end' => $end, 'tbr_percentage' => $percentage, 'is_renewal' => false,
  94. ]);
  95. }
  96. }
  97. private function makeContract(int $unitId, int $municipalitySizeId): FranchiseeContract
  98. {
  99. return FranchiseeContract::create([
  100. 'unit_id' => $unitId, 'protocol' => $unitId, 'name' => 'Contrato de Franquia',
  101. 'description' => 'Contrato de teste', 'start_date' => '2026-01-01', 'end_date' => '2031-12-31',
  102. 'signature_date' => '2026-01-01', 'validity_months' => 60, 'invoice_due_date' => 10,
  103. 'municipality_size_id' => $municipalitySizeId, 'tbr_fixed_value' => self::TBR_VALUE,
  104. ]);
  105. }
  106. /** Competência (ano/mês) que faz o mês de contrato bater com $contractMonth, dado início em 01/2026. */
  107. private function referenceFor(int $contractMonth): array
  108. {
  109. $date = Carbon::create(2026, 1, 1)->addMonthsNoOverflow($contractMonth - 1);
  110. return [$date->year, $date->month];
  111. }
  112. #[DataProvider('cenario50kProvider')]
  113. public function test_cenario_50k_pequeno_porte(int $contractMonth, float $revenue, float $expectedTotal): void
  114. {
  115. [$year, $month] = $this->referenceFor($contractMonth);
  116. $result = $this->service->preview([
  117. 'unit_id' => $this->unitPequenoPorte->id,
  118. 'reference_year' => $year,
  119. 'reference_month' => $month,
  120. 'revenue_value' => $revenue,
  121. ]);
  122. $this->assertSame($contractMonth, $result['contract_month_reference']);
  123. $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
  124. }
  125. #[DataProvider('cenario50kA100kProvider')]
  126. public function test_cenario_50k_a_100k_medio_porte(int $contractMonth, float $revenue, float $expectedTotal): void
  127. {
  128. [$year, $month] = $this->referenceFor($contractMonth);
  129. $result = $this->service->preview([
  130. 'unit_id' => $this->unitMedioPorte->id,
  131. 'reference_year' => $year,
  132. 'reference_month' => $month,
  133. 'revenue_value' => $revenue,
  134. ]);
  135. $this->assertSame($contractMonth, $result['contract_month_reference']);
  136. $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
  137. }
  138. /** [mês de contrato, faturamento, total esperado] — planilha "Cenário 50K". */
  139. public static function cenario50kProvider(): array
  140. {
  141. $rows = [
  142. [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
  143. [4, 5000, 1458.90], [5, 6000, 1458.90], [6, 7000, 1458.90], [7, 8000, 1458.90],
  144. [8, 9000, 1530.50], [9, 10000, 1610.50], [10, 11000, 1690.50], [11, 12000, 1770.50], [12, 13000, 1850.50],
  145. [13, 14000, 1930.50], [14, 15000, 2010.50], [15, 16000, 2090.50],
  146. ];
  147. // A partir do mês 16, tanto Royalties quanto FNM já usam o percentual do
  148. // faturamento (8% + 2% = 10%), então o total é sempre faturamento x 10% + R$ 486,30 de manutenção.
  149. for ($month = 16; $month <= 60; $month++) {
  150. $revenue = 1000 * $month + 1000;
  151. $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
  152. }
  153. return self::keyed($rows);
  154. }
  155. /** [mês de contrato, faturamento, total esperado] — planilha "Cenário 50K a 100K". */
  156. public static function cenario50kA100kProvider(): array
  157. {
  158. $rows = [
  159. [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
  160. [4, 5000, 1621.00], [5, 6000, 1621.00], [6, 7000, 1621.00], [7, 8000, 1621.00],
  161. [8, 9000, 1621.00], [9, 10000, 1621.00],
  162. [10, 11000, 1690.50], [11, 12000, 1770.50], [12, 13000, 1850.50],
  163. [13, 14000, 2026.25], [14, 15000, 2026.25], [15, 16000, 2090.50],
  164. ];
  165. // A partir do mês 16 o comportamento é idêntico ao cenário PP (ambos já
  166. // totalmente no percentual do faturamento).
  167. for ($month = 16; $month <= 60; $month++) {
  168. $revenue = 1000 * $month + 1000;
  169. $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
  170. }
  171. return self::keyed($rows);
  172. }
  173. #[DataProvider('cenario100kA200kProvider')]
  174. public function test_cenario_100k_a_200k_grande_porte(int $contractMonth, float $revenue, float $expectedTotal): void
  175. {
  176. [$year, $month] = $this->referenceFor($contractMonth);
  177. $result = $this->service->preview([
  178. 'unit_id' => $this->unitGrandePorte->id,
  179. 'reference_year' => $year,
  180. 'reference_month' => $month,
  181. 'revenue_value' => $revenue,
  182. ]);
  183. $this->assertSame($contractMonth, $result['contract_month_reference']);
  184. $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
  185. }
  186. #[DataProvider('cenarioAcima200kProvider')]
  187. public function test_cenario_acima_200k_porte_muito_grande(int $contractMonth, float $revenue, float $expectedTotal): void
  188. {
  189. [$year, $month] = $this->referenceFor($contractMonth);
  190. $result = $this->service->preview([
  191. 'unit_id' => $this->unitMuitoGrandePorte->id,
  192. 'reference_year' => $year,
  193. 'reference_month' => $month,
  194. 'revenue_value' => $revenue,
  195. ]);
  196. $this->assertSame($contractMonth, $result['contract_month_reference']);
  197. $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
  198. }
  199. /** [mês de contrato, faturamento, total esperado] — planilha "100 a 200k". */
  200. public static function cenario100kA200kProvider(): array
  201. {
  202. $rows = [
  203. [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
  204. // Royalties fixo (75% x 1.621 = 1.215,75) + FNM fixo (324,20) + TM (486,30).
  205. [4, 5000, 2026.25], [5, 6000, 2026.25], [6, 7000, 2026.25], [7, 8000, 2026.25],
  206. [8, 9000, 2026.25], [9, 10000, 2026.25], [10, 11000, 2026.25], [11, 12000, 2026.25], [12, 13000, 2026.25],
  207. // Mês 13: faixa muda pra 100% (1.621,00 fixo). Royalties e FNM ainda fixos.
  208. [13, 14000, 2431.50], [14, 15000, 2431.50], [15, 16000, 2431.50],
  209. // Meses 16-19: FNM já virou percentual do faturamento (2%), Royalties continua fixo em 1.621,00.
  210. [16, 17000, 2447.30], [17, 18000, 2467.30], [18, 19000, 2487.30], [19, 20000, 2507.30],
  211. ];
  212. // A partir do mês 20, Royalties também vira percentual do faturamento (8%): total = 10% x faturamento + 486,30.
  213. for ($month = 20; $month <= 60; $month++) {
  214. $revenue = 1000 * $month + 1000;
  215. $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
  216. }
  217. return self::keyed($rows);
  218. }
  219. /** [mês de contrato, faturamento, total esperado] — planilha "Acima de 200k". */
  220. public static function cenarioAcima200kProvider(): array
  221. {
  222. $rows = [
  223. [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
  224. // Royalties fixo (100% x 1.621 = 1.621,00) + FNM fixo (324,20) + TM (486,30).
  225. [4, 5000, 2431.50], [5, 6000, 2431.50], [6, 7000, 2431.50], [7, 8000, 2431.50],
  226. [8, 9000, 2431.50], [9, 10000, 2431.50], [10, 11000, 2431.50], [11, 12000, 2431.50], [12, 13000, 2431.50],
  227. ];
  228. // Mês 13: faixa muda pra 150% (2.431,50 fixo). Royalties e FNM ainda fixos até o mês 15.
  229. foreach ([13, 14, 15] as $month) {
  230. $revenue = 1000 * $month + 1000;
  231. $rows[] = [$month, $revenue, 3242.00];
  232. }
  233. // Meses 16-29: FNM já virou percentual do faturamento (2%), Royalties continua fixo em 2.431,50.
  234. for ($month = 16; $month <= 29; $month++) {
  235. $revenue = 1000 * $month + 1000;
  236. $rows[] = [$month, $revenue, round(2431.50 + 486.30 + $revenue * 0.02, 2)];
  237. }
  238. // A partir do mês 30, Royalties também vira percentual do faturamento (8%): total = 10% x faturamento + 486,30.
  239. for ($month = 30; $month <= 60; $month++) {
  240. $revenue = 1000 * $month + 1000;
  241. $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
  242. }
  243. return self::keyed($rows);
  244. }
  245. /** @param array<int, array{0:int,1:float,2:float}> $rows */
  246. private static function keyed(array $rows): array
  247. {
  248. $keyed = [];
  249. foreach ($rows as $row) {
  250. $keyed["mês {$row[0]}"] = $row;
  251. }
  252. return $keyed;
  253. }
  254. }