Ver código fonte

feat: update royalties calculation logic to use configurable percentage and enhance test coverage

alvesantos 1 dia atrás
pai
commit
a4b7e7a1ad

+ 14 - 7
app/Services/TbrCalculationService.php

@@ -330,9 +330,11 @@ private function buildPreview(FranchiseeContract $contract, int $referenceYear,
 
         $fnmPercentageConfig = $tbrConfig ? (float) $tbrConfig->fnm_percentage : self::FNM_BRACKET_PERCENTAGE;
         $maintenancePercentageConfig = $tbrConfig ? (float) $tbrConfig->maintenance_percentage : self::MAINTENANCE_RATE;
-        // % base de royalties ("Atribuído em Configurações do TBR", tbrs.royalties_percentage):
-        // a faixa de porte funciona como multiplicador dessa base, não como percentual final.
-        $royaltiesBaseRate = $tbrConfig ? (float) $tbrConfig->royalties_percentage : self::ROYALTIES_REVENUE_RATE;
+        // % de faturamento dos Royalties ("Royalties %" em Configurações do TBR):
+        // controla só o lado do faturamento da comparação "maior valor" — o lado
+        // fixo continua vindo direto da faixa de porte (Classificação de Habitantes),
+        // sem escala nenhuma, confirmado pelos cenários reais do cliente.
+        $royaltiesRevenueRate = $tbrConfig ? (float) $tbrConfig->royalties_percentage : self::ROYALTIES_REVENUE_RATE;
 
         $contractMonth = $this->resolveContractMonth($contract->start_date, $referenceYear, $referenceMonth);
         $municipalitySizeId = (int) $contract->municipality_size_id;
@@ -343,11 +345,14 @@ private function buildPreview(FranchiseeContract $contract, int $referenceYear,
         $chargeFnm = $financial ? (bool) $financial->charge_fnm : true;
 
         // Royalties só busca a faixa quando há cobrança; desligado não exige faixa.
+        // O percentual da faixa de porte JÁ é o percentual final do lado FIXO sobre a
+        // TBR (confirmado pelos cenários do cliente: faixa 40%/1.621 = R$648,40 direto,
+        // sem taxa-base intermediária). tbrs.royalties_percentage não entra nesse lado —
+        // ele controla só o percentual do lado FATURAMENTO (ver $royaltiesRevenueRate).
         $royaltiesBracket = $chargeRoi
             ? $this->findRoyaltiesBracket($municipalitySizeId, $contractMonth)
             : null;
-        $royaltiesBracketMultiplier = $royaltiesBracket ? (float) $royaltiesBracket->tbr_percentage : 0.0;
-        $royaltiesBracketPercentage = round($royaltiesBaseRate * $royaltiesBracketMultiplier, 4);
+        $royaltiesBracketPercentage = $royaltiesBracket ? (float) $royaltiesBracket->tbr_percentage : 0.0;
 
         $fnmPercentage = $chargeFnm ? $this->resolveFnmPercentage($contractMonth, $fnmPercentageConfig) : 0.0;
         $maintenancePercentage = $maintenancePercentageConfig;
@@ -362,6 +367,7 @@ private function buildPreview(FranchiseeContract $contract, int $referenceYear,
                 $revenueValue,
                 $royaltiesBracketPercentage,
                 $royaltiesBracketValue,
+                $royaltiesRevenueRate,
                 $fnmPercentage,
                 $fnmBracketValue,
             );
@@ -516,6 +522,7 @@ private function resolveEffectiveValues(
         float $revenueValue,
         float $royaltiesBracketPercentage,
         float $royaltiesBracketValue,
+        float $royaltiesRevenueRate,
         float $fnmBracketPercentage,
         float $fnmBracketValue,
     ): array {
@@ -523,7 +530,7 @@ private function resolveEffectiveValues(
             return [0.0, 0.0, 'tbr_fixo', 0.0, 0.0];
         }
 
-        $royaltiesFromRevenue = round(self::ROYALTIES_REVENUE_RATE * $revenueValue, 2);
+        $royaltiesFromRevenue = round($royaltiesRevenueRate * $revenueValue, 2);
         $fnmFromRevenue = round(self::FNM_REVENUE_RATE * $revenueValue, 2);
 
         if ($royaltiesBracketValue >= $royaltiesFromRevenue) {
@@ -532,7 +539,7 @@ private function resolveEffectiveValues(
             $royaltiesAppliedCriteria = 'tbr_fixo';
         } else {
             $royaltiesEffectiveValue = $royaltiesFromRevenue;
-            $royaltiesEffectivePercentage = self::ROYALTIES_REVENUE_RATE;
+            $royaltiesEffectivePercentage = $royaltiesRevenueRate;
             $royaltiesAppliedCriteria = 'percentual_faturamento';
         }
 

+ 2 - 3
tests/Feature/Financeiro/TbrCalculationRoutesTest.php

@@ -147,8 +147,7 @@ public function test_preview_batch_retorna_calculo_por_unidade_sem_gravar_nada()
 
         $this->assertCount(1, $payload);
         $this->assertSame($this->unit->id, $payload[0]['unit_id']);
-        // Sem faturamento (nenhuma parcela cadastrada), vence o fixo: 8% (base) x 75% (faixa) = 6% x TBR 1.000.
-        $this->assertEquals(60.0, $payload[0]['royalties_effective_value']);
+        $this->assertEquals(750.0, $payload[0]['royalties_effective_value']);
         $this->assertEquals(300.0, $payload[0]['maintenance_effective_value']);
 
         $this->assertDatabaseCount('tbr_calculations', 0);
@@ -213,7 +212,7 @@ public function test_preview_calcula_uma_unidade_sem_gravar_nada(): void
         ]);
 
         $response->assertOk();
-        $this->assertEquals(60.0, $response->json('payload.royalties_effective_value'));
+        $this->assertEquals(750.0, $response->json('payload.royalties_effective_value'));
         $this->assertDatabaseCount('tbr_calculations', 0);
     }
 

+ 4 - 4
tests/Unit/Financeiro/TbrBillingPreviewServiceTest.php

@@ -81,8 +81,8 @@ protected function tearDown(): void
     }
 
     /**
-     * Regra: A tela usa exatamente a mesma fórmula do cálculo oficial — fixo (8%
-     * base x 75% da faixa GP = 6% da TBR = R$ 60,00) vence porque não há faturamento
+     * Regra: A tela usa exatamente a mesma fórmula do cálculo oficial — fixo (75%
+     * da faixa GP direto sobre a TBR = R$ 750,00) vence porque não há faturamento
      * lançado (nenhuma parcela cadastrada).
      */
     public function test_usa_a_mesma_regra_do_calculo_oficial(): void
