Przeglądaj źródła

feat: unify billing preview logic with TbrCalculationService and enhance route coverage

alvesantos 2 dni temu
rodzic
commit
32663a7bf2

+ 43 - 34
app/Services/TbrBillingPreviewService.php

@@ -2,58 +2,67 @@
 
 namespace App\Services;
 
-use App\Models\FranchiseeContract;
+use Carbon\Carbon;
 use Illuminate\Support\Collection;
 
+/**
+ * Prévia de cobrança "da rede toda, mês corrente" usada pela tela de billing
+ * preview. Delega para TbrCalculationService::previewBatch() para que a regra
+ * (isenção 1-3, maior valor x faturamento, faixa de porte, flags de cobrança,
+ * renovação pós-mês-60) seja sempre a mesma do cálculo oficial — só o formato
+ * de saída é adaptado para o que a tela já espera.
+ */
 class TbrBillingPreviewService
 {
-    private const MAINTENANCE_RATE = 0.30;
-    private const EXEMPT_MONTHS    = 3;
+    public function __construct(
+        protected TbrCalculationService $calculationService,
+    ) {}
 
     public function getAll(): Collection
     {
-        $latestContracts = FranchiseeContract::with('unit')
-            ->whereNotNull('tbr_fixed_value')
-            ->whereNotNull('unit_id')
-            ->orderByDesc('created_at')
-            ->get()
-            ->unique('unit_id')
-            ->values();
+        $now = Carbon::now();
 
-        return $latestContracts->map(fn ($contract) => $this->buildPreview($contract));
+        return collect($this->calculationService->previewBatch($now->year, $now->month))
+            ->reject(fn (array $item) => isset($item['error']))
+            ->map(fn (array $item) => $this->toBillingPreview($item))
+            ->values();
     }
 
-    private function buildPreview(FranchiseeContract $contract): array
+    private function toBillingPreview(array $item): array
     {
-        $tbrValue      = (float) $contract->tbr_fixed_value;
-        $contractMonth = $this->resolveContractMonth($contract);
-        $isExempt      = $contractMonth <= self::EXEMPT_MONTHS;
-
-        $royaltiesValue    = $isExempt ? 0.0 : round($tbrValue * (float) $contract->tbr_fixed_value_percentage, 2);
-        $fnmValue          = $isExempt ? 0.0 : round($tbrValue * (float) $contract->marketing_fund_percentage, 2);
-        $maintenanceValue  = round($tbrValue * self::MAINTENANCE_RATE, 2);
-
         return [
-            'id'               => $contract->unit_id,
-            'unit_name'        => $contract->unit?->fantasy_name,
-            'tbr_value'        => $tbrValue,
-            'royalties_value'  => $royaltiesValue,
-            'royalties_rule'   => 'Fixo TBR',
-            'fnm_value'        => $fnmValue,
-            'fnm_rule'         => 'Fixo TBR',
-            'maintenance_value' => $maintenanceValue,
-            'total'            => round($royaltiesValue + $fnmValue + $maintenanceValue, 2),
+            'id'                => $item['unit_id'],
+            'unit_name'         => $item['unit_name'],
+            'tbr_value'         => $item['tbr_value'],
+            'royalties_value'   => $item['royalties_effective_value'],
+            'royalties_rule'    => $this->ruleLabel(
+                $item['contract_month_reference'],
+                $item['royalties_effective_value'],
+                $item['royalties_effective_percentage'],
+                $item['royalties_bracket_percentage'],
+            ),
+            'fnm_value'         => $item['fnm_effective_value'],
+            'fnm_rule'          => $this->ruleLabel(
+                $item['contract_month_reference'],
+                $item['fnm_effective_value'],
+                $item['fnm_effective_percentage'],
+                $item['fnm_bracket_percentage'],
+            ),
+            'maintenance_value' => $item['maintenance_effective_value'],
+            'total'             => $item['final_value'],
         ];
     }
 
-    private function resolveContractMonth(FranchiseeContract $contract): int
+    private function ruleLabel(int $contractMonth, float $effectiveValue, float $effectivePercentage, float $bracketPercentage): string
     {
-        $startDate = $contract->start_date ?? $contract->signature_date;
+        if ($contractMonth <= 3) {
+            return 'Isento';
+        }
 
-        if (!$startDate) {
-            return 1;
+        if ($effectiveValue <= 0.0) {
+            return 'Não cobrado';
         }
 
-        return (int) $startDate->startOfMonth()->diffInMonths(now()->startOfMonth()) + 1;
+        return abs($effectivePercentage - $bracketPercentage) < 0.0001 ? 'Fixo TBR' : '% Faturamento';
     }
 }

+ 5 - 1
app/Services/TbrCalculationService.php

@@ -330,6 +330,9 @@ 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;
 
         $contractMonth = $this->resolveContractMonth($contract->start_date, $referenceYear, $referenceMonth);
         $municipalitySizeId = (int) $contract->municipality_size_id;
@@ -343,7 +346,8 @@ private function buildPreview(FranchiseeContract $contract, int $referenceYear,
         $royaltiesBracket = $chargeRoi
             ? $this->findRoyaltiesBracket($municipalitySizeId, $contractMonth)
             : null;
-        $royaltiesBracketPercentage = $royaltiesBracket ? (float) $royaltiesBracket->tbr_percentage : 0.0;
+        $royaltiesBracketMultiplier = $royaltiesBracket ? (float) $royaltiesBracket->tbr_percentage : 0.0;
+        $royaltiesBracketPercentage = round($royaltiesBaseRate * $royaltiesBracketMultiplier, 4);
 
         $fnmPercentage = $chargeFnm ? $this->resolveFnmPercentage($contractMonth, $fnmPercentageConfig) : 0.0;
         $maintenancePercentage = $maintenancePercentageConfig;

+ 4 - 0
routes/authRoutes/franchisor_tbr.php

@@ -25,6 +25,10 @@
 
 Route::controller(TbrCalculationController::class)->prefix('tbr-calculation')->group(function () {
     Route::get('/', 'index')->middleware('permission:franchisor_tbr,view');
+    Route::post('/preview', 'preview')->middleware('permission:franchisor_tbr,view');
     Route::post('/preview-batch', 'previewBatch')->middleware('permission:franchisor_tbr,view');
+    Route::post('/', 'store')->middleware('permission:franchisor_tbr,add');
     Route::post('/generate-batch', 'generateBatch')->middleware('permission:franchisor_tbr,add');
+    Route::get('/{id}', 'show')->whereNumber('id')->middleware('permission:franchisor_tbr,view');
+    Route::post('/{id}/generate-receivable', 'generateReceivable')->whereNumber('id')->middleware('permission:franchisor_tbr,add');
 });

+ 81 - 5
tests/Feature/Financeiro/TbrCalculationRoutesTest.php

@@ -20,10 +20,10 @@
 /**
  * Camada HTTP das rotas de cálculo de contrato (routes/authRoutes/franchisor_tbr.php).
  *
- * Só GET /tbr-calculation, POST /tbr-calculation/preview-batch e
- * POST /tbr-calculation/generate-batch estão registradas hoje — os métodos
- * preview/store/show/generateReceivable existem no TbrCalculationController mas
- * não têm rota, então ficam fora daqui.
+ * O frontend (src/api/tbr_calculation.js) já tem previewTbrCalculation(),
+ * createTbrCalculation() e generateReceivable(id) prontos, mas as rotas
+ * preview/store/{id}/generate-receivable não existiam — foram registradas
+ * agora e são cobertas aqui junto com index/preview-batch/generate-batch.
  */
 class TbrCalculationRoutesTest extends TestCase
 {
@@ -147,7 +147,8 @@ 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']);
-        $this->assertEquals(750.0, $payload[0]['royalties_effective_value']);
+        // 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(300.0, $payload[0]['maintenance_effective_value']);
 
         $this->assertDatabaseCount('tbr_calculations', 0);
@@ -203,6 +204,81 @@ public function test_generate_batch_pula_unidade_que_ja_tem_titulo_no_mes(): voi
         $this->assertSame(1, $response->json('payload.skipped_count'));
     }
 
+    public function test_preview_calcula_uma_unidade_sem_gravar_nada(): void
+    {
+        $this->actingAsAdmin();
+
+        $response = $this->postJson('/api/tbr-calculation/preview', [
+            'unit_id' => $this->unit->id, 'reference_year' => 2026, 'reference_month' => 4,
+        ]);
+
+        $response->assertOk();
+        $this->assertEquals(60.0, $response->json('payload.royalties_effective_value'));
+        $this->assertDatabaseCount('tbr_calculations', 0);
+    }
+
+    public function test_store_grava_o_calculo_de_uma_unidade(): void
+    {
+        $this->actingAsAdmin();
+
+        $response = $this->postJson('/api/tbr-calculation', [
+            'unit_id' => $this->unit->id, 'reference_year' => 2026, 'reference_month' => 4,
+        ]);
+
+        $response->assertStatus(201);
+        $this->assertDatabaseHas('tbr_calculations', [
+            'unit_id' => $this->unit->id,
+            'contract_month_reference' => 4,
+            'receivable_generated' => false,
+        ]);
+    }
+
+    public function test_show_retorna_um_calculo_gravado(): void
+    {
+        $this->actingAsAdmin();
+
+        $id = $this->postJson('/api/tbr-calculation', [
+            'unit_id' => $this->unit->id, 'reference_year' => 2026, 'reference_month' => 4,
+        ])->json('payload.id');
+
+        $response = $this->getJson("/api/tbr-calculation/{$id}");
+
+        $response->assertOk();
+        $this->assertSame($id, $response->json('payload.id'));
+    }
+
+    public function test_generate_receivable_gera_titulo_para_um_calculo_especifico(): void
+    {
+        $this->actingAsAdmin();
+
+        $id = $this->postJson('/api/tbr-calculation', [
+            'unit_id' => $this->unit->id, 'reference_year' => 2026, 'reference_month' => 4,
+        ])->json('payload.id');
+
+        $response = $this->postJson("/api/tbr-calculation/{$id}/generate-receivable");
+
+        $response->assertStatus(201);
+        $this->assertDatabaseHas('tbr_calculations', ['id' => $id, 'receivable_generated' => true]);
+        $this->assertDatabaseHas('franchisee_account_receives', ['unit_id' => $this->unit->id]);
+
+        Bus::assertDispatched(\App\Jobs\SyncFranchiseeChargeJob::class);
+    }
+
+    public function test_generate_receivable_bloqueia_segunda_geracao_para_o_mesmo_calculo(): void
+    {
+        $this->actingAsAdmin();
+
+        $id = $this->postJson('/api/tbr-calculation', [
+            'unit_id' => $this->unit->id, 'reference_year' => 2026, 'reference_month' => 4,
+        ])->json('payload.id');
+
+        $this->postJson("/api/tbr-calculation/{$id}/generate-receivable")->assertStatus(201);
+
+        $this->postJson("/api/tbr-calculation/{$id}/generate-receivable")
+            ->assertStatus(422)
+            ->assertJsonValidationErrors(['tbr_calculation_id']);
+    }
+
     public function test_generate_batch_pode_ser_filtrado_por_unit_ids(): void
     {
         $this->actingAsAdmin();

+ 130 - 0
tests/Unit/Financeiro/TbrBillingPreviewServiceTest.php

@@ -0,0 +1,130 @@
+<?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\Models\UnitFinancial;
+use App\Services\TbrBillingPreviewService;
+use Carbon\Carbon;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Bus;
+use Tests\TestCase;
+
+/**
+ * A tela de billing preview (GET /tbr-billing-preview) tinha uma segunda
+ * implementação da regra de royalties/FNM/manutenção, divergente da oficial
+ * (não comparava com faturamento, ignorava as flags charge_roi/charge_fnm).
+ * Ela agora delega para TbrCalculationService::previewBatch() — este teste
+ * garante que a tela mostra os mesmos números e critérios do cálculo oficial.
+ */
+class TbrBillingPreviewServiceTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private TbrBillingPreviewService $service;
+
+    private Unit $unit;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        Bus::fake();
+        Carbon::setTestNow(Carbon::create(2026, 4, 15));
+
+        $this->service = app(TbrBillingPreviewService::class);
+
+        $country = Country::create(['name' => 'Brasil', 'code' => 'BR']);
+        $state   = State::create(['name' => 'Paraná', 'code' => 'PR', 'country_id' => $country->id]);
+        $city    = City::create(['name' => 'Maringá', 'country_id' => $country->id, 'state_id' => $state->id]);
+
+        $this->unit = Unit::create([
+            'fantasy_name' => 'Unidade Teste', 'social_reason' => 'Unidade Teste LTDA',
+            'cnpj' => '00000000000191', 'street' => 'Rua Teste', 'neighborhood' => 'Centro',
+            'postal_code' => '87000000', 'city_id' => $city->id, 'state_id' => $state->id,
+            'email' => 'unidade@teste.com', 'name_responsible' => 'Responsável Teste',
+        ]);
+
+        $size = MunicipalitySize::create(['acronym' => 'GP', 'description' => 'De 100 mil a 200 mil habitantes']);
+
+        foreach ([[1, 3, 0.0], [4, 12, 0.75], [13, 60, 1.00]] as [$start, $end, $percentage]) {
+            InhabitantClassification::create([
+                'municipality_size_id' => $size->id, 'description' => "Faixa {$start}-{$end}",
+                'start' => $start, 'end' => $end, 'tbr_percentage' => $percentage, 'is_renewal' => false,
+            ]);
+        }
+
+        Tbr::create([
+            'year' => 2026, 'tbr_value' => 1000.00,
+            'royalties_percentage' => 0.08, 'fnm_percentage' => 0.20, 'maintenance_percentage' => 0.30,
+        ]);
+
+        FranchiseeContract::create([
+            'unit_id' => $this->unit->id, 'protocol' => 1, 'name' => 'Contrato de Franquia',
+            'description' => 'Contrato de teste', 'start_date' => '2026-01-01', 'end_date' => '2031-01-01',
+            'signature_date' => '2026-01-01', 'validity_months' => 60, 'invoice_due_date' => 10,
+            'municipality_size_id' => $size->id, 'tbr_fixed_value' => 1000.00,
+        ]);
+    }
+
+    protected function tearDown(): void
+    {
+        Carbon::setTestNow();
+        parent::tearDown();
+    }
+
+    /**
+     * 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
+     * lançado (nenhuma parcela cadastrada).
+     */
+    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->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']);
+    }
+
+    /**
+     * Regra: Respeita a flag charge_roi da unidade — royalties zerado, rótulo
+     * "Não cobrado", assim como no cálculo oficial.
+     */
+    public function test_respeita_a_flag_charge_roi(): void
+    {
+        UnitFinancial::create(['unit_id' => $this->unit->id, 'charge_roi' => false, 'charge_fnm' => true]);
+
+        $result = $this->service->getAll()->firstWhere('id', $this->unit->id);
+
+        $this->assertEquals(0.0, $result['royalties_value']);
+        $this->assertSame('Não cobrado', $result['royalties_rule']);
+    }
+
+    /**
+     * Regra: Mês 1-3 aparece como "Isento" em vez de "Fixo TBR".
+     */
+    public function test_marca_isento_nos_meses_1_a_3(): void
+    {
+        Carbon::setTestNow(Carbon::create(2026, 2, 15)); // mês de contrato 2
+
+        $result = $this->service->getAll()->firstWhere('id', $this->unit->id);
+
+        $this->assertEquals(0.0, $result['royalties_value']);
+        $this->assertSame('Isento', $result['royalties_rule']);
+        $this->assertEquals(0.0, $result['fnm_value']);
+        $this->assertSame('Isento', $result['fnm_rule']);
+        $this->assertEquals(300.0, $result['maintenance_value']);
+    }
+}

