Просмотр исходного кода

Refactor code structure for improved readability and maintainability

alvesantos 1 день назад
Родитель
Сommit
8f6d185c1f

+ 24 - 8
app/Services/TbrCalculationService.php

@@ -29,6 +29,12 @@ class TbrCalculationService
 
     private const EXEMPT_THRESHOLD_MONTH = 3;
 
+    // Faixas de royalties (is_renewal=false) cobrem os meses 1-60 do primeiro
+    // ciclo contratual. A partir do mês 61 o contrato entra em renovação: o mês
+    // de contrato é "enrolado" de volta para o intervalo 1-60 para buscar a
+    // faixa de renovação (is_renewal=true) equivalente.
+    private const RENEWAL_CYCLE_MONTHS = 60;
+
     // Faturamento (competência): por padrão usa o próprio mês de referência
     // (Opção A — assume-se o mês já fechado ao gerar). Ligue para usar o mês
     // anterior fechado (Opção B) quando a geração ocorrer dentro do mês corrente.
@@ -552,14 +558,12 @@ private function resolveContractMonth(?Carbon $startDate, int $year, int $month)
 
     private function findRoyaltiesBracket(int $municipalitySizeId, int $contractMonth): InhabitantClassification
     {
-        $bracket = InhabitantClassification::where('municipality_size_id', $municipalitySizeId)
-            ->where('is_renewal', false)
-            ->where('start', '<=', $contractMonth)
-            ->where(function ($q) use ($contractMonth) {
-                $q->whereNull('end')->orWhere('end', '>=', $contractMonth);
-            })
-            ->orderBy('start')
-            ->first();
+        $bracket = $this->findBracket($municipalitySizeId, false, $contractMonth);
+
+        if (! $bracket && $contractMonth > self::RENEWAL_CYCLE_MONTHS) {
+            $renewalMonth = (($contractMonth - 1) % self::RENEWAL_CYCLE_MONTHS) + 1;
+            $bracket = $this->findBracket($municipalitySizeId, true, $renewalMonth);
+        }
 
         if (! $bracket) {
             throw ValidationException::withMessages([
@@ -570,6 +574,18 @@ private function findRoyaltiesBracket(int $municipalitySizeId, int $contractMont
         return $bracket;
     }
 
+    private function findBracket(int $municipalitySizeId, bool $isRenewal, int $month): ?InhabitantClassification
+    {
+        return InhabitantClassification::where('municipality_size_id', $municipalitySizeId)
+            ->where('is_renewal', $isRenewal)
+            ->where('start', '<=', $month)
+            ->where(function ($q) use ($month) {
+                $q->whereNull('end')->orWhere('end', '>=', $month);
+            })
+            ->orderBy('start')
+            ->first();
+    }
+
     private function resolveFnmPercentage(int $contractMonth, float $fnmPercentage): float
     {
         return $contractMonth <= self::EXEMPT_THRESHOLD_MONTH ? 0.0 : $fnmPercentage;

+ 1 - 1
database/seeders/TbrSeeder.php

@@ -17,7 +17,7 @@ public function run(): void
 
         $defaults = [
             'royalties_percentage'   => 0.0800,
-            'fnm_percentage'         => 0.0200,
+            'fnm_percentage'         => 0.2000,
             'maintenance_percentage' => 0.3000,
         ];
 

+ 225 - 0
tests/Feature/Financeiro/TbrCalculationRoutesTest.php

@@ -0,0 +1,225 @@
+<?php
+
+namespace Tests\Feature\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\User;
+use App\Models\UserType;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Bus;
+use Laravel\Sanctum\Sanctum;
+use Tests\TestCase;
+
+/**
+ * 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.
+ */
+class TbrCalculationRoutesTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private Unit $unit;
+
+    private MunicipalitySize $size;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        Bus::fake();
+
+        $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',
+        ]);
+
+        $this->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' => $this->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' => $this->size->id,
+            'tbr_fixed_value' => 1000.00,
+        ]);
+    }
+
+    /** Usuário ADMIN autenticado via Sanctum: hasPermission() libera qualquer scope. */
+    private function actingAsAdmin(): User
+    {
+        $userType = UserType::create([
+            'slug' => 'ADMIN_TEST',
+            'label' => 'Admin Teste',
+            'is_system' => true,
+            'access_scope' => 'franchisor',
+            'system_key' => 'ADMIN',
+            'scope_key' => 'franchisor',
+        ]);
+
+        $user = User::create([
+            'name' => 'Admin Teste',
+            'email' => 'admin.teste@example.com',
+            'password' => bcrypt('password'),
+            'franchisor_user_type_id' => $userType->id,
+        ]);
+
+        Sanctum::actingAs($user, ['access']);
+
+        return $user;
+    }
+
+    public function test_rotas_de_calculo_exigem_autenticacao(): void
+    {
+        $this->getJson('/api/tbr-calculation')->assertStatus(401);
+        $this->postJson('/api/tbr-calculation/preview-batch', [])->assertStatus(401);
+        $this->postJson('/api/tbr-calculation/generate-batch', [])->assertStatus(401);
+    }
+
+    public function test_index_lista_calculos_ja_gravados(): void
+    {
+        $this->actingAsAdmin();
+
+        $this->postJson('/api/tbr-calculation/generate-batch', [
+            'reference_year' => 2026, 'reference_month' => 4,
+        ])->assertStatus(201);
+
+        $response = $this->getJson('/api/tbr-calculation');
+
+        $response->assertOk();
+        $this->assertCount(1, $response->json('payload'));
+    }
+
+    public function test_preview_batch_retorna_calculo_por_unidade_sem_gravar_nada(): void
+    {
+        $this->actingAsAdmin();
+
+        $response = $this->postJson('/api/tbr-calculation/preview-batch', [
+            'reference_year' => 2026, 'reference_month' => 4,
+        ]);
+
+        $response->assertOk();
+        $payload = $response->json('payload');
+
+        $this->assertCount(1, $payload);
+        $this->assertSame($this->unit->id, $payload[0]['unit_id']);
+        $this->assertEquals(750.0, $payload[0]['royalties_effective_value']);
+        $this->assertEquals(300.0, $payload[0]['maintenance_effective_value']);
+
+        $this->assertDatabaseCount('tbr_calculations', 0);
+    }
+
+    public function test_preview_batch_valida_ano_e_mes_obrigatorios(): void
+    {
+        $this->actingAsAdmin();
+
+        $this->postJson('/api/tbr-calculation/preview-batch', [])
+            ->assertStatus(422)
+            ->assertJsonValidationErrors(['reference_year', 'reference_month']);
+    }
+
+    public function test_generate_batch_gera_titulo_e_marca_calculo_como_gerado(): void
+    {
+        $this->actingAsAdmin();
+
+        $response = $this->postJson('/api/tbr-calculation/generate-batch', [
+            'reference_year' => 2026, 'reference_month' => 4,
+        ]);
+
+        $response->assertStatus(201);
+        $this->assertSame(1, $response->json('payload.generated_count'));
+
+        $this->assertDatabaseHas('tbr_calculations', [
+            'unit_id' => $this->unit->id,
+            'contract_month_reference' => 4,
+            'receivable_generated' => true,
+        ]);
+
+        $this->assertDatabaseHas('franchisee_account_receives', [
+            'unit_id' => $this->unit->id,
+        ]);
+
+        Bus::assertDispatched(\App\Jobs\SyncFranchiseeChargeJob::class);
+    }
+
+    public function test_generate_batch_pula_unidade_que_ja_tem_titulo_no_mes(): void
+    {
+        $this->actingAsAdmin();
+
+        $this->postJson('/api/tbr-calculation/generate-batch', [
+            'reference_year' => 2026, 'reference_month' => 4,
+        ])->assertStatus(201);
+
+        $response = $this->postJson('/api/tbr-calculation/generate-batch', [
+            'reference_year' => 2026, 'reference_month' => 4,
+        ]);
+
+        $response->assertStatus(201);
+        $this->assertSame(0, $response->json('payload.generated_count'));
+        $this->assertSame(1, $response->json('payload.skipped_count'));
+    }
+
+    public function test_generate_batch_pode_ser_filtrado_por_unit_ids(): void
+    {
+        $this->actingAsAdmin();
+
+        $outraUnidade = Unit::create([
+            'fantasy_name' => 'Outra Unidade', 'social_reason' => 'Outra Unidade LTDA',
+            'cnpj' => '00000000000272', 'street' => 'Rua Teste', 'neighborhood' => 'Centro',
+            'postal_code' => '87000000', 'city_id' => $this->unit->city_id, 'state_id' => $this->unit->state_id,
+            'email' => 'outra@teste.com', 'name_responsible' => 'Responsável Teste',
+        ]);
+
+        $response = $this->postJson('/api/tbr-calculation/generate-batch', [
+            'reference_year' => 2026, 'reference_month' => 4, 'unit_ids' => [$outraUnidade->id],
+        ]);
+
+        // Outra Unidade não tem contrato cadastrado: filtro por ela não gera nada.
+        $response->assertStatus(201);
+        $this->assertSame(0, $response->json('payload.generated_count'));
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 268 - 354
tests/Unit/Financeiro/TbrCalculationRulesTest.php


Некоторые файлы не были показаны из-за большого количества измененных файлов