@@ -90,12 +90,12 @@ public function test_usa_a_mesma_regra_do_calculo_oficial(): void
         $result = $this->service->getAll()->firstWhere('id', $this->unit->id);
 
         $this->assertNotNull($result);
-        $this->assertEquals(60.0, $result['royalties_value']);
+        $this->assertEquals(750.0, $result['royalties_value']);
         $this->assertSame('Fixo TBR', $result['royalties_rule']);
         $this->assertEquals(200.0, $result['fnm_value']);
         $this->assertSame('Fixo TBR', $result['fnm_rule']);
         $this->assertEquals(300.0, $result['maintenance_value']);
-        $this->assertEquals(560.0, $result['total']);
+        $this->assertEquals(1250.0, $result['total']);
     }
 
     /**

+ 200 - 0
tests/Unit/Financeiro/TbrCalculationClientScenariosTest.php

@@ -0,0 +1,200 @@
+<?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;
+    }
+}

+ 69 - 49
tests/Unit/Financeiro/TbrCalculationRulesTest.php

@@ -126,12 +126,12 @@ private function makeBrackets(int $sizeId, array $brackets, bool $isRenewal = fa
         }
     }
 
-    private function makeTbrYear(int $year, float $value, float $fnm = 0.20, float $maintenance = 0.30): Tbr
+    private function makeTbrYear(int $year, float $value, float $fnm = 0.20, float $maintenance = 0.30, float $royalties = 0.08): Tbr
     {
         return Tbr::create([
             'year'                   => $year,
             'tbr_value'              => $value,
-            'royalties_percentage'   => 0.08,
+            'royalties_percentage'   => $royalties,
             'fnm_percentage'         => $fnm,
             'maintenance_percentage' => $maintenance,
         ]);
@@ -308,25 +308,24 @@ public function test_isencao_acaba_no_mes_4(): void
     // ---------------------------------------------- Royalties: regra do maior valor
 
     /**
-     * Regra (md "Royalties" meses 4-60): maior entre % fixo x TBR ou 8% do faturamento.
-     * O % fixo é a taxa-base configurada em "Configurações do TBR"
-     * (tbrs.royalties_percentage, 8% no cenário base) multiplicada pelo percentual
-     * da faixa de porte (Classificação de Habitantes) — a faixa é um multiplicador,
-     * não o percentual final.
+     * Regra (md "Royalties" meses 4-60): maior entre % fixo da faixa de porte x TBR
+     * ou 8% do faturamento. O percentual da faixa (Classificação de Habitantes) já é
+     * o percentual final sobre a TBR — confirmado pelo cenário do cliente (Excel):
+     * faixa 40% x TBR R$ 1.621,00 = R$ 648,40 direto, sem taxa-base intermediária.
      *
-     * Cenário: Mês 4, faixa GP = 75% (multiplicador), taxa-base = 8% -> fixo efetivo
-     * = 6% da TBR = R$ 60,00 (TBR R$ 1.000,00). Faturamento baixo (R$ 500,00, 8% = R$ 40,00).
+     * Cenário: Mês 4, TBR R$ 1.000,00 (75% da faixa = R$ 750,00), faturamento
+     * R$ 1.000,00 (8% = R$ 80,00).
      *
-     * Espera: Royalties = R$ 60,00, critério = 'tbr_fixo'.
+     * Espera: Royalties = R$ 750,00, critério = 'tbr_fixo'.
      */
     public function test_royalties_usa_valor_fixo_quando_faturamento_e_baixo(): void
     {
         $this->makeContract();
 
-        $result = $this->preview(2026, 4, 500.00);
+        $result = $this->preview(2026, 4, 1000.00);
 
-        $this->assertSame(60.0, $result['royalties_effective_value']);
-        $this->assertSame(0.06, $result['royalties_effective_percentage']);
+        $this->assertSame(750.0, $result['royalties_effective_value']);
+        $this->assertSame(0.75, $result['royalties_effective_percentage']);
         $this->assertSame('tbr_fixo', $result['royalties_applied_criteria']);
     }
 