+ 47 - 37
tests/Unit/Financeiro/TbrCalculationRulesTest.php

@@ -308,22 +308,25 @@ public function test_isencao_acaba_no_mes_4(): void
     // ---------------------------------------------- Royalties: regra do maior valor
 
     /**
-     * Regra (md "Royalties" meses 4-60): maior entre % fixo da faixa de porte x TBR
-     * ou 8% do faturamento.
+     * 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.
      *
-     * 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).
+     * 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).
      *
-     * Espera: Royalties = R$ 750,00, critério = 'tbr_fixo'.
+     * Espera: Royalties = R$ 60,00, critério = 'tbr_fixo'.
      */
     public function test_royalties_usa_valor_fixo_quando_faturamento_e_baixo(): void
     {
         $this->makeContract();
 
-        $result = $this->preview(2026, 4, 1000.00);
+        $result = $this->preview(2026, 4, 500.00);
 
-        $this->assertSame(750.0, $result['royalties_effective_value']);
-        $this->assertSame(0.75, $result['royalties_effective_percentage']);
+        $this->assertSame(60.0, $result['royalties_effective_value']);
+        $this->assertSame(0.06, $result['royalties_effective_percentage']);
         $this->assertSame('tbr_fixo', $result['royalties_applied_criteria']);
     }
 
