| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- <?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 dois cenários que o cliente mandou em planilha Excel
- * ("Cenário 50K" e "Cenário 50K a 100K", 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% para até 50k habitantes; 50%/75% para 50k-100k) 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.
- *
- * 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;
- 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']);
- // Faixas exatamente como no cenário do cliente: 40%/60% (PP) e 50%/75% (MP).
- $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]]);
- // TBR e percentuais fixos de FNM/Manutenção iguais em ambos 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->makeContract($this->unitPequenoPorte->id, $sizePP->id);
- $this->makeContract($this->unitMedioPorte->id, $sizeMP->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);
- }
- /** @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;
- }
- }
|