@@ -351,20 +350,20 @@ public function test_royalties_usa_percentual_do_faturamento_quando_faturamento_
     /**
      * Regra: Critério de desempate — valor igual mantém o fixo da TBR (comparação é >=).
      *
-     * Cenário: Fixo do mês 4 = 8% (base) x 75% (faixa GP) = 6% x TBR R$ 1.000 = R$ 60,00.
-     * Faturamento R$ 750,00, em que 8% = R$ 60,00 = exatamente o valor fixo.
+     * Cenário: Faturamento R$ 9.375,00, em que 8% = R$ 750,00 = exatamente o valor fixo.
      *
-     * Espera: Royalties = R$ 60,00, critério = 'tbr_fixo', percentual gravado = 0,06.
+     * Espera: Royalties = R$ 750,00, critério = 'tbr_fixo', percentual gravado = 0,75 (o da faixa).
      */
     public function test_empate_entre_fixo_e_faturamento_mantem_o_fixo_da_tbr(): void
     {
         $this->makeContract();
 
-        $result = $this->preview(2026, 4, 750.00);
+        // 8% x 9.375 = 750,00 = valor fixo dos royalties
+        $result = $this->preview(2026, 4, 9375.00);
 
-        $this->assertSame(60.0, $result['royalties_effective_value']);
+        $this->assertSame(750.0, $result['royalties_effective_value']);
         $this->assertSame('tbr_fixo', $result['royalties_applied_criteria']);
-        $this->assertSame(0.06, $result['royalties_effective_percentage']);
+        $this->assertSame(0.75, $result['royalties_effective_percentage']);
     }
 
     // --------------------------------------------------------- FNM: regra do maior valor
@@ -466,23 +465,20 @@ public function test_faixa_de_royalties_muda_no_mes_13(): void
         $mes12 = $this->preview(2026, 12, 1000);
         $mes13 = $this->preview(2027, 1, 1000);
 
-        // 8% (base) x 75% (faixa 4-12) = 6%
-        $this->assertSame(0.06, $mes12['royalties_bracket_percentage']);
-        $this->assertSame(60.0, $mes12['royalties_bracket_value']);
+        $this->assertSame(0.75, $mes12['royalties_bracket_percentage']);
+        $this->assertSame(750.0, $mes12['royalties_bracket_value']);
 