@@ -348,19 +351,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: Faturamento R$ 9.375,00, em que 8% = R$ 750,00 = exatamente o valor fixo.
+     * 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.
      *
-     * Espera: Royalties = R$ 750,00, critério = 'tbr_fixo', percentual gravado = 0,75 (o da faixa).
+     * Espera: Royalties = R$ 60,00, critério = 'tbr_fixo', percentual gravado = 0,06.
      */
     public function test_empate_entre_fixo_e_faturamento_mantem_o_fixo_da_tbr(): void
     {
         $this->makeContract();
 
-        $result = $this->preview(2026, 4, 9375.00);
+        $result = $this->preview(2026, 4, 750.00);
 
-        $this->assertSame(750.0, $result['royalties_effective_value']);
+        $this->assertSame(60.0, $result['royalties_effective_value']);
         $this->assertSame('tbr_fixo', $result['royalties_applied_criteria']);
-        $this->assertSame(0.75, $result['royalties_effective_percentage']);
+        $this->assertSame(0.06, $result['royalties_effective_percentage']);
     }
 
     // --------------------------------------------------------- FNM: regra do maior valor
@@ -462,20 +466,23 @@ public function test_faixa_de_royalties_muda_no_mes_13(): void
         $mes12 = $this->preview(2026, 12, 1000);
         $mes13 = $this->preview(2027, 1, 1000);
 
