| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- <?php
- namespace Tests\Unit\Financeiro;
- use App\Models\City;
- use App\Models\Country;
- use App\Models\FranchiseeContract;
- use App\Models\InhabitantClassification;
- use App\Models\MunicipalitySize;
- use App\Models\State;
- use App\Models\Tbr;
- use App\Models\Unit;
- use App\Services\TbrCalculationService;
- use Carbon\Carbon;
- use Illuminate\Foundation\Testing\RefreshDatabase;
- use Illuminate\Support\Facades\Bus;
- use PHPUnit\Framework\Attributes\DataProvider;
- use Tests\TestCase;
- /**
- * Reproduz, mês a mês, os quatro cenários que o cliente mandou em planilha Excel
- * — um por porte de município (até 50k, 50k-100k, 100k-200k, acima de 200k),
- * unidade Foz do Iguaçu, TBR R$ 1.621,00.
- *
- * Esses cenários foram o que confirmou a regra correta de Royalties: o percentual
- * da faixa de porte (40%/60%, 50%/75%, 75%/100%, 100%/150%) JÁ é o percentual
- * final aplicado direto sobre a TBR — sem nenhuma taxa-base intermediária vinda
- * de outra configuração. Ex.: mês 4, faixa 40%, TBR 1.621,00 -> Royalties fixo =
- * R$ 648,40 (0,40 x 1.621,00), batendo com a planilha.
- *
- * Os cenários de porte maior (100k-200k e acima de 200k) também confirmam que
- * Royalties e FNM são comparados de forma independente: nos meses 16-19 (GP) e
- * 16-29 (MGP) o FNM já vira "percentual do faturamento" enquanto os Royalties
- * continuam no fixo, exatamente como a planilha mostra.
- *
- * Cada linha testada é [mês de contrato, faturamento do mês, total esperado da
- * cobrança], exatamente como está na planilha do cliente.
- */
- class TbrCalculationClientScenariosTest extends TestCase
- {
- use RefreshDatabase;
- private const TBR_VALUE = 1621.00;
- private TbrCalculationService $service;
- private Unit $unitPequenoPorte;
- private Unit $unitMedioPorte;
- private Unit $unitGrandePorte;
- private Unit $unitMuitoGrandePorte;
- protected function setUp(): void
- {
- parent::setUp();
- Bus::fake();
- $this->service = new TbrCalculationService;
- $country = Country::create(['name' => 'Brasil', 'code' => 'BR']);
- $state = State::create(['name' => 'Paraná', 'code' => 'PR', 'country_id' => $country->id]);
- $city = City::create(['name' => 'Foz do Iguaçu', 'country_id' => $country->id, 'state_id' => $state->id]);
- $sizePP = MunicipalitySize::create(['acronym' => 'PP', 'description' => 'Até 50 mil habitantes']);
- $sizeMP = MunicipalitySize::create(['acronym' => 'MP', 'description' => 'De 50 mil a 100 mil habitantes']);
- $sizeGP = MunicipalitySize::create(['acronym' => 'GP', 'description' => 'De 100 mil a 200 mil habitantes']);
- $sizeMGP = MunicipalitySize::create(['acronym' => 'MGP', 'description' => 'Acima de 200 mil habitantes']);
- // Faixas exatamente como nos cenários do cliente: 40%/60% (PP), 50%/75% (MP),
- // 75%/100% (GP), 100%/150% (MGP).
- $this->makeBrackets($sizePP->id, [[1, 3, 0.0], [4, 12, 0.40], [13, 60, 0.60]]);
- $this->makeBrackets($sizeMP->id, [[1, 3, 0.0], [4, 12, 0.50], [13, 60, 0.75]]);
- $this->makeBrackets($sizeGP->id, [[1, 3, 0.0], [4, 12, 0.75], [13, 60, 1.00]]);
- $this->makeBrackets($sizeMGP->id, [[1, 3, 0.0], [4, 12, 1.00], [13, 60, 1.50]]);
- // TBR e percentuais fixos de FNM/Manutenção iguais em todos os cenários da planilha.
- Tbr::create([
- 'year' => 2026, 'tbr_value' => self::TBR_VALUE,
- 'royalties_percentage' => 0.08, 'fnm_percentage' => 0.20, 'maintenance_percentage' => 0.30,
- ]);
- $this->unitPequenoPorte = $this->makeUnit('Foz do Iguaçu (até 50k)', $city, $state, '00000000000191');
- $this->unitMedioPorte = $this->makeUnit('Foz do Iguaçu (50k a 100k)', $city, $state, '00000000000272');
- $this->unitGrandePorte = $this->makeUnit('Foz do Iguaçu (100k a 200k)', $city, $state, '00000000010316');
- $this->unitMuitoGrandePorte = $this->makeUnit('Foz do Iguaçu (acima 200k)', $city, $state, '00000000020460');
- $this->makeContract($this->unitPequenoPorte->id, $sizePP->id);
- $this->makeContract($this->unitMedioPorte->id, $sizeMP->id);
- $this->makeContract($this->unitGrandePorte->id, $sizeGP->id);
- $this->makeContract($this->unitMuitoGrandePorte->id, $sizeMGP->id);
- }
- private function makeUnit(string $name, City $city, State $state, string $cnpj): Unit
- {
- return Unit::create([
- 'fantasy_name' => $name, 'social_reason' => $name.' LTDA',
- 'cnpj' => $cnpj,
- 'street' => 'Rua Teste', 'neighborhood' => 'Centro', 'postal_code' => '85850000',
- 'city_id' => $city->id, 'state_id' => $state->id,
- 'email' => strtolower(str_replace(' ', '.', $name)).'@teste.com', 'name_responsible' => 'Responsável Teste',
- ]);
- }
- /** @param array<int, array{0:int,1:?int,2:float}> $brackets [start, end, percentual] */
- private function makeBrackets(int $sizeId, array $brackets): void
- {
- foreach ($brackets as [$start, $end, $percentage]) {
- InhabitantClassification::create([
- 'municipality_size_id' => $sizeId, 'description' => "Faixa {$start}-{$end}",
- 'start' => $start, 'end' => $end, 'tbr_percentage' => $percentage, 'is_renewal' => false,
- ]);
- }
- }
- private function makeContract(int $unitId, int $municipalitySizeId): FranchiseeContract
- {
- return FranchiseeContract::create([
- 'unit_id' => $unitId, 'protocol' => $unitId, 'name' => 'Contrato de Franquia',
- 'description' => 'Contrato de teste', 'start_date' => '2026-01-01', 'end_date' => '2031-12-31',
- 'signature_date' => '2026-01-01', 'validity_months' => 60, 'invoice_due_date' => 10,
- 'municipality_size_id' => $municipalitySizeId, 'tbr_fixed_value' => self::TBR_VALUE,
- ]);
- }
- /** Competência (ano/mês) que faz o mês de contrato bater com $contractMonth, dado início em 01/2026. */
- private function referenceFor(int $contractMonth): array
- {
- $date = Carbon::create(2026, 1, 1)->addMonthsNoOverflow($contractMonth - 1);
- return [$date->year, $date->month];
- }
- #[DataProvider('cenario50kProvider')]
- public function test_cenario_50k_pequeno_porte(int $contractMonth, float $revenue, float $expectedTotal): void
- {
- [$year, $month] = $this->referenceFor($contractMonth);
- $result = $this->service->preview([
- 'unit_id' => $this->unitPequenoPorte->id,
- 'reference_year' => $year,
- 'reference_month' => $month,
- 'revenue_value' => $revenue,
- ]);
- $this->assertSame($contractMonth, $result['contract_month_reference']);
- $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
- }
- #[DataProvider('cenario50kA100kProvider')]
- public function test_cenario_50k_a_100k_medio_porte(int $contractMonth, float $revenue, float $expectedTotal): void
- {
- [$year, $month] = $this->referenceFor($contractMonth);
- $result = $this->service->preview([
- 'unit_id' => $this->unitMedioPorte->id,
- 'reference_year' => $year,
- 'reference_month' => $month,
- 'revenue_value' => $revenue,
- ]);
- $this->assertSame($contractMonth, $result['contract_month_reference']);
- $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
- }
- /** [mês de contrato, faturamento, total esperado] — planilha "Cenário 50K". */
- public static function cenario50kProvider(): array
- {
- $rows = [
- [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
- [4, 5000, 1458.90], [5, 6000, 1458.90], [6, 7000, 1458.90], [7, 8000, 1458.90],
- [8, 9000, 1530.50], [9, 10000, 1610.50], [10, 11000, 1690.50], [11, 12000, 1770.50], [12, 13000, 1850.50],
- [13, 14000, 1930.50], [14, 15000, 2010.50], [15, 16000, 2090.50],
- ];
- // A partir do mês 16, tanto Royalties quanto FNM já usam o percentual do
- // faturamento (8% + 2% = 10%), então o total é sempre faturamento x 10% + R$ 486,30 de manutenção.
- for ($month = 16; $month <= 60; $month++) {
- $revenue = 1000 * $month + 1000;
- $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
- }
- return self::keyed($rows);
- }
- /** [mês de contrato, faturamento, total esperado] — planilha "Cenário 50K a 100K". */
- public static function cenario50kA100kProvider(): array
- {
- $rows = [
- [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
- [4, 5000, 1621.00], [5, 6000, 1621.00], [6, 7000, 1621.00], [7, 8000, 1621.00],
- [8, 9000, 1621.00], [9, 10000, 1621.00],
- [10, 11000, 1690.50], [11, 12000, 1770.50], [12, 13000, 1850.50],
- [13, 14000, 2026.25], [14, 15000, 2026.25], [15, 16000, 2090.50],
- ];
- // A partir do mês 16 o comportamento é idêntico ao cenário PP (ambos já
- // totalmente no percentual do faturamento).
- for ($month = 16; $month <= 60; $month++) {
- $revenue = 1000 * $month + 1000;
- $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
- }
- return self::keyed($rows);
- }
- #[DataProvider('cenario100kA200kProvider')]
- public function test_cenario_100k_a_200k_grande_porte(int $contractMonth, float $revenue, float $expectedTotal): void
- {
- [$year, $month] = $this->referenceFor($contractMonth);
- $result = $this->service->preview([
- 'unit_id' => $this->unitGrandePorte->id,
- 'reference_year' => $year,
- 'reference_month' => $month,
- 'revenue_value' => $revenue,
- ]);
- $this->assertSame($contractMonth, $result['contract_month_reference']);
- $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
- }
- #[DataProvider('cenarioAcima200kProvider')]
- public function test_cenario_acima_200k_porte_muito_grande(int $contractMonth, float $revenue, float $expectedTotal): void
- {
- [$year, $month] = $this->referenceFor($contractMonth);
- $result = $this->service->preview([
- 'unit_id' => $this->unitMuitoGrandePorte->id,
- 'reference_year' => $year,
- 'reference_month' => $month,
- 'revenue_value' => $revenue,
- ]);
- $this->assertSame($contractMonth, $result['contract_month_reference']);
- $this->assertEqualsWithDelta($expectedTotal, $result['final_value'], 0.01, "mês {$contractMonth}");
- }
- /** [mês de contrato, faturamento, total esperado] — planilha "100 a 200k". */
- public static function cenario100kA200kProvider(): array
- {
- $rows = [
- [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
- // Royalties fixo (75% x 1.621 = 1.215,75) + FNM fixo (324,20) + TM (486,30).
- [4, 5000, 2026.25], [5, 6000, 2026.25], [6, 7000, 2026.25], [7, 8000, 2026.25],
- [8, 9000, 2026.25], [9, 10000, 2026.25], [10, 11000, 2026.25], [11, 12000, 2026.25], [12, 13000, 2026.25],
- // Mês 13: faixa muda pra 100% (1.621,00 fixo). Royalties e FNM ainda fixos.
- [13, 14000, 2431.50], [14, 15000, 2431.50], [15, 16000, 2431.50],
- // Meses 16-19: FNM já virou percentual do faturamento (2%), Royalties continua fixo em 1.621,00.
- [16, 17000, 2447.30], [17, 18000, 2467.30], [18, 19000, 2487.30], [19, 20000, 2507.30],
- ];
- // A partir do mês 20, Royalties também vira percentual do faturamento (8%): total = 10% x faturamento + 486,30.
- for ($month = 20; $month <= 60; $month++) {
- $revenue = 1000 * $month + 1000;
- $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
- }
- return self::keyed($rows);
- }
- /** [mês de contrato, faturamento, total esperado] — planilha "Acima de 200k". */
- public static function cenarioAcima200kProvider(): array
- {
- $rows = [
- [1, 0, 486.30], [2, 0, 486.30], [3, 0, 486.30],
- // Royalties fixo (100% x 1.621 = 1.621,00) + FNM fixo (324,20) + TM (486,30).
- [4, 5000, 2431.50], [5, 6000, 2431.50], [6, 7000, 2431.50], [7, 8000, 2431.50],
- [8, 9000, 2431.50], [9, 10000, 2431.50], [10, 11000, 2431.50], [11, 12000, 2431.50], [12, 13000, 2431.50],
- ];
- // Mês 13: faixa muda pra 150% (2.431,50 fixo). Royalties e FNM ainda fixos até o mês 15.
- foreach ([13, 14, 15] as $month) {
- $revenue = 1000 * $month + 1000;
- $rows[] = [$month, $revenue, 3242.00];
- }
- // Meses 16-29: FNM já virou percentual do faturamento (2%), Royalties continua fixo em 2.431,50.
- for ($month = 16; $month <= 29; $month++) {
- $revenue = 1000 * $month + 1000;
- $rows[] = [$month, $revenue, round(2431.50 + 486.30 + $revenue * 0.02, 2)];
- }
- // A partir do mês 30, Royalties também vira percentual do faturamento (8%): total = 10% x faturamento + 486,30.
- for ($month = 30; $month <= 60; $month++) {
- $revenue = 1000 * $month + 1000;
- $rows[] = [$month, $revenue, round($revenue * 0.10 + 486.30, 2)];
- }
- return self::keyed($rows);
- }
- /** @param array<int, array{0:int,1:float,2:float}> $rows */
- private static function keyed(array $rows): array
- {
- $keyed = [];
- foreach ($rows as $row) {
- $keyed["mês {$row[0]}"] = $row;
- }
- return $keyed;
- }
- }
|