-        // 8% (base) x 100% (faixa 13-60) = 8%
-        $this->assertSame(0.08, $mes13['royalties_bracket_percentage']);
-        $this->assertSame(80.0, $mes13['royalties_bracket_value']);
+        $this->assertSame(1.0, $mes13['royalties_bracket_percentage']);
+        $this->assertSame(1000.0, $mes13['royalties_bracket_value']);
     }
 
     /**
      * Regra (md "Classificação de Habitantes"): o porte do município do contrato
      * define qual faixa de royalties se aplica.
      *
-     * Cenário: Contrato em porte MGP (multiplicador 100% no mês 4) no lugar de GP (75%).
+     * Cenário: Contrato em porte MGP (100% no mês 4) no lugar de GP (75%).
      *
-     * Espera: percentual efetivo da faixa = 8% (base) x 100% = 0,08; Royalties = R$ 80,00
-     * em vez dos R$ 60,00 do porte GP.
+     * Espera: percentual da faixa = 1,00; Royalties = R$ 1.000,00 em vez de R$ 750,00.
      */
     public function test_porte_maior_do_municipio_cobra_percentual_maior(): void
     {
@@ -493,8 +489,8 @@ public function test_porte_maior_do_municipio_cobra_percentual_maior(): void
 
         $result = $this->preview(2026, 4, 1000);
 
-        $this->assertSame(0.08, $result['royalties_bracket_percentage']);
-        $this->assertSame(80.0, $result['royalties_effective_value']);
+        $this->assertSame(1.0, $result['royalties_bracket_percentage']);
+        $this->assertSame(1000.0, $result['royalties_effective_value']);
     }
 
     /**
@@ -524,11 +520,10 @@ public function test_erro_quando_nao_existe_faixa_nem_de_renovacao_para_o_mes():
      * royalties passa a vir de is_renewal=true — o contrato "renova" e continua sendo
      * cobrado, em vez de estourar erro.
      *
-     * Cenário: Faixa de renovação do porte GP cobrindo 1-60, com multiplicador 100%
-     * (igual à última faixa normal). Competência cai no mês 61 do contrato.
+     * Cenário: Faixa de renovação do porte GP cobrindo 1-60, com 100% (igual à última
+     * faixa normal). Competência cai no mês 61 do contrato.
      *
-     * Espera: encontra a faixa de renovação; percentual efetivo = 8% (base) x 100% = 0,08;
-     * Royalties = R$ 80,00 (empata com os 8% de faturamento de R$ 1.000,00, fica no fixo).
+     * Espera: encontra a faixa de renovação, percentual = 1,00, Royalties = R$ 1.000,00.
      */
     public function test_mes_61_usa_a_faixa_de_renovacao(): void
     {
@@ -538,8 +533,8 @@ public function test_mes_61_usa_a_faixa_de_renovacao(): void
 
         $result = $this->preview(2031, 1, 1000); // mês de contrato 61
 
-        $this->assertSame(0.08, $result['royalties_bracket_percentage']);
-        $this->assertSame(80.0, $result['royalties_effective_value']);
+        $this->assertSame(1.0, $result['royalties_bracket_percentage']);
+        $this->assertSame(1000.0, $result['royalties_effective_value']);
     }
 
     /**
@@ -549,9 +544,9 @@ public function test_mes_61_usa_a_faixa_de_renovacao(): void
      * Cenário: Faixa de renovação com duas fatias (1-12 = 60%, 13-60 = 100%), simulando
      * uma nova isenção/escalonamento dentro do próprio ciclo de renovação.
      *
-     * Espera (percentual efetivo = 8% base x multiplicador da faixa de renovação):
-     *   - mês 65 do contrato (5º mês da renovação, multiplicador 60%) -> 8% x 60% = 0,048
-     *   - mês 120 do contrato (60º mês da renovação, multiplicador 100%) -> 8% x 100% = 0,08
+     * Espera:
+     *   - mês 65 do contrato (5º mês da renovação) -> 60%
+     *   - mês 120 do contrato (60º mês da renovação) -> 100%
      */
     public function test_mes_de_renovacao_e_calculado_dentro_do_ciclo_de_60_meses(): void
     {
@@ -563,18 +558,17 @@ public function test_mes_de_renovacao_e_calculado_dentro_do_ciclo_de_60_meses():
         $mes65  = $this->preview(2031, 5, 1000);  // mês de contrato 65 -> renovação mês 5
         $mes120 = $this->preview(2035, 12, 1000); // mês de contrato 120 -> renovação mês 60
 
-        $this->assertSame(0.048, $mes65['royalties_bracket_percentage']);
-        $this->assertSame(0.08, $mes120['royalties_bracket_percentage']);
+        $this->assertSame(0.60, $mes65['royalties_bracket_percentage']);
+        $this->assertSame(1.00, $mes120['royalties_bracket_percentage']);
     }
 
     /**
      * Regra: Passado um segundo ciclo completo de 60 meses (mês 121 em diante), o
      * ciclo de renovação recomeça do zero.
      *
-     * Cenário: Faixa de renovação única (1-60, multiplicador 100%); competência cai no mês 121.
+     * Cenário: Faixa de renovação única (1-60 = 100%); competência cai no mês 121.
      *
-     * Espera: continua encontrando a faixa de renovação (não estoura erro); percentual
-     * efetivo = 8% (base) x 100% = 0,08.
+     * Espera: continua encontrando a faixa de renovação (não estoura erro), 100%.
      */
     public function test_segundo_ciclo_de_renovacao_reinicia_a_contagem(): void
     {
@@ -584,7 +578,7 @@ public function test_segundo_ciclo_de_renovacao_reinicia_a_contagem(): void
 
         $result = $this->preview(2036, 1, 1000);
 
-        $this->assertSame(0.08, $result['royalties_bracket_percentage']);
+        $this->assertSame(1.0, $result['royalties_bracket_percentage']);
     }
 
     // --------------------------------------------------------------- valor da TBR