-        $this->assertSame(0.75, $mes12['royalties_bracket_percentage']);
-        $this->assertSame(750.0, $mes12['royalties_bracket_value']);
+        // 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(1.0, $mes13['royalties_bracket_percentage']);
-        $this->assertSame(1000.0, $mes13['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']);
     }
 
     /**
      * 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 (100% no mês 4) no lugar de GP (75%).
+     * Cenário: Contrato em porte MGP (multiplicador 100% no mês 4) no lugar de GP (75%).
      *
-     * Espera: percentual da faixa = 1,00; Royalties = R$ 1.000,00 em vez de R$ 750,00.
+     * Espera: percentual efetivo da faixa = 8% (base) x 100% = 0,08; Royalties = R$ 80,00
+     * em vez dos R$ 60,00 do porte GP.
      */
     public function test_porte_maior_do_municipio_cobra_percentual_maior(): void
     {
@@ -486,8 +493,8 @@ public function test_porte_maior_do_municipio_cobra_percentual_maior(): void
 
         $result = $this->preview(2026, 4, 1000);
 
-        $this->assertSame(1.0, $result['royalties_bracket_percentage']);
-        $this->assertSame(1000.0, $result['royalties_effective_value']);
+        $this->assertSame(0.08, $result['royalties_bracket_percentage']);
+        $this->assertSame(80.0, $result['royalties_effective_value']);
     }
 
     /**
@@ -517,10 +524,11 @@ 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 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 multiplicador 100%
+     * (igual à última faixa normal). Competência cai no mês 61 do contrato.
      *
-     * Espera: encontra a faixa de renovação, percentual = 1,00, Royalties = R$ 1.000,00.
+     * 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).
      */
     public function test_mes_61_usa_a_faixa_de_renovacao(): void
     {
@@ -530,8 +538,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(1.0, $result['royalties_bracket_percentage']);
-        $this->assertSame(1000.0, $result['royalties_effective_value']);
+        $this->assertSame(0.08, $result['royalties_bracket_percentage']);
+        $this->assertSame(80.0, $result['royalties_effective_value']);
     }
 
     /**
@@ -541,9 +549,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:
-     *   - mês 65 do contrato (5º mês da renovação) -> 60%
-     *   - mês 120 do contrato (60º mês da renovação) -> 100%
+     * 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
      */
     public function test_mes_de_renovacao_e_calculado_dentro_do_ciclo_de_60_meses(): void
     {
@@ -555,17 +563,18 @@ 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.60, $mes65['royalties_bracket_percentage']);
-        $this->assertSame(1.00, $mes120['royalties_bracket_percentage']);
+        $this->assertSame(0.048, $mes65['royalties_bracket_percentage']);
+        $this->assertSame(0.08, $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 = 100%); competência cai no mês 121.
+     * Cenário: Faixa de renovação única (1-60, multiplicador 100%); competência cai no mês 121.
      *
-     * Espera: continua encontrando a faixa de renovação (não estoura erro), 100%.
+     * Espera: continua encontrando a faixa de renovação (não estoura erro); percentual
+     * efetivo = 8% (base) x 100% = 0,08.
      */
     public function test_segundo_ciclo_de_renovacao_reinicia_a_contagem(): void
     {
@@ -575,7 +584,7 @@ public function test_segundo_ciclo_de_renovacao_reinicia_a_contagem(): void
 
         $result = $this->preview(2036, 1, 1000);
 
-        $this->assertSame(1.0, $result['royalties_bracket_percentage']);
+        $this->assertSame(0.08, $result['royalties_bracket_percentage']);
     }
 
     // --------------------------------------------------------------- valor da TBR
@@ -586,17 +595,18 @@ 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$ 750,00; Manutenção = R$ 300,00.
+     * Espera: TBR usada = R$ 1.000,00; Royalties = R$ 60,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, 1000);
+        $result = $this->preview(2026, 4, 500);
 
         $this->assertSame(1000.0, $result['tbr_value']);
-        $this->assertSame(750.0, $result['royalties_effective_value']);
+        $this->assertSame(60.0, $result['royalties_effective_value']);
         $this->assertSame(300.0, $result['maintenance_effective_value']);
     }