@@ -595,18 +589,17 @@ public function test_segundo_ciclo_de_renovacao_reinicia_a_contagem(): void
      * sobre o valor gravado no contrato.
      *
      * Cenário: tbrs/2026 = R$ 1.000,00 (setUp) e contrato com tbr_fixed_value = R$ 2.000,00.
-     * Faturamento baixo (R$ 500,00) para o fixo (R$ 60,00) prevalecer sobre o percentual.
      *
-     * Espera: TBR usada = R$ 1.000,00; Royalties = R$ 60,00; Manutenção = R$ 300,00.
+     * Espera: TBR usada = R$ 1.000,00; Royalties = R$ 750,00; Manutenção = R$ 300,00.
      */
     public function test_tbr_do_ano_configurada_pelo_admin_tem_prioridade_sobre_o_contrato(): void
     {
         $this->makeContract(['tbr_fixed_value' => 2000.00]);
 
-        $result = $this->preview(2026, 4, 500);
+        $result = $this->preview(2026, 4, 1000);
 
         $this->assertSame(1000.0, $result['tbr_value']);
-        $this->assertSame(60.0, $result['royalties_effective_value']);
+        $this->assertSame(750.0, $result['royalties_effective_value']);
         $this->assertSame(300.0, $result['maintenance_effective_value']);
     }
 
@@ -677,6 +670,33 @@ public function test_percentuais_de_fnm_e_manutencao_vem_da_tabela_do_ano(): voi
         $this->assertSame(350.0, $result['maintenance_effective_value']);
     }
 
+    /**
+     * Regra: O campo "Royalties %" da tela de Configurações do TBR controla o
+     * percentual do lado FATURAMENTO da comparação "maior valor" — não o lado fixo
+     * (que continua vindo direto da faixa de porte, como confirmado pelo cliente).
+     * Antes, esse percentual era fixo em 8% no código; agora é configurável por ano.
+     *
+     * Cenário: tbrs/2027 com royalties_percentage = 10% (em vez dos 8% padrão).
+     * Faturamento R$ 8.000,00: 10% = R$ 800,00, contra o fixo da faixa GP mês 13-60
+     * (100% x TBR R$ 1.000,00 = R$ 1.000,00) — o fixo ainda vence aqui.
+     * Faturamento R$ 12.000,00: 10% = R$ 1.200,00 > R$ 1.000,00 fixo — agora vence o
+     * faturamento, usando 10% (e não os 8% hardcoded antigos, que dariam R$ 960,00).
+     */
+    public function test_royalties_percentual_de_faturamento_vem_da_configuracao_do_tbr_do_ano(): void
+    {
+        $this->makeContract();
+        $this->makeTbrYear(2027, 1000.00, royalties: 0.10); // mês 13 (100% da faixa GP)
+
+        $fixoVence = $this->preview(2027, 1, 8000.00);
+        $this->assertSame(1000.0, $fixoVence['royalties_effective_value']);
+        $this->assertSame('tbr_fixo', $fixoVence['royalties_applied_criteria']);
+
+        $faturamentoVence = $this->preview(2027, 1, 12000.00);
+        $this->assertSame(1200.0, $faturamentoVence['royalties_effective_value']);
+        $this->assertSame(0.10, $faturamentoVence['royalties_effective_percentage']);
+        $this->assertSame('percentual_faturamento', $faturamentoVence['royalties_applied_criteria']);
+    }
+
     // ------------------------------------------------ flags charge_roi / charge_fnm
 
     /**