merge: reconcile local fiscal corrections with delivered portal

This commit is contained in:
2026-10-29 19:21:47 +02:00
276 changed files with 21293 additions and 567 deletions
@@ -0,0 +1,126 @@
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Services\InvestmentCatalogue;
use App\Services\InvestmentGeography;
use App\Services\CorridorAgribusinessOpportunities;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class CatalogueProvinceCoverageTest extends TestCase
{
use DatabaseTransactions;
private const PROVINCE = 'bfde2651-2b46-5d9c-a500-733fa55a8c5c';
protected function setUp(): void
{
parent::setUp();\Tests\Support\ProactiveInvestmentFixture::guard();$this->withoutVite();
}
public function test_accepted_maputo_source_is_found_in_both_approved_provinces_without_becoming_a_district_site(): void
{
$c=app(InvestmentCatalogue::class);
$expected=Opportunity::published()->where('workflow_context->corridor_agribusiness->source_sha256',CorridorAgribusinessOpportunities::SOURCE_HASH)
->where('workflow_context->corridor_agribusiness->corridor','Corredor de Maputo')->pluck('id')->all();
$this->assertCount(5,$expected);
foreach([self::PROVINCE,'4aa31470-997e-5c63-8fe0-ff2f62586f56']as$province){
foreach(['all','context']as$basis){
$ids=$c->query(['sector'=>'agronegocio','province_id'=>$province,'territory_basis'=>$basis])->pluck('id')->all();
foreach($expected as$id)$this->assertContains($id,$ids);
}
}
foreach($expected as$id)$this->assertNotContains($id,$c->query(['province_id'=>self::PROVINCE,'territory_basis'=>'direct'])->pluck('id')->all());
$district=app(InvestmentGeography::class)->publicReferences()->where('parent_id',self::PROVINCE)->where('location_type','district')->first();
$this->assertNotNull($district);
foreach($expected as$id)$this->assertNotContains($id,$c->query(['province_id'=>self::PROVINCE,'district_id'=>$district->id])->pluck('id')->all());
$label=$c->presentLocation(Opportunity::findOrFail($expected[0]))->territorial_summary;
$this->assertStringContainsString('contexto provincial do corredor',$label);
$this->assertStringContainsString('distrito e posto administrativo não indicados',$label);
foreach($expected as$id)$this->assertContains($id,$c->query(['geo_location_id'=>self::PROVINCE])->pluck('id')->all());
$this->assertSame([],array_intersect($expected,$c->query(['province_id'=>'059b02ca-7095-577a-8705-4d4c46896747'])->pluck('id')->all()));
}
public function test_province_includes_its_descendants_with_full_location_and_excludes_private_and_other_provinces(): void
{
$geo=app(InvestmentGeography::class);$c=app(InvestmentCatalogue::class);
$district=$geo->publicReferences()->where('parent_id',self::PROVINCE)->where('location_type','district')->first();
$post=$geo->publicReferences()->where('parent_id',$district->id)->where('location_type','admin_post')->first();
$this->assertNotNull($post);
$make=fn($territory,$status='published')=>Opportunity::create(['sector_id'=>1,'reference'=>'SYNTHETIC-QA-'.Str::uuid(),'slug'=>'synthetic-'.Str::uuid(),'title'=>'SYNTHETIC QA province coverage','status'=>$status,'published_at'=>now()->subMinute(),'primary_territory_id'=>$territory,'geography_scope'=>'specific']);
$record=$make($post->id);$private=$make($post->id,'draft');$other=$make('059b02ca-7095-577a-8705-4d4c46896747');
$ids=$c->query(['province_id'=>self::PROVINCE,'sector'=>'agronegocio'])->pluck('id')->all();
$this->assertContains($record->id,$ids);$this->assertNotContains($private->id,$ids);$this->assertNotContains($other->id,$ids);
$label=$c->presentLocation($record)->territorial_summary;
foreach(['Maputo Província',$district->name,$post->name]as$name)$this->assertStringContainsString($name,$label);
}
public function test_actual_territory_and_publication_decisions_override_old_corridor_context(): void
{
$c=app(InvestmentCatalogue::class);
$record=Opportunity::published()->where('workflow_context->corridor_agribusiness->corridor','Corredor de Maputo')->firstOrFail();
$record->primary_territory_id='059b02ca-7095-577a-8705-4d4c46896747';$record->save();
$this->assertNotContains($record->id,$c->query(['province_id'=>self::PROVINCE])->pluck('id')->all());
$record->primary_territory_id=null;$record->status='draft';$record->save();
$this->assertNotContains($record->id,$c->query(['province_id'=>self::PROVINCE])->pluck('id')->all());
}
public function test_public_search_and_corridor_view_agree_without_inventing_map_coordinates(): void
{
$filters=['province_id'=>self::PROVINCE,'sector'=>'agronegocio'];
$response=$this->getJson('/api/v1/investment/search?'.http_build_query($filters))->assertOk()->assertJsonPath('total',5);
foreach($response->json('data')as$row)$this->assertStringContainsString('contexto provincial do corredor',$row['territorial_summary']);
$groups=app(\App\Services\OpportunityCorridorExplorer::class)->groups($filters);
$this->assertCount(1,$groups);$this->assertSame(5,$groups[0]['count']);
foreach($groups[0]['opportunities']as$row){$this->assertContains('Maputo Província',$row['provinces']);$this->assertNull($row['district']);}
$map=app(InvestmentCatalogue::class)->map($filters);
$this->assertCount(5,$map['unlocated']);$this->assertSame([],$map['features']);
}
public function test_all_approved_corridors_are_found_in_their_provinces_and_not_in_unrelated_provinces(): void
{
$catalogue=app(InvestmentCatalogue::class);
$provinces=app(InvestmentGeography::class)->publicReferences()->where('location_type','province')->get();
$this->assertCount(11,$provinces);
foreach(config('investment_corridors')as$entry){
$records=Opportunity::published()->whereNull('primary_territory_id')
->where('workflow_context->corridor_agribusiness->source_sha256',CorridorAgribusinessOpportunities::SOURCE_HASH)
->where('workflow_context->corridor_agribusiness->corridor',$entry['name'])->pluck('id')->all();
$this->assertNotEmpty($records,$entry['name']);
foreach($provinces as$province){
$ids=$catalogue->query(['province_id'=>$province->id,'sector'=>'agronegocio'])->pluck('id')->all();
foreach($records as$id){
if(in_array($province->source_code,$entry['provinces'],true))$this->assertContains($id,$ids);
else $this->assertNotContains($id,$ids);
}
}
}
}
public function test_descendant_coverage_is_national_and_shared_by_sectors_and_location_entry_points(): void
{
$geo=app(InvestmentGeography::class);$catalogue=app(InvestmentCatalogue::class);
$provinces=$geo->publicReferences()->where('location_type','province')->get();
$sectors=DB::table('sectors')->where('is_active',true)->whereNotNull('published_at')->whereNull('deleted_at')->orderBy('id')->limit(2)->get();
$this->assertCount(2,$sectors);
foreach($provinces as$province){
$district=$geo->publicReferences()->where('parent_id',$province->id)->where('location_type','district')->first();
$this->assertNotNull($district,$province->name);
$post=$geo->publicReferences()->where('parent_id',$district->id)->where('location_type','admin_post')->first();
$leaf=$post?:$district;
foreach($sectors as$sector){
$record=Opportunity::create(['sector_id'=>$sector->id,'reference'=>'SYNTHETIC-QA-'.Str::uuid(),'slug'=>'synthetic-'.Str::uuid(),
'title'=>'SYNTHETIC QA national coverage','status'=>'published','published_at'=>now()->subMinute(),'primary_territory_id'=>$leaf->id,'geography_scope'=>'specific']);
foreach([['province_id'=>$province->id],['geo_location_id'=>$province->id],['province_id'=>$province->id,'district_id'=>$district->id]]as$entry){
$this->assertContains($record->id,$catalogue->query([...$entry,'sector'=>$sector->slug])->pluck('id')->all());
}
$other=$sectors->firstWhere('id','!=',$sector->id);
$this->assertNotContains($record->id,$catalogue->query(['province_id'=>$province->id,'sector'=>$other->slug])->pluck('id')->all());
$label=$catalogue->presentLocation($record)->territorial_summary;
$this->assertStringContainsString($province->name,$label);$this->assertStringContainsString($district->name,$label);
if($post)$this->assertStringContainsString($post->name,$label);
}
}
}
}
@@ -0,0 +1,175 @@
<?php
namespace Tests\Feature;
use App\Models\EconomicCluster;
use App\Models\InvestmentDossier;
use App\Models\User;
use App\Services\ClusterCompetitiveness;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
class ClusterCompetitivenessTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
private function manager(): User
{
$user = User::factory()->create();
config(['portal.managers' => [$user->email], 'portal.profile_access_enabled' => false]);
$this->actingAs($user);
return $user;
}
private function cluster(): EconomicCluster
{
return EconomicCluster::create(['code' => 'COMP-'.bin2hex(random_bytes(6)), 'name' => 'Cluster de teste de persistência', 'sector' => 'Energia', 'status' => 'planned']);
}
private function draft(EconomicCluster $cluster): array
{
$draft = ['version' => 1, 'clusterId' => $cluster->id, 'sector' => 'Energia', 'region' => 'Distrito de demonstração', 'forces' => []];
foreach (ClusterCompetitiveness::FORCES as $force) {
$draft['forces'][$force] = ['pressure' => 'medium', 'evidence' => 'Observação de teste, não publicável como facto real',
'source' => 'Fonte de demonstração p. 2', 'period' => '2025', 'rationale' => 'Apreciação de teste',
'limitations' => 'Dados sintéticos', 'opportunity' => 'Hipótese de demonstração'];
}
return $draft;
}
private function url(EconomicCluster $cluster): string
{
return '/management/clusters/'.$cluster->id.'/competitiveness';
}
public function test_save_recovers_from_database_across_requests_and_records_encrypted_history(): void
{
$actor = $this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])
->assertOk()->assertJsonPath('version', 1)->assertJsonPath('draft', $draft)->assertJsonPath('approved', null);
$this->getJson($this->url($cluster))->assertOk()->assertHeader('Cache-Control', 'no-store, private')
->assertJsonPath('draft', $draft)->assertJsonPath('history.0.actor', $actor->id)->assertJsonPath('history.0.action', 'saved');
$this->get('/management/clusters/'.$cluster->id.'/edit')->assertInertia(fn (AssertableInertia $page) => $page
->component('structures/editor')->where('competitiveness.version', 1)->where('competitiveness.draft.region', $draft['region']));
$event = DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $cluster->id)->first();
$this->assertStringNotContainsString($draft['forces']['rivalry']['evidence'], $event->after_data);
$this->assertSame('restricted', $event->data_classification);
$this->assertNull(app(ClusterCompetitiveness::class)->publicData($cluster->id));
}
public function test_validation_conflict_and_server_error_do_not_overwrite_the_saved_version(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk();
$changed = $draft;
$changed['region'] = 'Outra região';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $changed])->assertConflict();
$changed['forces']['buyers']['pressure'] = 'zero';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $changed])->assertUnprocessable();
$changed = $draft;
$changed['clusterId'] = $this->cluster()->id;
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $changed])->assertUnprocessable();
$this->getJson($this->url($cluster))->assertJsonPath('version', 1)->assertJsonPath('draft', $draft)->assertJsonCount(1, 'history');
}
public function test_approval_pins_content_and_later_drafts_and_restoration_do_not_publish(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$url = $this->url($cluster);
$this->postJson($url.'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk();
$this->postJson($url.'/approved', ['expected_version' => 1, 'reason' => 'Aprovação sintética em QA'])->assertOk()->assertJsonPath('version', 2);
$changed = $draft;
$changed['region'] = 'Rascunho privado posterior';
$this->postJson($url.'/saved', ['expected_version' => 2, 'draft' => $changed])->assertOk();
$public = app(ClusterCompetitiveness::class)->publicData($cluster->id);
$this->assertSame($draft, $public['draft']);
$this->assertSame(['draft', 'version', 'at'], array_keys($public));
$this->postJson($url.'/restored', ['expected_version' => 3, 'revision' => 1])->assertOk()
->assertJsonPath('version', 4)->assertJsonPath('draft', $draft)->assertJsonPath('approved.version', 2);
$this->assertSame($public, app(ClusterCompetitiveness::class)->publicData($cluster->id));
$this->postJson($url.'/withdrawn', ['expected_version' => 4, 'reason' => 'Retirada sintética em QA'])->assertOk()->assertJsonCount(5, 'history');
$this->assertNull(app(ClusterCompetitiveness::class)->publicData($cluster->id));
$this->getJson($url)->assertJsonPath('draft', $draft);
}
public function test_incomplete_drafts_are_saved_but_not_approved_and_unknown_revisions_do_not_write(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$draft['forces']['buyers']['source'] = '';
$draft['forces']['entrants']['pressure'] = '';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk();
$this->postJson($this->url($cluster).'/approved', ['expected_version' => 1, 'reason' => 'Pedido de aprovação QA'])->assertUnprocessable();
$this->postJson($this->url($cluster).'/restored', ['expected_version' => 1, 'revision' => 99])->assertNotFound();
$this->getJson($this->url($cluster))->assertJsonPath('version', 1)->assertJsonPath('draft.forces.buyers.source', '');
}
public function test_saved_data_keeps_the_portable_file_contract_and_rejects_missing_forces(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$draft['version'] = '1';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])
->assertOk()->assertJsonPath('draft.version', 1);
unset($draft['forces']['suppliers']);
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $draft])->assertUnprocessable();
$this->getJson($this->url($cluster))->assertJsonPath('version', 1);
}
public function test_anonymous_and_unauthorised_users_cannot_read_history_or_mutate(): void
{
$cluster = $this->cluster();
$url = $this->url($cluster);
$this->getJson($url)->assertUnauthorized();
$this->postJson($url.'/saved', [])->assertUnauthorized();
$this->actingAs(User::factory()->create());
$this->getJson($url)->assertForbidden();
foreach (['saved', 'approved', 'withdrawn', 'restored'] as $action) {
$this->postJson($url.'/'.$action, [])->assertForbidden();
}
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $cluster->id)->count());
}
public function test_public_dossier_returns_only_the_approved_projection_and_unpublished_dossiers_remain_hidden(): void
{
$actor = $this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$dossier = InvestmentDossier::create(['cluster_id' => $cluster->id, 'title' => 'Ficha sintética QA', 'source' => 'Fonte sintética QA',
'content' => ['sector' => 'Energia'], 'publication_status' => 'published', 'approval_basis' => 'document_owner',
'approval_evidence' => 'Aprovação sintética só na base QA', 'published_at' => now()]);
$service = app(ClusterCompetitiveness::class);
$service->change($cluster, $actor->id, 0, 'saved', ['draft' => $draft]);
$this->app['auth']->forgetGuards();
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page->where('competitiveness', null));
$service->change($cluster, $actor->id, 1, 'approved', ['reason' => 'Aprovação privada de teste']);
$draft['region'] = 'CONTEÚDO PRIVADO NÃO APROVADO';
$service->change($cluster, $actor->id, 2, 'saved', ['draft' => $draft]);
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page
->where('competitiveness.version', 2)->where('competitiveness.draft.region', 'Distrito de demonstração')
->missing('competitiveness.actor')->missing('competitiveness.evidence')->missing('competitiveness.history'));
$dossier->update(['publication_status' => 'draft']);
$this->get('/carteira/'.$dossier->id)->assertNotFound();
$dossier->update(['publication_status' => 'published']);
$service->change($cluster, $actor->id, 3, 'withdrawn', ['reason' => 'Retirada privada de teste']);
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page->where('competitiveness', null));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
final class FiscalFormEntryTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
\Tests\Support\ProactiveInvestmentFixture::guard();
$this->withoutVite();
}
public function test_both_fiscal_forms_are_public_and_do_not_redirect_to_legal_documents(): void
{
config(['portal.homologation_only' => false]);
foreach (['iva', 'irpc'] as $tax) {
$this->get('/formularios/fiscais?imposto='.$tax)
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('portal/fiscal-forms')->where('initialTax', $tax));
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\InvestmentGeography;
use App\Services\InvestmentZones;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class InvestorUsabilityScopeTest extends TestCase
{
use DatabaseTransactions;
protected function setUp():void{parent::setUp();\Tests\Support\ProactiveInvestmentFixture::guard();$this->withoutVite();}
public function test_public_sector_is_consultable_by_guest_and_accounts_without_promoter_grants():void
{
config(['portal.homologation_only'=>false,'portal.legacy_content_enabled'=>false,'portal.legacy_content_qa_enabled'=>false]);
$this->get('/sectores/turismo')->assertOk();
$actor=User::factory()->create();
$this->actingAs($actor)->get('/sectores/turismo')->assertOk();
$this->assertSame(0,DB::table('portal_role_assignments')->where('user_id',$actor->id)->count());
config(['portal.profile_access_enabled'=>true]);
$entry=app(\App\Services\SectorInvestment::class)->publicView(\App\Models\Sector::where('slug','turismo')->firstOrFail());
$this->assertSame('/perfil/promotor',$entry['register_href']);
$this->assertSame('Pedir acesso para cadastrar oportunidades',$entry['register_label']);
}
public function test_zones_all_equals_visible_set_and_province_and_district_never_fall_back_to_all():void
{
$zones=app(InvestmentZones::class);$geo=app(InvestmentGeography::class);
$province='059b02ca-7095-577a-8705-4d4c46896747';
$district=$geo->publicReferences()->where('parent_id',$province)->where('location_type','district')->first();
$template=$zones->visible()->firstOrFail();
$make=function($territory,$published=true)use($template){$zone=$template->replicate();$zone->id=(string)Str::uuid();$zone->code='SYNTHETIC-'.Str::random(12);$zone->name='SYNTHETIC QA zone scope';$zone->primary_territory_id=$territory;$zone->publication_status=$published?'published':'draft';$zone->save();return$zone;};
$inside=$make($district->id);$outside=$make('9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8');$private=$make($district->id,false);
$all=$zones->search([])->orderBy('id')->pluck('id')->all();
$this->assertSame($zones->visible()->orderBy('id')->pluck('id')->all(),$all);
foreach([['province_id'=>$province],['province_id'=>$province,'district_id'=>$district->id]]as$selection){
$ids=$zones->search($selection)->pluck('id')->all();
$this->assertContains($inside->id,$ids);$this->assertNotContains($outside->id,$ids);$this->assertNotContains($private->id,$ids);
}
}
public function test_documented_legal_scope_covers_child_area_but_influence_does_not_become_an_installation():void
{
$zones=app(InvestmentZones::class);$geo=app(InvestmentGeography::class);$province='059b02ca-7095-577a-8705-4d4c46896747';
$district=$geo->publicReferences()->where('parent_id',$province)->where('location_type','district')->first();
$editor=User::factory()->create();$reviewer=User::factory()->create();$template=$zones->visible()->firstOrFail();$ids=[];
foreach(['legal_scope','influence']as$scope){
$zone=$template->replicate();$zone->id=(string)Str::uuid();$zone->code='SYNTHETIC-'.Str::random(12);$zone->name='SYNTHETIC QA legal coverage';$zone->primary_territory_id='9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8';$zone->save();$ids[$scope]=$zone->id;
DB::table('investment_zone_locations')->insert(['id'=>(string)Str::uuid(),'zone_id'=>$zone->id,'geo_location_id'=>$scope==='legal_scope'?$province:$district->id,'scope_type'=>$scope,'source_reference'=>'SYNTHETIC isolated QA scope fixture','validation_status'=>'validated','editor_id'=>$editor->id,'reviewer_id'=>$reviewer->id,'valid_from'=>now()->subDay(),'version_no'=>1,'created_at'=>now(),'updated_at'=>now()]);
}
$matches=$zones->search(['province_id'=>$province,'district_id'=>$district->id])->pluck('id')->all();
$this->assertContains($ids['legal_scope'],$matches);$this->assertNotContains($ids['influence'],$matches);
}
}
@@ -0,0 +1,56 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcAnnualSettlementDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_annual_simulated_balances_round_trip_with_proofs_states_and_preserved_books(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);
$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['payable','recoverable','balanced','absent','missing','duplicate','pec']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l8-settlement/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);
$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$before=json_decode($d['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$scope=$d['context_opportunity'];
$b=$s['irpcAnnual'][$scope];$m=$b['result']['model'];
foreach(['irpcNormal','irpcWithholdings','books','currencyEvents','ispcQuarters']as$map)$this->assertSame($before[$map],$s[$map]);
$this->assertSame('IRPC-annual-settlement/1',$m['version']);
$this->assertSame('123456789',$m['subjectNuit']);$this->assertSame('2026',$m['year']);
$this->assertSame(['numerator'=>'1408','denominator'=>'1'],$m['assessment']['colecta']);
$this->assertFalse($m['officialBalancesConsumed']);$this->assertFalse($m['submitted']);$this->assertFalse($m['refundReceived']);
$this->assertSame('known',$m['input']['credits']['state']);$this->assertSame('0',$m['input']['credits']['amount']);
if(in_array($case,['missing','duplicate','pec'],true)){
$this->assertSame('indeterminate',$m['status']);$this->assertNotEmpty($m['pending']);
$this->assertNull($m['arithmeticBalance']);$this->assertNull($m['payable']);$this->assertNull($m['recoverable']);
if($case==='missing')$this->assertSame('',$m['input']['ppc']['rows'][0]['effective']);
if($case==='pec')$this->assertSame('10',$m['input']['pec']['amount']);
if($case==='duplicate')$this->assertCount(2,$m['input']['ppc']['rows']);
}else{
$this->assertEmpty($m['pending']);
$this->assertSame(['payable'=>'payable','recoverable'=>'recoverable','balanced'=>'balanced','absent'=>'payable'][$case],$m['status']);
$this->assertSame(['numerator'=>['payable'=>'608','recoverable'=>'-192','balanced'=>'0','absent'=>'1408'][$case],'denominator'=>'1'],$m['arithmeticBalance']);
$this->assertSame(['numerator'=>$case==='recoverable'?'192':'0','denominator'=>'1'],$m['recoverable']);
if($case==='absent'){$this->assertSame('absent',$m['input']['withholdings']['state']);$this->assertSame('absent',$m['input']['ppc']['state']);}
else{$this->assertSame('yes',$m['input']['withholdings']['rows'][0]['incomeIncluded']);$this->assertSame('yes',$m['input']['ppc']['rows'][0]['effective']);}
}
file_put_contents(base_path('outputs/irpc-l8-settlement/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcCreditCarryDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_prior_credit_details_round_trip_with_frozen_history_and_preserved_records(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test', $target->name);
$this->assertSame(55441, (int) $target->port);
$snapshot = static fn (string $table): array => collect(DB::select('select id::text as id,md5(row_to_json(t)::text) as hash from public."'.$table.'" t order by id'))->pluck('hash', 'id')->all();
$protected = [];
foreach (['opportunities', 'documents', 'opportunity_documents', 'simulation_form_drafts', 'users', 'organizations', 'audit_events'] as $table) {
$protected[$table] = $snapshot($table);
}
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::findOrFail(1021));
$fixture = json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-carry-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
$this->assertTrue($fixture['synthetic']);
$this->assertFalse($fixture['actualBalances']);
$id = null;
$etag = null;
foreach ($fixture['cases'] as $name => $draft) {
$personal = ['person_type' => $draft['person_type']];
unset($draft['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $draft];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$this->assertSame($draft['business_plan_snapshot'], $back['business_plan_snapshot']);
$original = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$recovered = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
foreach (['irpcNormal', 'irpcWithholdings', 'books', 'currencyEvents', 'ispcQuarters'] as $map) {
$this->assertSame($original[$map], $recovered[$map]);
}
$book = $recovered['irpcAnnual'][$draft['context_opportunity']];
$model = $book['result']['model'];
$this->assertSame($original['irpcAnnual'][$draft['context_opportunity']], $book);
$this->assertCount(1, $book['history']);
$this->assertSame('IRPC-credit-carry/1', $model['creditCarry']['version']);
$this->assertFalse($model['creditCarry']['carryCreated']);
$this->assertFalse($model['creditCarry']['officialBalancesConsumed']);
$this->assertFalse($model['submitted']);
if (in_array($name, ['expired', 'missing-use', 'overallocated'], true)) {
$this->assertSame('indeterminate', $model['status']);
$this->assertNull($model['arithmeticBalance']);
$this->assertNull($model['internationalCredit']['deducted']);
$this->assertNull($model['pecDeduction']['deducted']);
} else {
$expected = ['payable' => ['550', '1'], 'fractional' => ['1822', '3'], 'recoverable' => ['-500', '1']][$name];
$this->assertEmpty($model['pending']);
$this->assertSame(['numerator' => $expected[0], 'denominator' => $expected[1]], $model['arithmeticBalance']);
}
if (getenv('INVESTMOZ_CARRY_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-carry-20261028/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
$preserved = [];
foreach ($protected as $table => $before) {
$this->assertSame($before, array_intersect_key($snapshot($table), $before), 'Existing records changed: '.$table);
$preserved[$table] = ['existing_rows' => count($before), 'unchanged' => true];
}
if (getenv('INVESTMOZ_CARRY_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-carry-20261028/existing-records-preservation.json'), json_encode(['database' => $target->name, 'tables' => $preserved, 'transaction_rollback' => true], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT));
}
}
}
@@ -0,0 +1,83 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcEconomicCreditDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_economic_credit_and_compatible_carry_round_trip_without_overwriting_accepted_history(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test', $target->name);
$this->assertSame(55441, (int) $target->port);
$snapshot = static fn (string $table): array => collect(DB::select('select id::text as id,md5(row_to_json(t)::text) as hash from public."'.$table.'" t order by id'))->pluck('hash', 'id')->all();
$protected = [];
foreach (['opportunities', 'documents', 'opportunity_documents', 'simulation_form_drafts', 'users', 'organizations', 'audit_events'] as $table) {
$protected[$table] = $snapshot($table);
}
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::findOrFail(1021));
$fixture = json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-economic-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
$this->assertTrue($fixture['synthetic']);
$this->assertFalse($fixture['actualTaxEvidence']);
$id = null;
$etag = null;
foreach ($fixture['cases'] as $name => $draft) {
$personal = ['person_type' => $draft['person_type']];
unset($draft['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $draft];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$this->assertSame($draft['business_plan_snapshot'], $back['business_plan_snapshot']);
$original = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$recovered = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
foreach (['irpcNormal', 'irpcWithholdings', 'books', 'currencyEvents', 'ispcQuarters'] as $map) {
$this->assertSame($original[$map], $recovered[$map]);
}
$book = $recovered['irpcAnnual'][$draft['context_opportunity']];
$model = $book['result']['model'];
$this->assertSame($original['irpcAnnual'][$draft['context_opportunity']], $book);
$this->assertCount(1, $book['history']);
$this->assertSame('IRPC-economic-credit/1', $model['economicCredit']['version']);
$this->assertFalse($model['economicCredit']['carryCreated']);
$this->assertFalse($model['economicCredit']['officialBalancesConsumed']);
$this->assertFalse($model['submitted']);
if (in_array($name, ['duplicate', 'missing-gross-up', 'article40', 'partial-parcel'], true)) {
$this->assertSame('indeterminate', $model['status']);
$this->assertNull($model['arithmeticBalance']);
$this->assertNull($model['economicCredit']['deducted']);
$this->assertNull($model['internationalCredit']['deducted']);
$this->assertNull($model['pecDeduction']['deducted']);
} else {
$expected = ['payable' => ['688', '1'], 'combined' => ['538', '1'], 'quota' => ['695', '1'], 'prior-use' => ['691', '1'], 'shared-paper' => ['688', '1'], 'fractional' => ['349997', '500'], 'recoverable' => ['-500', '1']][$name];
$this->assertEmpty($model['pending']);
$this->assertSame(['numerator' => $expected[0], 'denominator' => $expected[1]], $model['arithmeticBalance']);
}
if (getenv('INVESTMOZ_ECONOMIC_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-economic-20261028/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
$preserved = [];
foreach ($protected as $table => $before) {
$this->assertSame($before, array_intersect_key($snapshot($table), $before), 'Existing records changed: '.$table);
$preserved[$table] = ['existing_rows' => count($before), 'unchanged' => true];
}
if (getenv('INVESTMOZ_ECONOMIC_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-economic-20261028/existing-records-preservation.json'), json_encode(['database' => $target->name, 'tables' => $preserved, 'transaction_rollback' => true], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT));
}
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcGroupedDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_grouped_irpc_rates_and_existing_fx_recover_with_originals_and_accepted_history():void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$d=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-grouped-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');$this->assertSame($d['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcWithholdings'][$d['context_opportunity']];
$foreign=$b['result']['rows'][2];$this->assertSame('calculated',$foreign['status']);$this->assertSame(20,$foreign['rate']);$this->assertSame(['numerator'=>'64000','denominator'=>'1'],$foreign['base']);$this->assertSame(['numerator'=>'12800','denominator'=>'1'],$foreign['tax']);$this->assertSame(['numerator'=>'7800','denominator'=>'1'],$foreign['outstanding']);$this->assertSame('1000',$foreign['conversion']['originalAmount']);$this->assertSame('USD',$foreign['conversion']['originalCurrency']);$this->assertSame('2026-10-20',$foreign['conversion']['date']);$this->assertSame('2026-10-02',$foreign['eventDate']);
$this->assertSame(10,$b['result']['rows'][3]['rate']);$this->assertSame('on_account',$b['result']['rows'][4]['nature']);$this->assertSame(['numerator'=>'10000','denominator'=>'1'],$b['result']['rows'][4]['tax']);$this->assertSame(['numerator'=>'640','denominator'=>'1'],$b['result']['rows'][5]['settled']);$this->assertSame(['numerator'=>'640','denominator'=>'1'],$b['result']['rows'][5]['outstanding']);$this->assertSame('10',$b['result']['paymentConversions']['p4']['originalAmount']);$this->assertSame('USD',$b['result']['payments'][3]['currency']);
$old=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-l5-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$prior=json_decode($old['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);foreach($prior as$k=>$v){if(in_array($k,['irpcWithholdings','currencyEvents'],true))continue;$this->assertSame($v,$s[$k],$k);}
$oldBook=$prior['irpcWithholdings'][$d['context_opportunity']];$this->assertSame($oldBook['result'],$b['history'][array_key_last($b['history'])]);$this->assertSame($oldBook['rows'],array_slice($b['rows'],0,count($oldBook['rows'])));$this->assertSame($oldBook['payments'],array_slice($b['payments'],0,count($oldBook['payments'])));foreach(($prior['currencyEvents']??[])as$ctx=>$value){if($ctx!==$d['context_opportunity'])$this->assertSame($value,$s['currencyEvents'][$ctx]);}
if(is_dir(base_path('outputs/irpc-l5-grouped-20261025')))file_put_contents(base_path('outputs/irpc-l5-grouped-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,45 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcInterestCerDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_documented_parameter_calculation_and_segmented_cases_recover_through_private_api(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);
$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$draft=json_decode(file_get_contents(base_path('outputs/irpc-interest-cer/calculated.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$draft['person_type']];unset($draft['person_type']);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$draft],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);
$store=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
$model=$store['irpcWithholdings'][$draft['context_opportunity']]['mrftaResult']['model'];
$this->assertSame('7300',$model['principal']['numerator']);
$this->assertSame('3650',$model['paid']['numerator']);
$this->assertSame('15',$model['interest']['numerator']);
$this->assertSame('3665',$model['total']['numerator']);
$this->assertSame('IRPC-interest/1',$model['automatic']['version']);
$this->assertSame('',$model['input']['interest']['mode']);
$this->assertSame('automatic',$model['input']['interest']['calculation']);
$this->assertSame('10',$model['automatic']['rule']['annualRate']);
$this->assertStringContainsString('fictício',$model['automatic']['rule']['rateSource']);
$segments=$model['automatic']['rows'][0]['segments'];
$this->assertCount(2,$segments);
$this->assertSame(5,$segments[0]['days']);
$this->assertSame('7300',$segments[0]['principal']['numerator']);
$this->assertSame('3650',$segments[1]['principal']['numerator']);
$this->assertSame('999',$model['lines'][0]['cer']);
file_put_contents(base_path('outputs/irpc-interest-cer/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,62 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcInternationalCreditDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_international_credit_delta_round_trips_with_frozen_history_and_unchanged_existing_records(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test',$target->name);
$this->assertSame(55441,(int)$target->port);
$snapshot=static fn(string $table):array=>collect(DB::select('select id::text as id,md5(row_to_json(t)::text) as hash from public."'.$table.'" t order by id'))->pluck('hash','id')->all();
$protected=[];
foreach(['opportunities','documents','opportunity_documents','simulation_form_drafts','users','organizations','audit_events']as$table)$protected[$table]=$snapshot($table);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::findOrFail(1021)); // Existing synthetic QA actor, no new grants.
$base=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-assessment-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$fixture=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-international-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$this->assertTrue($fixture['synthetic']);$this->assertFalse($fixture['actualConventionData']);
$personal=['person_type'=>$base['person_type']];unset($base['person_type']);
$id=null;$etag=null;$previous=null;
foreach($fixture['cases']as$name=>$book){
$draft=$base;$s=json_decode($draft['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
if($previous!==null)$book['history']=[$previous];
$s['irpcAnnual'][$draft['context_opportunity']]=$book;
$draft['modelo_a_snapshot']=json_encode($s,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE);
$payload=['personal_snapshot'=>$personal,'economic_snapshot'=>$draft];
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',$payload,['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,$payload,['If-Match'=>$etag])->assertOk();
$response=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$response->headers->get('ETag');$back=$response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($draft['business_plan_snapshot'],$back['business_plan_snapshot']);
$recovered=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
foreach(['irpcNormal','irpcWithholdings','books','currencyEvents','ispcQuarters']as$map)$this->assertSame($s[$map],$recovered[$map]);
$saved=$recovered['irpcAnnual'][$draft['context_opportunity']];$m=$saved['result']['model'];$credit=$m['internationalCredit'];
$this->assertSame($book,$saved);$this->assertSame('IRPC-international-credit/1',$credit['version']);$this->assertFalse($credit['carryCreated']);$this->assertFalse($credit['officialBalancesConsumed']);$this->assertFalse($credit['countryAllocationCreated']);$this->assertFalse($m['submitted']);
$this->assertSame(['numerator'=>'1408','denominator'=>'1'],$m['assessment']['colecta']);
if(in_array($name,['missing','duplicate'],true)){
$this->assertSame('indeterminate',$m['status']);$this->assertNull($m['arithmeticBalance']);$this->assertNull($credit['deducted']);$this->assertNull($m['pecDeduction']['deducted']);$this->assertNotEmpty($m['pending']);
}else{
$expected=['payable'=>['payable','700','8','1400','200'],'treaty'=>['payable','702','6','1402','200'],'limited-collecta'=>['recoverable','-500','1408','0','0'],'absent'=>['payable','708','0','1408','200'],'prior-use'=>['payable','703','5','1403','200']][$name];
$this->assertSame($expected[0],$m['status']);$this->assertEmpty($m['pending']);
$this->assertSame(['numerator'=>$expected[1],'denominator'=>'1'],$m['arithmeticBalance']);
$this->assertSame(['numerator'=>$expected[2],'denominator'=>'1'],$credit['deducted']);
$this->assertSame(['numerator'=>$expected[3],'denominator'=>'1'],$credit['afterCredit']);
$this->assertSame(['numerator'=>$expected[4],'denominator'=>'1'],$m['pecDeduction']['deducted']);
if($name==='limited-collecta'){$this->assertSame(['numerator'=>'592','denominator'=>'1'],$credit['insufficientCollecta']);$this->assertSame(['numerator'=>'200','denominator'=>'1'],$m['pecDeduction']['remaining']);}
}
if($previous!==null)$this->assertSame($previous,$saved['history'][0]);$previous=$saved['result'];
if(getenv('INVESTMOZ_INTERNATIONAL_QA_OUTPUT')==='1')file_put_contents(base_path('outputs/irpc-l8-international-20261027/recovered-'.$name.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
$preserved=[];
foreach($protected as$table=>$before){$this->assertSame($before,array_intersect_key($snapshot($table),$before),'Existing records changed: '.$table);$preserved[$table]=['existing_rows'=>count($before),'unchanged'=>true];}
if(getenv('INVESTMOZ_INTERNATIONAL_QA_OUTPUT')==='1')file_put_contents(base_path('outputs/irpc-l8-international-20261027/existing-records-preservation.json'),json_encode(['database'=>$target->name,'tables'=>$preserved,'transaction_rollback'=>true],JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT));
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcMrftaDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_mrfta_and_documented_interest_recover_from_private_api_without_losing_history(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled'=>true, 'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$draft = json_decode(file_get_contents(base_path('outputs/irpc-mrfta/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type'=>$draft['person_type']];
unset($draft['person_type']);
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot'=>$personal, 'economic_snapshot'=>$draft], ['Idempotency-Key'=>(string) Str::uuid()])->assertCreated()->json('data.id');
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$book = $store['irpcWithholdings'][$draft['context_opportunity']];
$model = $book['mrftaResult']['model'];
$this->assertSame('2026-10', $model['period']);
$this->assertSame('20000', $model['principal']['numerator']);
$this->assertSame('3000', $model['paid']['numerator']);
$this->assertSame('500', $model['interest']['numerator']);
$this->assertSame('17500', $model['total']['numerator']);
$this->assertSame([], $model['pending']);
$this->assertSame('999', $model['lines'][0]['cer']);
$this->assertStringContainsString('fictício', $model['input']['codes'][0]['reference']);
$this->assertNotEmpty($book['history']);
file_put_contents(base_path('outputs/irpc-mrfta/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalAssessmentDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_normal_colecta_and_missing_classification_recover_without_annual_settlement(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['calculated','missing'] as $case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-colecta/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];
$this->assertSame('IRPC-normal-adjustments/4',$m['version']);$this->assertSame('6700',$m['taxableProfit']['numerator']);$this->assertSame('4400',$m['taxableBase']['numerator']);$this->assertNull($m['tax']);
$this->assertSame('2026',$m['assessment']['period']['year']);$this->assertSame('2026-01-01',$m['assessment']['period']['from']);$this->assertSame('2026-12-31',$m['assessment']['period']['to']);
$this->assertSame('L8',$m['assessment']['finalBalance']['scope']);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($m['assessment']['finalBalance']['recoverable']);
$this->assertSame('IRPC-normal-adjustments/3',$b['history'][2]['model']['version']);$this->assertArrayHasKey('irpcWithholdings',$s);
if($case==='calculated'){$this->assertSame('32',$m['assessment']['rate']['numerator']);$this->assertSame('1408',$m['colecta']['numerator']);$this->assertSame('general_61_1',$m['assessment']['input']['treatment']);}
else{$this->assertNull($m['colecta']);$this->assertNull($m['assessment']['rate']);$this->assertSame('',$m['assessment']['input']['treatment']);}
file_put_contents(base_path('outputs/irpc-l6-colecta/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalBaseDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_taxable_base_and_missing_deduction_information_recover_without_consuming_loss_balances(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['calculated','missing'] as $case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-base/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];
$this->assertSame('6700',$m['taxableProfit']['numerator']);$this->assertNull($m['tax']);$this->assertSame('IRPC-normal-adjustments/3',$m['version']);
$this->assertSame('2300',$m['base']['requestedLosses']['numerator']);$this->assertSame('2021',$m['base']['losses'][0]['year']);$this->assertSame(2026,$m['base']['losses'][0]['lastOrdinaryYear']);
$this->assertSame('800',$m['base']['losses'][0]['available']['numerator']);$this->assertSame('2500',$m['base']['losses'][1]['available']['numerator']);
$this->assertSame('200',$b['base']['losses'][0]['previouslyUsed']);$this->assertSame('500',$b['base']['losses'][1]['previouslyUsed']);
$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertSame('IRPC-normal-adjustments/2',$b['history'][1]['model']['version']);
if($case==='calculated'){
$this->assertSame('4400',$m['taxableBase']['numerator']);$this->assertSame('2300',$m['base']['deductedLosses']['numerator']);$this->assertSame('1000',$m['base']['losses'][1]['remaining']['numerator']);
$this->assertSame('known',$m['base']['input']['benefits']['state']);$this->assertSame('0',$m['base']['input']['benefits']['amount']);
}else{$this->assertNull($m['taxableBase']);$this->assertNull($m['base']['deductedLosses']);$this->assertSame('',$m['base']['input']['benefits']['state']);}
file_put_contents(base_path('outputs/irpc-l6-base/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalCostsDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_cost_limits_provisions_and_timing_recover_their_facts_results_and_original_histories(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$expected=['calculated'=>['6730','4430','7088'],'additional'=>['7050','4750','1520'],'limits'=>['7210','4910','7856'],'closing'=>['6530','4230','6768']];$id=null;$etag=null;
foreach(['calculated','missing','additional','limits','closing']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-costs/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];
$this->assertSame('IRPC-normal-adjustments/6',$m['version']);$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertArrayHasKey('books',$s);
$this->assertContains('IRPC-normal-adjustments/5',array_column(array_column($b['history'],'model'),'version'));$this->assertSame($b['costs'],$m['costs']['input']);
$this->assertNull($m['tax']);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($m['assessment']['finalBalance']['recoverable']);
if($case==='missing'){$this->assertNull($m['taxableProfit']);$this->assertNull($m['taxableBase']);$this->assertNull($m['colecta']);$this->assertSame('520',$m['schedules']['net']['numerator']);$this->assertSame('40',$m['costs']['rows'][1]['adjustment']['numerator']);}
else{[$profit,$base,$tax]=$expected[$case];$this->assertSame($profit,$m['taxableProfit']['numerator']);$this->assertSame($base,$m['taxableBase']['numerator']);$this->assertSame(['numerator'=>$tax,'denominator'=>$case==='additional'?'1':'5'],$m['colecta']);$this->assertSame([],$m['costs']['pending']);}
file_put_contents(base_path('outputs/irpc-l6-costs/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,26 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalDepreciationDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_depreciation_parameters_quotas_prior_balances_and_results_recover_without_rewriting_legacy_assets():void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
$expected=['months'=>['6780','4480','7168'],'declining'=>['6630','4330','6928'],'prior'=>['6730','4430','7088'],'specific'=>['13535','8935','7148']];
foreach(['months','declining','prior','specific','missing']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-depreciation/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');else$this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$this->assertSame('IRPC-normal-adjustments/7',$m['version']);$this->assertSame($b['depreciation'],$m['depreciation']['input']);$this->assertSame($b['schedules']['assets'],$m['schedules']['input']['assets']);$this->assertSame([],$m['schedules']['assets']);$this->assertContains('IRPC-normal-adjustments/6',array_column(array_column($b['history'],'model'),'version'));$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertNull($m['tax']);$this->assertNull($m['assessment']['finalBalance']['payable']);
if($case==='missing'){$this->assertNull($m['depreciation']['adjustment']);$this->assertNull($m['taxableProfit']);$this->assertNull($m['colecta']);$this->assertSame('40',$m['costs']['rows'][1]['adjustment']['numerator']);}
else{[$profit,$base,$colecta]=$expected[$case];$this->assertSame($profit,$m['taxableProfit']['numerator']);$this->assertSame($base,$m['taxableBase']['numerator']);$this->assertSame(['numerator'=>$colecta,'denominator'=>'5'],$m['colecta']);$this->assertSame([],$m['depreciation']['pending']);if($case==='prior'){$this->assertSame('20',$m['depreciation']['assets'][0]['priorUsed']['numerator']);$this->assertSame('40',$b['depreciation']['assets'][0]['priorAvailable']);}}
file_put_contents(base_path('outputs/irpc-l6-depreciation/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_partial_normal_irpc_adjustments_recover_without_overwriting_withholdings(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);
$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$draft=json_decode(file_get_contents(base_path('outputs/irpc-l6/calculated.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$draft['person_type']];unset($draft['person_type']);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$draft],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);
$this->assertSame($draft['business_plan_snapshot'],$recovered['business_plan_snapshot']);
$store=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
$book=$store['irpcNormal'][$draft['context_opportunity']];$model=$book['result']['model'];
$this->assertSame('6480',$model['accountingProfit']['numerator']);
$this->assertSame('120',$model['additions']['numerator']);
$this->assertSame('6600',$model['partialAdjustedResult']['numerator']);
$this->assertNull($model['taxableProfit']);
$this->assertNull($model['taxableBase']);
$this->assertNull($model['tax']);
$this->assertSame('projected',$model['origin']);
$this->assertSame('IRPC-normal-adjustments/1',$model['version']);
$this->assertSame('2026',$model['year']);
$this->assertCount(2,$model['rows']);
$this->assertSame('compensatory_interest',$model['rows'][1]['rule']);
$this->assertStringContainsString('L6 parcial',implode(' ',$model['pending']));
$this->assertArrayHasKey('irpcWithholdings',$store);
file_put_contents(base_path('outputs/irpc-l6/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,28 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalFormPreparationDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_form_classification_exact_values_and_unmapped_fields_recover_without_changing_calculation_or_history():void
{
$db=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$db->name);$this->assertSame(55441,(int)$db->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
foreach(['mapped','fractional','missing','unmapped','booktax']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-values-forms/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');else$this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$f=$m['formPreparation'];$fields=array_column($f['fields'],null,'code');$this->assertSame('IRPC-normal-adjustments/9',$m['version']);$this->assertSame($b['forms'],$f['input']);$this->assertContains('IRPC-normal-adjustments/8',array_column(array_column($b['history'],'model'),'version'));$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertSame('3130',$m['taxableProfit']['numerator']);$this->assertSame($m['taxableProfit'],$m['taxableBase']);$this->assertSame(['numerator'=>'5008','denominator'=>'5'],$m['colecta']);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($fields['309–316']['value']);$this->assertFalse($f['transport']['ready']);$this->assertSame($m['colecta'],$fields['300']['value']);
if($case==='missing'){$this->assertNull($f['netResult']);$this->assertNull($fields['200']['value']);$this->assertSame($m['taxableProfit'],$fields['260']['value']);}
elseif($case==='unmapped'||$case==='booktax'){$this->assertNull($fields['258']['value']);$this->assertNotEmpty($f['unmapped']);$this->assertFalse($f['reconciliation']['matches']);}
else{$this->assertSame('3120',$f['netResult']['numerator']);$this->assertTrue($f['reconciliation']['matches']);$this->assertSame('0',$fields['213']['value']['numerator']);}
if($case==='booktax'){$this->assertSame('2800',$f['netResult']['numerator']);$this->assertSame('0',$fields['213']['value']['numerator']);}
if($case==='fractional'){$parts=array_column($f['m20'],null,'account');$this->assertSame(['numerator'=>'601','denominator'=>'2'],$parts['6.3']['value']);$this->assertFalse($parts['6.3']['representation']['ready']);$this->assertNull($parts['6.3']['representation']['represented']);}
file_put_contents(base_path('outputs/irpc-l6-values-forms/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalPeriodFxDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_period_and_currency_source_evidence_recover_with_original_plan_and_histories():void
{
$db=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$db->name);$this->assertSame(55441,(int)$db->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
foreach(['short','fx','missing','loss']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-periods-fx/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');else$this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$this->assertSame('IRPC-normal-adjustments/8',$m['version']);$this->assertSame($b['periodsFx'],$m['periodsFx']['input']);$this->assertContains('IRPC-normal-adjustments/7',array_column(array_column($b['history'],'model'),'version'));$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($m['tax']);
if($case==='short'||$case==='fx'){$this->assertSame($case==='short'?'3130':'200320',$m['taxableProfit']['numerator']);$this->assertSame($m['taxableProfit'],$m['taxableBase']);$this->assertSame(['numerator'=>$case==='short'?'5008':'320512','denominator'=>'5'],$m['colecta']);$this->assertSame(6,$m['periodsFx']['months']);$this->assertSame([],$m['periodsFx']['pending']);}
else{$this->assertNull($m['colecta']);if($case==='loss'){$this->assertSame('3130',$m['taxableProfit']['numerator']);$this->assertNull($m['taxableBase']);}else{$this->assertNull($m['accountingProfit']);$this->assertNotEmpty($m['periodsFx']['pending']);}}
file_put_contents(base_path('outputs/irpc-l6-periods-fx/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalProfitDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_scoped_taxable_profit_and_missing_information_recover_with_all_prior_books(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);
$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['calculated','missing'] as $case){
$draft=json_decode(file_get_contents(base_path('outputs/irpc-l6-profit/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$draft['person_type']];unset($draft['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$draft],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$draft],['If-Match'=>$etag])->assertOk();
$response=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$response->headers->get('ETag');$recovered=$response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);
$this->assertSame($draft['business_plan_snapshot'],$recovered['business_plan_snapshot']);
$store=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
$book=$store['irpcNormal'][$draft['context_opportunity']];$m=$book['result']['model'];
$this->assertSame('6480',$m['accountingProfit']['numerator']);
$this->assertSame('220',$m['additions']['numerator']);
$this->assertNull($m['taxableBase']);$this->assertNull($m['tax']);
$this->assertSame('IRPC-normal-adjustments/2',$m['version']);
$this->assertSame('projected',$m['origin']);
$this->assertSame('absent',$m['profit']['input']['topics']['positivePatrimonial']['state']);
$this->assertCount(6,$m['profit']['lines']);
$this->assertSame('known',$m['profit']['input']['topics']['incomeCorrections']['state']);
$this->assertSame('0',$m['profit']['input']['topics']['incomeCorrections']['amount']);
$this->assertArrayHasKey('irpcWithholdings',$store);
$this->assertSame('IRPC-normal-adjustments/1',$book['history'][0]['model']['version']);
if($case==='calculated'){$this->assertSame('6700',$m['taxableProfit']['numerator']);$this->assertCount(0,$m['profit']['pending']);}
else{$this->assertNull($m['taxableProfit']);$this->assertSame('',$m['profit']['input']['topics']['negativePatrimonial']['state']);}
file_put_contents(base_path('outputs/irpc-l6-profit/recovered-'.$case.'.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcNormalSchedulesDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_complementary_rules_recover_calculated_missing_and_current_loss_without_settlement(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['calculated','missing','loss','ep'] as $case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-sets/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];
$this->assertSame('IRPC-normal-adjustments/5',$m['version']);$this->assertNull($m['tax']);
$this->assertSame('2026',$m['assessment']['period']['year']);$this->assertSame($case==='ep'?'2025-07-01':'2026-01-01',$m['assessment']['period']['from']);$this->assertSame($case==='ep'?'2026-06-30':'2026-12-31',$m['assessment']['period']['to']);
$this->assertSame('L8',$m['assessment']['finalBalance']['scope']);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($m['assessment']['finalBalance']['recoverable']);
$this->assertContains('IRPC-normal-adjustments/4',array_column(array_column($b['history'],'model'),'version'));$this->assertArrayHasKey('schedules',$m);$this->assertArrayHasKey('irpcWithholdings',$s);
if(in_array($case,['calculated','ep'],true)){$this->assertSame('6980',$m['taxableProfit']['numerator']);$this->assertSame('4680',$m['taxableBase']['numerator']);$this->assertSame(['numerator'=>'7488','denominator'=>'5'],$m['colecta']);$this->assertSame('120',$m['schedules']['interest']['numerator']);}
elseif($case==='missing'){$this->assertNull($m['taxableProfit']);$this->assertNull($m['colecta']);$this->assertNull($m['schedules']['interest']);$this->assertSame('520',$m['schedules']['net']['numerator']);}
else{$this->assertSame('3820',$m['currentLoss']['numerator']);$this->assertSame('0',$m['taxableBase']['numerator']);$this->assertSame('0',$m['colecta']['numerator']);$this->assertSame([],$b['base']['losses']);}
file_put_contents(base_path('outputs/irpc-l6-sets/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcPecDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_pec_delta_round_trips_without_consuming_balances_or_changing_existing_records(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test',$target->name);
$this->assertSame(55441,(int)$target->port);
$snapshot=static function(string $table): array {
return collect(DB::select('select id::text as id,md5(row_to_json(t)::text) as hash from public."'.$table.'" t order by id'))->pluck('hash','id')->all();
};
$protected=[];
foreach(['opportunities','documents','opportunity_documents','simulation_form_drafts','users','organizations','audit_events']as$table)$protected[$table]=$snapshot($table);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::findOrFail(1021)); // Existing synthetic QA operator; no new grants.
$base=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-assessment-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$fixture=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-pec-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$base['person_type']];unset($base['person_type']);
$id=null;$etag=null;$previous=null;
foreach($fixture['cases']as$name=>$book){
$draft=$base;$store=json_decode($draft['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
if($previous!==null)$book['history']=[$previous];
$store['irpcAnnual'][$draft['context_opportunity']]=$book;
$draft['modelo_a_snapshot']=json_encode($store,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE);
$payload=['personal_snapshot'=>$personal,'economic_snapshot'=>$draft];
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',$payload,['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,$payload,['If-Match'=>$etag])->assertOk();
$response=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$response->headers->get('ETag');$back=$response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$back['modelo_a_snapshot']);
$this->assertSame($draft['business_plan_snapshot'],$back['business_plan_snapshot']);
$recovered=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
foreach(['irpcNormal','irpcWithholdings','books','currencyEvents','ispcQuarters']as$map)$this->assertSame($store[$map],$recovered[$map]);
$saved=$recovered['irpcAnnual'][$draft['context_opportunity']];$m=$saved['result']['model'];
$this->assertSame($book,$saved);$this->assertSame('IRPC-PEC/1',$m['pecDeduction']['version']);
$this->assertFalse($m['officialBalancesConsumed']);$this->assertFalse($m['pecDeduction']['carryCreated']);
$this->assertSame(['numerator'=>'1408','denominator'=>'1'],$m['assessment']['colecta']);
if($name==='missing'){$this->assertSame('indeterminate',$m['status']);$this->assertNull($m['arithmeticBalance']);$this->assertNull($m['pecDeduction']['deducted']);}
else{
$expected=['payable'=>['payable','708','200','0'],'capped'=>['balanced','0','1408','592'],'recoverable'=>['recoverable','-500','1408','592'],'prior-use'=>['payable','1208','200','0']][$name];
$this->assertSame($expected[0],$m['status']);
foreach(['arithmeticBalance'=>1,'deducted'=>2,'remaining'=>3]as$field=>$index)$this->assertSame(['numerator'=>$expected[$index],'denominator'=>'1'],$field==='arithmeticBalance'?$m[$field]:$m['pecDeduction'][$field]);
}
if($previous!==null)$this->assertSame($previous,$saved['history'][0]);
$previous=$saved['result'];
if(getenv('INVESTMOZ_PEC_QA_OUTPUT')==='1')file_put_contents(base_path('outputs/irpc-l8-pec-20261027/recovered-'.$name.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
$preserved=[];
foreach($protected as$table=>$before){
$after=$snapshot($table);
$this->assertSame($before,array_intersect_key($after,$before),'Existing records changed: '.$table);
$preserved[$table]=['existing_rows'=>count($before),'unchanged'=>true];
}
if(getenv('INVESTMOZ_PEC_QA_OUTPUT')==='1')file_put_contents(base_path('outputs/irpc-l8-pec-20261027/existing-records-preservation.json'),json_encode(['database'=>$target->name,'tables'=>$preserved,'transaction_rollback'=>true],JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT));
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcReliefsDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_demonstrated_reliefs_and_insufficient_information_recover_through_private_api(): void
{
$target = DB::selectOne('select current_database() as name, inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled'=>true, 'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$draft = json_decode(file_get_contents(base_path('outputs/irpc-reliefs/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type'=>$draft['person_type']];
unset($draft['person_type']);
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot'=>$personal, 'economic_snapshot'=>$draft], ['Idempotency-Key'=>(string) Str::uuid()])->assertCreated()->json('data.id');
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$book = $store['irpcWithholdings'][$draft['context_opportunity']];
[$dispensed, $rent, $treaty, $missing] = array_slice($book['result']['rows'], -4);
$this->assertSame('dispensed', $dispensed['treatment']);
$this->assertSame(['numerator'=>'0','denominator'=>'1'], $dispensed['tax']);
$this->assertSame('dispensed', $rent['treatment']);
$this->assertSame('cirpc68f', $rent['reliefRule']);
$this->assertSame(['numerator'=>'0','denominator'=>'1'], $rent['tax']);
$this->assertSame('treaty', $treaty['treatment']);
$this->assertSame(10, $treaty['rate']);
$this->assertSame(['numerator'=>'10000','denominator'=>'1'], $treaty['tax']);
$this->assertSame('indeterminate', $missing['status']);
$this->assertNull($missing['tax']);
$this->assertSame('QA certificado fiscal Portugal 2026', $book['result']['inputs'][count($book['result']['inputs'])-2]['reliefPreparation']['residenceDocument']);
$this->assertNotEmpty($book['history']);
file_put_contents(base_path('outputs/irpc-reliefs/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcSpecialCessationPriorDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_cbf16_cessation_and_prior_quotas_recover_without_consuming_declared_balances(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
foreach(['cessation','prior','annual','exit','missing','negative','absent']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-cessation-prior/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$a=$m['special']['acceleration'];
$this->assertSame('IRPC-normal-adjustments/13',$m['version']);$this->assertSame($b['special']['acceleration']['monthly'],$a['monthly']['input']);
$this->assertContains('IRPC-normal-adjustments/12',array_column(array_column($b['history'],'model'),'version'));
$this->assertContains('IRPC-normal-adjustments/11',array_column(array_column($b['history'],'model'),'version'));
$this->assertSame($b['depreciation'],$a['normal']['input']);$this->assertSame(['scope'=>'L8','payable'=>null,'recoverable'=>null],$m['special']['finalBalance']);
$this->assertNull($m['tax']);$this->assertArrayHasKey('irpcWithholdings',$s);
if($case==='cessation'){
$v=$a['monthly']['assets'][0];$this->assertSame(6,$v['months']);$this->assertSame('absent',$v['exit']['state']);$this->assertSame('10',$a['additionalDeduction']['numerator']);
$this->assertSame('3120',$m['special']['taxableProfit']['numerator']);$this->assertSame(['numerator'=>'4992','denominator'=>'5'],$m['special']['colecta']);
}elseif($case==='prior'||$case==='annual'){
$v=$a['monthly']['assets'][0];$this->assertSame('150',$b['depreciation']['assets'][0]['previousAccounting']);$this->assertSame('100',$b['depreciation']['assets'][0]['previousFiscal']);$this->assertSame('50',$b['depreciation']['assets'][0]['priorAvailable']);
$this->assertSame($case==='prior'?'15':'30',$v['prior']['acceleratedUsed']['numerator']);$this->assertSame($case==='prior'?'35':'20',$v['prior']['acceleratedRemaining']['numerator']);
$this->assertSame($case==='prior'?'25':'50',$a['additionalDeduction']['numerator']);$this->assertSame($case==='prior'?'3105':'6210',$m['special']['taxableProfit']['numerator']);
$this->assertSame(['numerator'=>$case==='prior'?'4968':'9936','denominator'=>'5'],$m['special']['colecta']);$this->assertSame($b['depreciation'],$a['accelerated']['input']);
}elseif($case==='exit'){
$v=$a['monthly']['assets'][0];$this->assertSame(5,$v['months']);$this->assertSame('2026-06-20',$v['exit']['date']);
$this->assertSame(['numerator'=>'125','denominator'=>'3'],$v['normalQuota']);$this->assertSame(['numerator'=>'125','denominator'=>'6'],$a['additionalDeduction']);
}elseif($case==='missing'){
$this->assertSame('indeterminate',$a['status']);$this->assertNull($a['additionalDeduction']);$this->assertSame('3130',$m['taxableProfit']['numerator']);$this->assertNull($m['special']['colecta']);
}elseif($case==='negative'){
$this->assertSame('not_applicable',$a['rows'][0]['status']);$this->assertFalse($a['monthly']['assets'][0]['applied']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
}else{
$this->assertSame('absent',$a['status']);$this->assertSame([],$a['monthly']['assets']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
}
file_put_contents(base_path('outputs/irpc-l7-cessation-prior/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,44 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcSpecialCombinationsDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_qualified_combinations_and_incompatibilities_round_trip_without_rewriting_inputs(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
foreach(['combined','autonomous','incompatible','missing','exception','negative','absent']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-combinations/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$v=$m['special'];$c=$v['combinations'];
$this->assertSame('IRPC-normal-adjustments/14',$m['version']);$this->assertSame($b['special']['combinations'],$c['input']);
$this->assertContains('IRPC-normal-adjustments/13',array_column(array_column($b['history'],'model'),'version'));
$this->assertSame('150',$b['depreciation']['assets'][0]['previousAccounting']);$this->assertSame('100',$b['depreciation']['assets'][0]['previousFiscal']);$this->assertSame('50',$b['depreciation']['assets'][0]['priorAvailable']);
$this->assertSame($b['depreciation'],$v['acceleration']['normal']['input']);$this->assertSame(['scope'=>'L8','payable'=>null,'recoverable'=>null],$v['finalBalance']);$this->assertNull($m['tax']);$this->assertArrayHasKey('irpcWithholdings',$s);
if(in_array($case,['combined','autonomous'],true)){
$this->assertSame('compatible',$c['status']);$this->assertSame('25',$v['acceleration']['additionalDeduction']['numerator']);$this->assertSame('15',$c['assetClaims'][0]['priorRecoveredIncluded']['numerator']);$this->assertSame('75',$c['assetClaims'][0]['acceleratedDeduction']['numerator']);
$this->assertSame('16',$v['cooperative']['rate']['numerator']);$this->assertSame($case==='combined'?'3105':'3205',$v['taxableBase']['numerator']);
$this->assertSame(['numerator'=>$case==='combined'?'2484':'2739','denominator'=>'5'],$v['combinedColecta']);
if($case==='autonomous'){$this->assertSame('35',$v['expenses']['autonomousTax']['numerator']);$this->assertSame('0',$v['expenses']['rows'][0]['additionalProfitAdjustment']['numerator']);}
}elseif(in_array($case,['incompatible','missing','exception'],true)){
$this->assertSame($case==='incompatible'?'incompatible':'indeterminate',$c['status']);$this->assertNull($v['taxableBase']);$this->assertNull($v['combinedColecta']);$this->assertSame('3130',$m['taxableProfit']['numerator']);
}elseif($case==='negative'){
$this->assertSame('not_applicable',$v['cooperative']['status']);$this->assertSame(['numerator'=>'4968','denominator'=>'5'],$v['colecta']);
}else{
$this->assertSame('absent',$v['acceleration']['status']);$this->assertSame('0',$v['acceleration']['additionalDeduction']['numerator']);$this->assertSame(['numerator'=>'2504','denominator'=>'5'],$v['colecta']);
}
file_put_contents(base_path('outputs/irpc-l7-combinations/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcSpecialDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_special_treatments_recover_with_original_plan_history_and_indeterminate_cases(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);
$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['expense','missing','negative','absent','cooperative','acceleration'] as $case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-common/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$l7=$m['special'];
$this->assertSame('IRPC-normal-adjustments/10',$m['version']);
$this->assertSame($b['special'],$l7['input']);
$this->assertContains('IRPC-normal-adjustments/9',array_column(array_column($b['history'],'model'),'version'));
$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertArrayHasKey('books',$s);
$this->assertNull($m['tax']);$this->assertNull($l7['finalBalance']['payable']);$this->assertNull($l7['finalBalance']['recoverable']);
$this->assertSame('123456789',$l7['subjectNuit']);
$this->assertStringContainsString('61(4)',$l7['expenses']['source']);
if($case==='expense'){
$this->assertSame('35',$l7['expenses']['autonomousTax']['numerator']);
$this->assertSame('100',$l7['expenses']['rows'][0]['l6Adjustment']['numerator']);
$this->assertSame('0',$l7['expenses']['rows'][0]['additionalProfitAdjustment']['numerator']);
$this->assertSame($m['taxableProfit'],$l7['taxableProfit']);
$this->assertSame(['numerator'=>'5343','denominator'=>'5'],$l7['combinedColecta']);
}elseif($case==='missing'){
$this->assertSame('indeterminate',$l7['expenses']['status']);$this->assertNull($l7['expenses']['autonomousTax']);$this->assertNull($l7['combinedColecta']);
$this->assertSame('3230',$m['taxableProfit']['numerator']);
}elseif($case==='negative'){
$this->assertSame('not_applicable',$l7['expenses']['rows'][0]['status']);$this->assertSame('0',$l7['expenses']['autonomousTax']['numerator']);
}elseif($case==='absent'){
$this->assertSame('absent',$l7['expenses']['status']);$this->assertSame('0',$l7['expenses']['autonomousTax']['numerator']);
}elseif($case==='cooperative'){
$this->assertSame('16',$l7['cooperative']['rate']['numerator']);$this->assertSame(['numerator'=>'2504','denominator'=>'5'],$l7['colecta']);
$this->assertSame(['numerator'=>'5008','denominator'=>'5'],$m['colecta']);
}else{
$this->assertSame('20',$l7['acceleration']['additionalDeduction']['numerator']);
$this->assertSame('150',$l7['acceleration']['accelerated']['assets'][0]['quota']['numerator']);
$this->assertSame('120',$l7['acceleration']['accelerated']['assets'][0]['deduction']['numerator']);
$this->assertSame('6240',$l7['taxableProfit']['numerator']);$this->assertSame('6260',$m['taxableProfit']['numerator']);
$this->assertSame($b['depreciation'],$l7['acceleration']['accelerated']['input']);
}
file_put_contents(base_path('outputs/irpc-l7-common/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcSpecialMonthlyDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_monthly_cbf16_recovers_new_results_missing_cases_and_accepted_history(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
foreach(['monthly','fraction','missing','negative','absent']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-months/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$a=$m['special']['acceleration'];
$this->assertSame('IRPC-normal-adjustments/11',$m['version']);$this->assertSame($b['special']['acceleration']['monthly'],$a['monthly']['input']);
$this->assertContains('IRPC-normal-adjustments/10',array_column(array_column($b['history'],'model'),'version'));
$this->assertSame($b['depreciation'],$a['normal']['input']);$this->assertSame('L8',$m['special']['finalBalance']['scope']);$this->assertNull($m['special']['finalBalance']['payable']);
$this->assertNull($m['special']['finalBalance']['recoverable']);$this->assertNull($m['tax']);$this->assertArrayHasKey('irpcWithholdings',$s);
if($case==='monthly'){
$v=$a['monthly']['assets'][0];$this->assertSame(6,$v['months']);$this->assertSame('50',$v['normalQuota']['numerator']);$this->assertSame('75',$v['acceleratedQuota']['numerator']);
$this->assertSame('60',$v['acceleratedDeduction']['numerator']);$this->assertSame('10',$a['additionalDeduction']['numerator']);
$this->assertSame('6310',$m['taxableProfit']['numerator']);$this->assertSame('6300',$m['special']['taxableProfit']['numerator']);$this->assertSame('2016',$m['special']['colecta']['numerator']);
$this->assertSame($b['depreciation'],$a['accelerated']['input']);
}elseif($case==='fraction'){
$v=$a['monthly']['assets'][0];$this->assertSame(1,$v['months']);$this->assertSame(['numerator'=>'25','denominator'=>'3'],$v['normalQuota']);$this->assertSame(['numerator'=>'25','denominator'=>'6'],$a['additionalDeduction']);
}elseif($case==='missing'){
$this->assertSame('indeterminate',$a['status']);$this->assertNull($a['additionalDeduction']);$this->assertSame('6310',$m['taxableProfit']['numerator']);$this->assertNull($m['special']['colecta']);
}elseif($case==='negative'){
$this->assertSame('not_applicable',$a['rows'][0]['status']);$this->assertFalse($a['monthly']['assets'][0]['applied']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
}else{
$this->assertSame('absent',$a['status']);$this->assertSame([],$a['monthly']['assets']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
}
file_put_contents(base_path('outputs/irpc-l7-months/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcSpecialStartPeriodDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_start_period_cbf16_recovers_new_results_missing_cases_and_accepted_history(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
foreach(['start','fraction','missing','negative','absent']as$case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-start-period/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$a=$m['special']['acceleration'];
$this->assertSame('IRPC-normal-adjustments/12',$m['version']);$this->assertSame($b['special']['acceleration']['monthly'],$a['monthly']['input']);
$this->assertContains('IRPC-normal-adjustments/11',array_column(array_column($b['history'],'model'),'version'));$this->assertContains('IRPC-normal-adjustments/10',array_column(array_column($b['history'],'model'),'version'));
$this->assertSame($b['depreciation'],$a['normal']['input']);$this->assertSame('L8',$m['special']['finalBalance']['scope']);$this->assertNull($m['special']['finalBalance']['payable']);
$this->assertNull($m['special']['finalBalance']['recoverable']);$this->assertNull($m['tax']);$this->assertArrayHasKey('irpcWithholdings',$s);
if($case==='start'){
$v=$a['monthly']['assets'][0];$this->assertSame(6,$v['months']);$this->assertSame('50',$v['normalQuota']['numerator']);$this->assertSame('75',$v['acceleratedQuota']['numerator']);
$this->assertSame('60',$v['acceleratedDeduction']['numerator']);$this->assertSame('10',$a['additionalDeduction']['numerator']);
$this->assertSame('3130',$m['taxableProfit']['numerator']);$this->assertSame('3120',$m['special']['taxableProfit']['numerator']);$this->assertSame(['numerator'=>'4992','denominator'=>'5'],$m['special']['colecta']);$this->assertTrue($m['periodsFx']['short']);$this->assertSame('2026-07-01',$a['monthly']['period']['from']);$this->assertStringContainsString('início de actividade',$a['source']);
$this->assertSame($b['depreciation'],$a['accelerated']['input']);
}elseif($case==='fraction'){
$v=$a['monthly']['assets'][0];$this->assertSame(1,$v['months']);$this->assertSame(['numerator'=>'25','denominator'=>'3'],$v['normalQuota']);$this->assertSame(['numerator'=>'5','denominator'=>'3'],$a['additionalDeduction']);
}elseif($case==='missing'){
$this->assertSame('indeterminate',$a['status']);$this->assertNull($a['additionalDeduction']);$this->assertSame('3130',$m['taxableProfit']['numerator']);$this->assertNull($m['special']['colecta']);
}elseif($case==='negative'){
$this->assertSame('not_applicable',$a['rows'][0]['status']);$this->assertFalse($a['monthly']['assets'][0]['applied']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
}else{
$this->assertSame('absent',$a['status']);$this->assertSame([],$a['monthly']['assets']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
}
file_put_contents(base_path('outputs/irpc-l7-start-period/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,21 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcWithholdingDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_irpc_recovers_private_retentions_payments_and_history_without_changing_installed_books():void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$d=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-l5-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');$this->assertSame($d['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);
$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$key=$d['context_opportunity'];$b=$s['irpcWithholdings'][$key];$this->assertSame('calculated',$b['result']['rows'][0]['status']);$this->assertSame(['numerator'=>'10000','denominator'=>'1'],$b['result']['rows'][0]['tax']);$this->assertSame(['numerator'=>'6000','denominator'=>'1'],$b['result']['rows'][0]['settled']);$this->assertSame(['numerator'=>'4000','denominator'=>'1'],$b['result']['rows'][0]['outstanding']);$this->assertSame('2026-10',$b['result']['rows'][0]['period']);$this->assertSame('received',$b['result']['rows'][1]['direction']);$this->assertSame(['numerator'=>'2000','denominator'=>'1'],$b['result']['rows'][1]['tax']);$this->assertSame(['numerator'=>'500','denominator'=>'1'],$b['result']['rows'][1]['outstanding']);
$old=json_decode(file_get_contents(base_path('tests/frontend/fixtures/ispc-residual-preserved.json')),true,flags:JSON_THROW_ON_ERROR);foreach(json_decode($old['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR)as$k=>$v)$this->assertSame($v,$s[$k],$k.' must be preserved');
if(is_dir(base_path('outputs/irpc-l5-20261025')))file_put_contents(base_path('outputs/irpc-l5-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IspcResidualDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_new_residual_books_recover_without_rewriting_installed_habitual_or_L3_results():void
{
$t=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$t->name);$this->assertSame(55441,(int)$t->port);config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$d=json_decode(file_get_contents(base_path('tests/frontend/fixtures/ispc-residual-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');$this->assertSame($d['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
$own=$s['ispcResiduals']['opportunity:376']['occasional']['result'];$this->assertSame('calculated',$own['status']);$this->assertSame(['numerator'=>'3000','denominator'=>'1'],$own['amounts']['tax']);$this->assertSame('singular',$own['input']['personType']);
$a=$s['ispcResiduals']['opportunity:375']['acquisitions']['result'];$this->assertSame('calculated',$a['status']);$this->assertSame(['numerator'=>'5000','denominator'=>'1'],$a['amounts']['coveredTax']);$this->assertSame(['numerator'=>'20000','denominator'=>'1'],$a['amounts']['excessTax']);$this->assertSame(['numerator'=>'25000','denominator'=>'1'],$a['amounts']['total']);
$this->assertSame('partial',$s['ispcResiduals']['opportunity:375']['formalities']['result']['status']);$this->assertStringContainsString('Indeterminado',$s['ispcResiduals']['opportunity:375']['formalities']['result']['details']['formPeriod']);
$old=json_decode(file_get_contents(base_path('tests/frontend/fixtures/ispc-quarter-preserved.json')),true,flags:JSON_THROW_ON_ERROR);foreach(json_decode($old['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR)as$key=>$value){if($key==='ispcWithholdings')continue;if($key==='ispcEligibility'){foreach($value as$ctx=>$book)$this->assertSame($book,$s[$key][$ctx]);}else$this->assertSame($value,$s[$key]);}
if(is_dir(base_path('outputs/ispc-residuals-20261025')))file_put_contents(base_path('outputs/ispc-residuals-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityManagement;
use App\Services\ProactiveInvestment;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\Support\ProactiveInvestmentFixture as F;
use Tests\TestCase;
final class ProactiveInvestmentTest extends TestCase
{
use DatabaseTransactions;
private User $actor;
protected function setUp(): void
{
parent::setUp();F::guard();$this->withoutVite();
config(['portal.profile_access_enabled'=>true,'portal.opportunity_specialized_drafts_enabled'=>true,'portal.managers'=>[],'investor-path.enabled'=>true,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false]);
$this->actor=F::operator();$this->login($this->actor);
}
private function login(User $u): void {$this->actingAs($u)->withSession(['portal.mfa_actor'=>$u->id,'portal.mfa_at'=>time(),'portal.access_revision'=>(int)$u->portal_access_revision]);}
private function create(string $pilot='Cahora Bassa'): Opportunity
{
$response=$this->postJson('/minhas-oportunidades/drafts',F::input($pilot));if($response->status()!==201)fwrite(STDERR,substr($response->getContent(),0,400)."\n");$id=$response->assertCreated()->json('id');return Opportunity::findOrFail($id);
}
private function publication(Opportunity $o,array $patch=[]): array
{
$a=app(OpportunityManagement::class)->values($o)['proactive'];
return [...['etag'=>app(OpportunityManagement::class)->etag($o),'action'=>'disclose','title'=>$o->title,'summary'=>'SYNTHETIC QA ideia em preparação; não é oportunidade com participação disponível.','description'=>'Conteúdo público expressamente redigido.','limitations'=>'Conclusões locais por confirmar.','source_reference'=>'SYNTHETIC QA exemplo público','edition'=>'QA1','source_date'=>'2026-10-27','sector_id'=>$o->sector_id,'territory_ids'=>[$o->primary_territory_id]],...F::publication($a),...$patch];
}
private function publish(Opportunity $o): array
{
$p=$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa',$this->publication($o))->assertOk()->json();
$input=['token'=>$p['token'],'operation_key'=>(string)Str::uuid(),'confirm'=>true,'reason'=>'SYNTHETIC QA explicit public projection reviewed without private information'];
$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/confirmar',$input)->assertOk();return $input;
}
public function test_legacy_records_without_analysis_add_no_publication_lookup(): void
{
$o=new Opportunity;$o->forceFill(['workflow_context'=>[]]);DB::enableQueryLog();DB::flushQueryLog();
$this->assertNull(app(ProactiveInvestment::class)->publicData($o));$this->assertSame([],DB::getQueryLog());DB::disableQueryLog();
}
public function test_create_recover_edit_with_etag_and_historical_foundation_without_financial_or_promoter_requirements(): void
{
$o=$this->create();$a=app(OpportunityManagement::class)->values($o)['proactive'];$tag=app(OpportunityManagement::class)->etag($o);
$this->assertNull($o->estimated_investment_amount);$this->assertNull($o->promoter_name);$this->assertNotNull($o->primary_territory_id);
$this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertOk()->assertInertia(fn($page)=>$page->where('values.proactive.need.state','hypothesis')->has('proactiveHistory',1));
$a['need']['state']='confirmed';$a['need']['confirmation']='SYNTHETIC QA confirmation of fixture conditions only, no real market validation.';
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>$tag,'proactive'=>$a,'reason'=>'SYNTHETIC QA updated documented foundation for this fixture only'])->assertOk();
$this->assertSame('confirmed',app(OpportunityManagement::class)->values($o->fresh())['proactive']['need']['state']);
$history=app(ProactiveInvestment::class)->history($o);$this->assertCount(2,$history);$this->assertSame('hypothesis',$history[1]['state']);
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>$tag,'proactive'=>$a,'reason'=>'SYNTHETIC QA stale version cannot overwrite current analysis'])->assertStatus(409);
}
public function test_hypothesis_cannot_be_confirmed_without_documented_conditions_and_missing_is_not_zero(): void
{
$o=$this->create();$a=app(OpportunityManagement::class)->values($o)['proactive'];$a['need']['state']='confirmed';$a['need']['observed_demand']='';
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA incomplete confirmation must preserve prior hypothesis'])->assertUnprocessable();
$this->assertSame('hypothesis',app(OpportunityManagement::class)->values($o->fresh())['proactive']['need']['state']);
$a['need']['state']='hypothesis';$a['idea']['resources']='';$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA unknown resource remains blank rather than zero'])->assertOk();
$this->assertNull(app(OpportunityManagement::class)->values($o->fresh())['proactive']['idea']['resources']);
}
public function test_methodology_alone_cannot_confirm_local_need(): void
{
$o=$this->create();$a=app(OpportunityManagement::class)->values($o)['proactive'];$a['need']['state']='confirmed';$a['evidence'][0]['purpose']='methodology';
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA international methodology is not evidence of local demand'])->assertUnprocessable();
$this->assertSame('hypothesis',app(OpportunityManagement::class)->values($o->fresh())['proactive']['need']['state']);
}
public function test_creation_replay_and_duplicate_evidence_and_relations_do_not_duplicate_analysis(): void
{
$input=F::input();$id=$this->postJson('/minhas-oportunidades/drafts',$input)->assertCreated()->json('id');$this->postJson('/minhas-oportunidades/drafts',$input)->assertCreated()->assertJsonPath('id',$id);
$o=Opportunity::findOrFail($id);$a=$input['proactive'];$a['evidence'][]=[...$a['evidence'][0],'id'=>(string)Str::uuid()];
$this->putJson('/minhas-oportunidades/'.$id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA repeated evidence must be reconciled, not counted twice'])->assertUnprocessable();
$this->assertSame(1,Opportunity::where('management_creation_key',$input['creation_key'])->count());
}
public function test_document_from_another_scope_cannot_be_attached_or_reclassified_as_public(): void
{
$o=$this->create();$a=app(OpportunityManagement::class)->values($o)['proactive'];$a['evidence'][0]['document_id']=(string)Str::uuid();
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA document association must use existing access rules'])->assertUnprocessable();
}
public function test_existing_permissions_reject_unassigned_and_unrelated_operators(): void
{
$o=$this->create();$this->login(User::factory()->create());
$this->postJson('/minhas-oportunidades/drafts',F::input())->assertForbidden();
$this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertForbidden();
$this->login(F::operator());$this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertForbidden();
$this->login($this->actor);$input=F::input();unset($input['proactive']);$this->postJson('/minhas-oportunidades/drafts',$input)->assertForbidden();
$this->assertNull($o->organization_id);
}
public function test_internal_attached_document_cannot_be_changed_to_public_through_analysis(): void
{
$o=$this->create();$doc=(string)Str::uuid();$hash=hash('sha256','SYNTHETIC QA private source');
DB::table('documents')->insert(['id'=>$doc,'file_name'=>'SYNTHETIC-QA-INTERNAL-LIST.txt','storage_key'=>'synthetic-qa/'.$doc,'media_type'=>'text/plain','sha256'=>$hash,'size_bytes'=>strlen('SYNTHETIC QA private source'),'uploaded_by'=>$this->actor->id,'status'=>'active','data_classification'=>'internal','malware_scan_status'=>'clean','malware_scanned_sha256'=>$hash]);
DB::table('opportunity_documents')->insert(['id'=>(string)Str::uuid(),'opportunity_id'=>$o->id,'document_id'=>$doc,'version'=>1,'created_by'=>$this->actor->id,'payload'=>json_encode(['title'=>'SYNTHETIC QA internal list','visibility'=>'internal'])]);
$a=app(OpportunityManagement::class)->values($o)['proactive'];$a['evidence'][0]['document_id']=$doc;$a['evidence'][0]['access']='public';
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA original document restrictions cannot be overridden here'])->assertUnprocessable();
$this->assertSame('internal',DB::table('documents')->where('id',$doc)->value('data_classification'));
}
public function test_selected_public_relationship_and_assumptions_travel_without_private_reference_or_duplicate(): void
{
$o=$this->create();$a=app(OpportunityManagement::class)->values($o)['proactive'];$target=app(\App\Services\OpportunityClassification::class)->targets('project')->first(['id','name']);$this->assertNotNull($target);
$r=['id'=>(string)Str::uuid(),'kind'=>'project','target_id'=>$target->id,'relation'=>'hypothesis','reference'=>'PRIVATE-QA-RELATION-REFERENCE-NOT-FOR-PUBLICATION','evidence_ids'=>[$a['evidence'][0]['id']]];$a['relations']=[$r];
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA reference is a hypothesis, not access or contract'])->assertOk();$o->refresh();
$bad=$a;$bad['relations'][]=[...$r,'id'=>(string)Str::uuid()];$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$bad,'reason'=>'SYNTHETIC QA same reference cannot appear twice in the same analysis'])->assertUnprocessable();
$d=$this->publication($o);$d['proactive_public']=[...$d['proactive_public'],'relation_ids'=>[$r['id']],'resources'=>'SYNTHETIC QA public resource, no guaranteed capacity.','assumptions'=>'No contract, land or power guaranteed.','conditions'=>'Confirm actual local evidence.'];
$p=$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa',$d)->assertOk()->json();$this->assertSame($target->name,$p['projection']['proactive']['relations'][0]['label']);$this->assertStringNotContainsString('PRIVATE-QA-RELATION',json_encode($p['projection']));
$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/confirmar',['token'=>$p['token'],'operation_key'=>(string)Str::uuid(),'confirm'=>true,'reason'=>'SYNTHETIC QA explicitly reviewed public relationship and assumptions'])->assertOk();
$ref=app(\App\Services\InvestorContext::class)->reference('opportunity:'.$o->id);$this->assertSame('hypothesis',$ref['analysis']['proactive']['relations'][0]['relationship']);$this->assertSame('No contract, land or power guaranteed.',$ref['analysis']['proactive']['assumptions']);
}
public function test_publication_needs_explicit_review_and_rejects_internal_source_and_literal_leakage(): void
{
$o=$this->create();$data=$this->publication($o);unset($data['proactive_reviewed']);$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa',$data)->assertUnprocessable();
$data=$this->publication($o);$a=app(OpportunityManagement::class)->values($o)['proactive'];$data['proactive_public']['evidence_ids']=[$a['evidence'][1]['id']];$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa',$data)->assertUnprocessable();
$data=$this->publication($o,['description'=>$a['evidence'][1]['statement']]);$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa',$data)->assertUnprocessable();
$data=$this->publication($o);$data['proactive_public']['state']='confirmed';$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa',$data)->assertUnprocessable();
}
public function test_public_snapshot_separate_from_private_edits_and_anonymous_access_no_coordinates_required(): void
{
$o=$this->create();$private=$o->management_draft;$confirm=$this->publish($o);$this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/confirmar',$confirm)->assertOk()->assertJsonPath('replayed',true);
$o->refresh();$this->assertSame($private,$o->management_draft);$this->assertFalse(app(ProactiveInvestment::class)->publicData($o)['economic_validation']);
$a=app(OpportunityManagement::class)->values($o)['proactive'];$a['idea']['solution']='PRIVATE-QA-NEW-UNPUBLISHED-SOLUTION';
$this->putJson('/minhas-oportunidades/'.$o->id.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA private update must not change published projection'])->assertOk();
$this->app['auth']->forgetGuards();$this->get('/oportunidades/'.$o->slug)->assertOk()->assertDontSee('PRIVATE-QA-');
$this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertRedirect('/login');
$this->assertNotNull(app(ProactiveInvestment::class)->publicData($o->fresh()));
}
public function test_three_synthetic_pilots_reuse_existing_territories_and_catalogue_groups(): void
{
foreach(['Cahora Bassa','Montepuez','Gurué']as$pilot){$o=$this->create($pilot);$this->publish($o);$this->assertNotNull($o->fresh()->primary_territory_id);$this->assertTrue(app(\App\Services\InvestmentCatalogue::class)->query(['perspective'=>'ideas'])->whereKey($o->id)->exists());$this->assertFalse(app(\App\Services\InvestmentCatalogue::class)->query(['perspective'=>'needs'])->whereKey($o->id)->exists());}
$this->get('/oportunidades?perspective=ideas')->assertOk();
}
public function test_existing_facts_and_needs_do_not_require_irrelevant_idea_fields(): void
{
foreach(['exists','needs']as$kind){
$input=F::input();$input['proactive']['kind']=$kind;$id=$this->postJson('/minhas-oportunidades/drafts',$input)->assertCreated()->json('id');$o=Opportunity::findOrFail($id);
$d=$this->publication($o);unset($d['proactive_public']['proposal']);if($kind==='exists')unset($d['proactive_public']['need']);
$p=$this->postJson('/minhas-oportunidades/'.$id.'/divulgacao/previa',$d)->assertOk()->json();$this->assertSame($kind,$p['projection']['proactive']['group']);$this->assertSame('',$p['projection']['proactive']['proposal']);
$this->postJson('/minhas-oportunidades/'.$id.'/divulgacao/confirmar',['token'=>$p['token'],'operation_key'=>(string)Str::uuid(),'confirm'=>true,'reason'=>'SYNTHETIC QA only applicable questions for this content group'])->assertOk();
$this->assertTrue(app(\App\Services\InvestmentCatalogue::class)->query(['perspective'=>$kind])->whereKey($id)->exists());
}
}
public function test_private_analysis_complements_keep_preparation_valid_without_inventing_public_offers(): void
{
$private=$this->create('Gurué');$public=$this->create();
$input=$this->publication($public,['summary'=>'SYNTHETIC QA: silos e armazenagem como componente documentada.']);
$plan=$this->postJson('/minhas-oportunidades/'.$public->id.'/divulgacao/previa',$input)->assertOk()->json();
$this->postJson('/minhas-oportunidades/'.$public->id.'/divulgacao/confirmar',['token'=>$plan['token'],'operation_key'=>(string)Str::uuid(),'confirm'=>true,'reason'=>'SYNTHETIC QA public component in transaction only'])->assertOk();
$result=$this->postJson('/complementos-investimento',['contexts'=>[['origin'=>'opportunity:'.$private->id],['origin'=>'opportunity:'.$public->id]]])->assertOk()->json();
$this->assertSame([],$result['issues']);$this->assertNotEmpty($result['items']);
foreach($result['items']as$item)$this->assertSame('opportunity:'.$public->id,$item['origin']['key']);
$this->assertStringNotContainsString('PRIVATE-QA-',json_encode($result));
$this->assertFalse(Opportunity::published()->whereKey($private->id)->exists());
$this->assertSame('draft',$private->fresh()->status);
}
public function test_private_analysis_complements_still_hide_unauthorised_sources(): void
{
$private=$this->create('Gurué');$this->login(User::factory()->create());
$this->postJson('/complementos-investimento',['origin'=>'opportunity:'.$private->id])->assertNotFound();
$result=$this->postJson('/complementos-investimento',['contexts'=>[['origin'=>'opportunity:'.$private->id]]])->assertOk()->json();
$this->assertSame([],$result['items']);$this->assertCount(1,$result['issues']);
$this->assertStringNotContainsString($private->title,json_encode($result));
$this->assertStringNotContainsString('PRIVATE-QA-',json_encode($result));
}
public function test_authenticated_multi_idea_preparation_keeps_each_origin_and_deliberate_edits_in_existing_dossier(): void
{
$first=$this->create();$second=$this->create('Gurué');$this->publish($first);$this->publish($second);$investor=User::factory()->create();$this->login($investor);
$key=(string)Str::uuid();$create=['title'=>'SYNTHETIC QA deliberately chosen project title','client_key'=>$key,'reuse_consent'=>true];
$id=$this->postJson('/investimentos',$create)->assertCreated()->json('id');$this->postJson('/investimentos',$create)->assertCreated()->assertJsonPath('id',$id);
$before=$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
$input=['version'=>$before['case']['version'],'selection'=>[],'opportunity_choices'=>array_map(fn($o)=>['source'=>'opportunity:'.$o->id,'territory'=>'territory:'.$o->primary_territory_id],[$first,$second])];
$preview=$this->postJson('/investimentos/'.$id.'/contexto/previa',$input)->assertOk()->json();
$this->assertCount(2,$preview['opportunity_choices']);$this->assertSame('SYNTHETIC QA deliberately chosen project title',$preview['fields']['project_title']);
foreach($preview['opportunity_choices']as$c){$this->assertSame('ideas',$c['opportunity']['analysis']['proactive']['group']);$this->assertStringNotContainsString('PRIVATE-QA-',json_encode($c));}
$confirm=[...$input,'preview_hash'=>$preview['preview_hash'],'operation_key'=>(string)Str::uuid(),'confirm'=>true];$this->postJson('/investimentos/'.$id.'/contexto/confirmar',$confirm)->assertOk();$this->postJson('/investimentos/'.$id.'/contexto/confirmar',$confirm)->assertOk()->assertJsonPath('replayed',true);
$saved=$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();$this->assertCount(2,$saved['context']['opportunity_choices']);
$consumers=[];foreach([$first,$second]as$record){$consumer=$this->getJson('/investimentos/'.$id.'/contexto?opportunity=opportunity:'.$record->id)->assertOk()->json();$snapshot=json_decode($consumer['prefill']['simulator']['context_applicability_snapshot'],true);$this->assertCount(1,$snapshot['opportunity_choices']);$this->assertSame('opportunity:'.$record->id,$snapshot['opportunity_choices'][0]['opportunity']['key']);$this->assertSame('ideas',$snapshot['opportunity_choices'][0]['opportunity']['analysis']['proactive']['group']);$this->assertArrayNotHasKey('tax_benefit',$consumer['prefill']['simulator']);$consumers[]=$consumer;}$saved['consumer_contexts']=$consumers;
file_put_contents(base_path('outputs/proactive-investment-20261027/recovered-context.json'),json_encode($saved,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR));
$this->get('/investimentos/'.$id)->assertOk();$this->login($this->actor);$this->getJson('/investimentos/'.$id.'/contexto')->assertForbidden();
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityManagement;
use App\Services\ProactiveSupply;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Support\ProactiveInvestmentFixture as F;
use Tests\TestCase;
final class ProactiveSupplyTest extends TestCase
{
use DatabaseTransactions;
private User $actor;
private string $territory;
private string $observation;
protected function setUp():void
{
parent::setUp();F::guard();$this->withoutVite();
config(['portal.profile_access_enabled'=>true,'portal.opportunity_specialized_drafts_enabled'=>true,'portal.managers'=>[],'investor-path.enabled'=>true,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false]);
$this->actor=F::operator();$this->login($this->actor);
$input=F::input('Gurué');$this->territory=$input['district_id'];
$hash=hash('sha256','SYNTHETIC-QA-SUPPLY-SOURCE');
DB::table('proactive_supply_sources')->insert(['sha256'=>$hash,'name'=>'SYNTHETIC-QA-SUPPLY.xlsx','period'=>'2020','limitations'=>'Synthetic, not a real market source','receipt'=>'{}','imported_at'=>now()]);
$this->observation=hash('sha256','SYNTHETIC-QA-OBSERVATION');
$row=['id'=>$this->observation,'source_hash'=>$hash,'sheet'=>'QA','row_number'=>2,'entity_key'=>hash('sha256','QA-ENTITY'),'name'=>'PRIVATE-UNIQUE-QA-SUPPLIER','province'=>'Zambézia','district'=>'Gurué','cae'=>'33120','activity'=>'SYNTHETIC QA repair','sector'=>'SYNTHETIC QA manufacturing','source_date'=>'2020','fingerprint'=>hash('sha256','QA-FINGERPRINT'),'duplicate_of'=>null,'identity_conflict'=>false,'territory_id'=>$this->territory,'territory_resolution'=>'exact_normalized_province_district'];
DB::table('proactive_supply_observations')->insert($row);
DB::table('proactive_supply_observations')->insert([...$row,'id'=>hash('sha256','QA-REPEATED-OBSERVATION'),'row_number'=>3,'duplicate_of'=>$this->observation]);
}
private function login(User $u):void{$this->actingAs($u)->withSession(['portal.mfa_actor'=>$u->id,'portal.mfa_at'=>time(),'portal.access_revision'=>(int)$u->portal_access_revision]);}
private function url(string $extra=''):string{return '/minhas-oportunidades/oferta-interna?territory_id='.$this->territory.$extra;}
public function test_private_query_filters_supply_and_preserves_source_and_duplicate_count():void
{
$r=$this->getJson($this->url('&activity=SYNTHETIC%20QA%20repair'))->assertOk()->assertJsonPath('total',1)->assertJsonPath('duplicate_sightings',1)->assertJsonPath('rows.0.source_name','SYNTHETIC-QA-SUPPLY.xlsx')->assertJsonPath('rows.0.row_number',2)->assertJsonPath('rows.0.source_date','2020');
$this->assertStringContainsString('no-store',$r->headers->get('Cache-Control'));
$this->assertArrayNotHasKey('contact',$r->json('rows.0'));
$this->assertArrayNotHasKey('nuit',$r->json('rows.0'));
}
public function test_no_match_means_insufficient_information_not_absent_market():void
{
$this->getJson($this->url('&activity=NO-SUCH-SYNTHETIC-ACTIVITY'))->assertOk()->assertJsonPath('total',0)->assertJsonPath('status','Informação insuficiente nesta base e nestes filtros.');
}
public function test_unassigned_user_cannot_read_internal_register():void
{
$this->login(User::factory()->create());$this->getJson($this->url())->assertForbidden();
$this->getJson('/minhas-oportunidades/oferta-interna?territory_id=not-a-uuid')->assertForbidden();
}
public function test_comparison_persists_source_lineage_and_rejects_forgery_and_public_reclassification():void
{
$input=F::input('Gurué');$a=$input['proactive'];$id=(string)\Illuminate\Support\Str::uuid();
$a['evidence'][]=['id'=>$id,'interpretation'=>'SYNTHETIC QA possible complement, not a contract','verified_on'=>'2026-10-28',...app(ProactiveSupply::class)->evidence($this->actor,$this->observation,null)];
$a['supply_comparisons']=[['observation_id'=>$this->observation,'relationship'=>'complementarity','rationale'=>'SYNTHETIC QA compare servicing with tea-chain idea','limitations'=>'No current capacity or demand proved']];
$input['proactive']=$a;
$oid=$this->postJson('/minhas-oportunidades/drafts',$input)->assertCreated()->json('id');$o=Opportunity::findOrFail($oid);
$saved=app(OpportunityManagement::class)->values($o)['proactive'];$this->assertSame($this->observation,$saved['supply_comparisons'][0]['observation_id']);
$this->assertSame('internal',$saved['evidence'][2]['access']);$this->assertStringContainsString('SYNTHETIC-QA-SUPPLY.xlsx',$saved['evidence'][2]['source']);
$a['evidence'][2]['source']='Forged source';$a['evidence'][2]['statement']='Forged supply fact';
$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA canonical source lineage is server-resolved'])->assertOk();
$saved=app(OpportunityManagement::class)->values($o->fresh())['proactive'];$this->assertStringContainsString('PRIVATE-UNIQUE-QA-SUPPLIER',$saved['evidence'][2]['statement']);
$a['evidence'][2]['access']='public';$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA internal list cannot become public'])->assertUnprocessable();
$a['evidence'][2]['access']='internal';$a['supply_comparisons'][0]['observation_id']=str_repeat('f',64);
$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA unknown supply observation rejected'])->assertUnprocessable();
}
public function test_sector_filter_does_not_infer_absence_and_comparison_requires_its_evidence():void
{
$this->getJson($this->url('&sector=NO-SUCH-SECTOR'))->assertOk()->assertJsonPath('total',0);
$input=F::input('Gurué');$input['proactive']['supply_comparisons']=[['observation_id'=>$this->observation,'relationship'=>'partner','rationale'=>'SYNTHETIC QA prospective partner','limitations'=>'Not contacted, no agreement']];
$this->postJson('/minhas-oportunidades/drafts',$input)->assertUnprocessable();
}
public function test_database_policy_hides_internal_rows_without_actor_context():void
{
$role='supply_qa_'.bin2hex(random_bytes(6));
DB::statement('CREATE ROLE '.$role.' NOLOGIN');
DB::statement('GRANT USAGE ON SCHEMA public TO '.$role);
DB::statement('GRANT SELECT ON proactive_supply_sources,proactive_supply_observations TO '.$role);
DB::select("select set_config('app.actor_user_id','',true),set_config('app.proactive_supply_read','',true)");
try{
DB::statement('SET LOCAL ROLE '.$role);
$this->assertSame(0,DB::table('proactive_supply_observations')->count());
$this->assertSame(0,DB::table('proactive_supply_sources')->count());
DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)",[(string)$this->actor->id]);
$this->assertNotNull(DB::table('proactive_supply_observations')->where('id',$this->observation)->first());
}finally{DB::statement('RESET ROLE');}
}
public function test_preserved_repetitions_cannot_inflate_saved_comparisons():void
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
$this->expectExceptionMessage('Esta observação administrativa é repetida.');
$comparison=['relationship'=>'competition','rationale'=>'SYNTHETIC QA duplicate must not count as another competitor','limitations'=>'Historical observation,not a unique company'];
app(ProactiveSupply::class)->comparisons($this->actor,[
[...$comparison,'observation_id'=>$this->observation],
[...$comparison,'observation_id'=>hash('sha256','QA-REPEATED-OBSERVATION')],
],null);
}
}
@@ -0,0 +1,169 @@
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Services\CorridorAgribusinessOpportunities;
use App\Services\InvestmentGeography;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
class ProvinceSearchCompatibilityTest extends TestCase
{
use DatabaseTransactions;
public function createApplication(): Application
{
$app = parent::createApplication();
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
throw new \LogicException('Isolated QA required');
}
return $app;
}
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
private function createOpportunity(?string $territory, string $prefix, string $status = 'published', ?string $province = null): Opportunity
{
return Opportunity::create(['sector_id' => 1, 'reference' => 'SYNTHETIC-QA-'.Str::uuid(), 'slug' => 'synthetic-'.Str::uuid(),
'title' => $prefix.' synthetic provincial search', 'status' => $status, 'published_at' => now()->subMinute(),
'primary_territory_id' => $territory, 'province' => $province, 'district' => null, 'geography_scope' => 'specific']);
}
private function requests(array $filters): array
{
$query = http_build_query([...$filters, 'per_page' => 50]);
return ['/api/v1/investment/search?'.$query, '/api/v1/opportunities/search?'.$query];
}
public function test_a_single_province_includes_all_lower_levels_and_linked_locations_in_both_apis(): void
{
$geo = app(InvestmentGeography::class);
$locality = $geo->publicReferences()->where('location_type', 'locality')->orderBy('id')->first();
$this->assertNotNull($locality, 'Public locality fixture required');
$path = $geo->path($locality->id);
$this->assertCount(4, $path);
$prefix = 'SYNTHETIC-'.Str::uuid();
$records = array_map(fn ($node) => $this->createOpportunity($node->id, $prefix, 'published', 'Legacy label differs from canonical hierarchy'), $path);
$linked = $this->createOpportunity(null, $prefix);
$linked->additionalTerritories()->attach($locality->id);
$records[] = $linked;
$private = $this->createOpportunity($locality->id, $prefix, 'draft');
$deleted = $this->createOpportunity($locality->id, $prefix);
$deleted->delete();
$otherProvince = $geo->publicReferences()->where('location_type', 'province')->where('id', '<>', $path[0]->id)->first();
$outside = $this->createOpportunity($otherProvince->id, $prefix);
foreach ([['province_id' => $path[0]->id], ['province' => $path[0]->name], ['geo_location_id' => $path[0]->id]] as $entry) {
foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) {
$response = $this->getJson($url)->assertOk()->assertJsonPath('total', 5);
$this->assertEqualsCanonicalizing(array_map(fn ($row) => $row->id, $records), array_column($response->json('data'), 'id'), $url);
foreach ($response->json('data') as $row) {
$this->assertNull($row['district']);
}
foreach ([$private, $deleted, $outside] as $row) {
$this->assertNotContains($row->id, array_column($response->json('data'), 'id'));
}
}
}
foreach ($records as $record) {
$this->assertNull($record->fresh()->district);
$this->assertSame($record->primary_territory_id, $record->fresh()->primary_territory_id);
}
$this->assertSame([$locality->id], $linked->additionalTerritories()->pluck('geo_locations.id')->all());
foreach ($this->requests(['province_id' => $path[0]->id, 'district_id' => $path[1]->id, 'q' => $prefix]) as $url) {
// Province-only records are not silently assigned to a district.
$this->getJson($url)->assertOk()->assertJsonPath('total', 4);
}
foreach ($this->requests(['district_id' => $path[1]->id]) as $url) {
$this->getJson($url)->assertUnprocessable();
}
}
public function test_published_province_declaration_is_found_without_inventing_a_district_or_overriding_an_explicit_other_territory(): void
{
$geo = app(InvestmentGeography::class);
$province = $geo->publicReferences()->where('location_type', 'province')->first();
$other = $geo->publicReferences()->where('location_type', 'province')->where('id', '<>', $province->id)->first();
$district = $geo->publicReferences()->where('parent_id', $province->id)->where('location_type', 'district')->first();
$prefix = 'SYNTHETIC-'.Str::uuid();
$legacy = $this->createOpportunity(null, $prefix, 'published', $province->name);
$contradictory = $this->createOpportunity($other->id, $prefix, 'published', $province->name);
$linkedOutside = $this->createOpportunity(null, $prefix, 'published', $province->name);
$linkedOutside->additionalTerritories()->attach($other->id);
foreach ([['province' => $province->name], ['province_id' => $province->id], ['geo_location_id' => $province->id]] as $entry) {
foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) {
$this->getJson($url)->assertOk()->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $legacy->id)->assertJsonPath('data.0.district', null);
}
}
foreach ($this->requests(['province_id' => $province->id, 'district_id' => $district->id, 'q' => $prefix]) as $url) {
$this->getJson($url)->assertOk()->assertJsonPath('total', 0);
}
$this->assertNull($legacy->fresh()->primary_territory_id);
$this->assertNull($legacy->fresh()->district);
$this->assertSame($other->id, $contradictory->fresh()->primary_territory_id);
}
public function test_provincial_only_search_covers_the_eleven_provinces_without_including_neighbouring_provinces(): void
{
$geo = app(InvestmentGeography::class);
$provinces = $geo->publicReferences()->where('location_type', 'province')->get();
$this->assertCount(11, $provinces);
$prefix = 'SYNTHETIC-'.Str::uuid();
$records = [];
foreach ($provinces as $province) {
$district = $geo->publicReferences()->where('parent_id', $province->id)->where('location_type', 'district')->first();
$this->assertNotNull($district, $province->name);
$records[$province->id] = $this->createOpportunity($district->id, $prefix);
}
foreach ($provinces as $province) {
foreach ([['province_id' => $province->id], ['province' => $province->name]] as $entry) {
foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) {
$this->getJson($url)->assertOk()->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $records[$province->id]->id);
}
}
}
}
public function test_maputo_names_ids_page_map_and_export_share_the_existing_approved_corridor_coverage(): void
{
$provinceId = 'bfde2651-2b46-5d9c-a500-733fa55a8c5c';
$expected = Opportunity::published()->where('workflow_context->corridor_agribusiness->source_sha256', CorridorAgribusinessOpportunities::SOURCE_HASH)
->where('workflow_context->corridor_agribusiness->corridor', 'Corredor de Maputo')->pluck('id')->all();
$this->assertCount(5, $expected);
foreach ([['province' => 'Maputo Província'], ['province' => ' mApUtO Província '], ['province_id' => $provinceId]] as $entry) {
foreach ($this->requests([...$entry, 'sector' => 'agronegocio']) as $url) {
$response = $this->getJson($url)->assertOk()->assertJsonPath('total', 5);
$this->assertEqualsCanonicalizing($expected, array_column($response->json('data'), 'id'));
foreach ($response->json('data') as $row) {
$this->assertNull($row['district']);
$this->assertStringContainsString('contexto provincial do corredor', $row['territorial_summary']);
}
}
}
$query = http_build_query(['sector' => 'agronegocio', 'province' => 'Maputo Província']);
$this->get('/oportunidades?'.$query)->assertInertia(fn (AssertableInertia $page) => $page
->component('investment/catalogue')->where('filters.province_id', $provinceId)->missing('filters.province')
->has('territorySelection', 1)->where('results.total', 5));
$this->getJson('/api/v1/investment/map?'.$query)->assertOk()->assertJsonCount(0, 'features')->assertJsonCount(5, 'unlocated');
$csv = $this->get('/api/v1/investment/export?'.$query)->assertOk()->streamedContent();
foreach (Opportunity::whereIn('id', $expected)->get() as $row) {
$this->assertStringContainsString($row->title, $csv);
}
foreach ($this->requests(['sector' => 'turismo', 'province_id' => $provinceId]) as $url) {
$response = $this->getJson($url)->assertOk();
foreach ($response->json('data') as $row) {
$this->assertNotContains($row['id'], $expected);
}
}
}
}
@@ -0,0 +1,126 @@
<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Services\SectorInvestment;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class SectorCatalogueContentTest extends TestCase
{
use DatabaseTransactions;
public function createApplication(): Application
{
$app = parent::createApplication();
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
throw new \LogicException('Isolated QA required for sector catalogue restoration.');
}
return $app;
}
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
config(['portal.legacy_content_enabled' => false, 'portal.legacy_content_qa_enabled' => false,
'investor-path.enabled' => true, 'portal.profile_access_enabled' => true]);
}
private function sector(array $attributes = []): Sector
{
return Sector::create([...['name' => 'Sector sintético QA', 'slug' => 'qa-sector-'.Str::lower(Str::random(12)),
'short_description' => 'Exemplo isolado para a reposição.', 'is_active' => true, 'published_at' => now()->subDay()], ...$attributes]);
}
public function test_complete_catalogue_matches_public_sector_pages_including_sectors_without_guides(): void
{
$withoutGuide = $this->sector();
$this->assertArrayNotHasKey($withoutGuide->slug, SectorInvestment::CONTENT);
$before = [DB::table('desk_cases')->count(), DB::table('opportunities')->count(), DB::table('portal_resources')->count()];
$props = $this->get('/sectores')->assertOk()->inertiaProps();
$this->assertArrayNotHasKey('sectorDetails', $props);
$ids = array_column($props['sectors'], 'id');
$this->assertSame($ids, array_values(array_unique($ids)));
foreach ($props['sectors'] as $card) {
$this->assertSame(\App\Services\SectorOrganization::href($card['slug']), $card['url']);
$this->assertStringNotContainsString('#', $card['url']);
$single = $this->get($card['url'])->assertOk()->inertiaProps();
$this->assertSame('sector', $single['section']);
$this->assertSame($card['id'], $single['sector']['id']);
$this->assertArrayNotHasKey('sectorDetails', $single);
$this->assertArrayNotHasKey('sectors', $single);
$this->assertIsArray($single['sectorEntry']['articles']);
$this->assertIsArray($single['sectorEntry']['relations']);
$this->assertIsArray($single['sectorEntry']['locations']);
}
$entry = $this->get('/sectores/'.$withoutGuide->slug)->assertOk()->inertiaProps()['sectorEntry'];
$this->assertSame([], $entry['articles']);
$this->assertSame('/minhas-oportunidades/create?sector_id='.$withoutGuide->id, $entry['register_href']);
$this->assertNotNull($entry['start_href']);
$this->assertGuest();
$this->assertSame($before, [DB::table('desk_cases')->count(), DB::table('opportunities')->count(), DB::table('portal_resources')->count()]);
}
public function test_full_catalogue_preserves_publication_boundaries_and_private_tool_notices(): void
{
$public = $this->sector();
$unpublished = $this->sector(['published_at' => null]);
$inactive = $this->sector(['is_active' => false]);
$slugs = [];
foreach (['published', 'draft', 'future'] as $state) {
$slug = 'qa-sector-opportunity-'.Str::lower(Str::random(12));
$slugs[$state] = $slug;
Opportunity::create(['sector_id' => $public->id, 'reference' => $slug, 'title' => $state,
'slug' => $slug, 'summary' => 'Conteúdo sintético de teste.',
'status' => $state === 'draft' ? 'draft' : 'published', 'published_at' => $state === 'future' ? now()->addDay() : now()->subDay()]);
}
config(['investor-path.enabled' => false, 'portal.profile_access_enabled' => false]);
$props = $this->get('/sectores')->assertOk()->inertiaProps();
$this->assertArrayNotHasKey('sectorDetails', $props);
$ids = array_column($props['sectors'], 'id');
$this->assertNotContains($unpublished->id, $ids);
$this->assertNotContains($inactive->id, $ids);
$detail = $this->get('/sectores/'.$public->slug)->assertOk()->inertiaProps();
$this->assertSame([$slugs['published']], array_column($detail['opportunities'], 'slug'));
$this->assertNull($detail['sectorEntry']['start_href']);
$this->assertNull($detail['sectorEntry']['register_href']);
$this->assertNotEmpty($detail['sectorEntry']['start_notice']);
$this->assertNotEmpty($detail['sectorEntry']['register_notice']);
$this->get('/sectores/'.$unpublished->slug)->assertNotFound();
$this->get('/sectores/'.$inactive->slug)->assertNotFound();
}
public function test_selecting_a_sector_keeps_opportunities_scoped_on_reload_and_return(): void
{
$a = $this->sector();$b = $this->sector();
foreach ([$a, $b] as $sector) {
$slug = 'qa-isolated-'.Str::lower(Str::random(12));
Opportunity::create(['sector_id'=>$sector->id, 'reference'=>$slug, 'title'=>$slug, 'slug'=>$slug,
'summary'=>'Synthetic scoped opportunity', 'status'=>'published', 'published_at'=>now()->subDay()]);
}
$index = $this->get('/sectores')->assertOk()->inertiaProps();
foreach ([$a, $b, $a] as $sector) {
$url = collect($index['sectors'])->firstWhere('id', $sector->id)['url'];
$first = $this->get($url)->assertOk()->inertiaProps();
$reload = $this->get($url)->assertOk()->inertiaProps();
$this->assertSame($first['sector'], $reload['sector']);
$this->assertSame($first['opportunities'], $reload['opportunities']);
$this->assertSame($sector->id, $first['sector']['id']);
$this->assertSame(Opportunity::where('sector_id',$sector->id)->pluck('slug')->all(), array_column($first['opportunities'],'slug'));
$this->assertSame('/investimentos/a-partir-do-setor/'.$sector->slug, $first['sectorEntry']['start_href']);
$this->get($first['sectorEntry']['start_href'])->assertRedirect('/login');
$returned = $this->get('/sectores')->assertOk()->inertiaProps();
$this->assertSame($index['sectors'], $returned['sectors']);
$this->assertArrayNotHasKey('sectorDetails', $returned);
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace Tests\Support;
use App\Models\User;
use App\Models\Sector;
use App\Services\OpportunityManagement;
use App\Services\PortalProfileAdministration;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/** Synthetic fixture only, exclusively in the isolated QA database. */
final class ProactiveInvestmentFixture
{
public static function guard(): void
{
$t=DB::selectOne('select current_database() as name,inet_server_port() as port');
throw_unless($t->name==='proactive_investment_20261027_test'&&(int)$t->port===55441,\RuntimeException::class,'Isolated proactive QA required');
}
public static function operator(): User
{
self::guard();
$actor=User::factory()->create(['name'=>'SYNTHETIC QA editorial operator','email'=>'proactive-'.Str::uuid().'@example.invalid','password'=>bcrypt('Synthetic-QA-only-2026!')]);
$actor->forceFill(['two_factor_secret'=>Crypt::encrypt('JBSWY3DPEHPK3PXP'),'two_factor_confirmed_at'=>now()])->save();
$org=(string)Str::uuid();
DB::table('organizations')->insert(['id'=>$org,'name'=>'SYNTHETIC QA proactivity operator','external_id'=>'SYNTHETIC-QA-'.$org,'organization_type'=>'private']);
DB::table('audit_events')->insert(['source_system'=>'portal-profiles-v1','entity_type'=>'authorization','action'=>'operator_organization_registered','data_classification'=>'restricted','after_data'=>json_encode(['organization_id'=>$org,'organization'=>['name'=>'SYNTHETIC QA proactivity operator','nuit'=>'SYNTHETIC-QA-'.$org],'source_reference'=>'SYNTHETIC QA designation, not a real appointment'])]);
app(PortalProfileAdministration::class)->insertGrant((string)Str::uuid(),User::factory()->create(),['user_id'=>$actor->id,'role'=>'promoter','organization_id'=>$org,'opportunity_types'=>OpportunityManagement::CATEGORIES,'responsibilities'=>[],'source'=>'internal-portal-designation']);
return $actor;
}
public static function analysis(string $pilot='Cahora Bassa'): array
{
$id=(string)Str::uuid();$private=(string)Str::uuid();
return ['version'=>1,'kind'=>'ideas','evidence'=>[
['id'=>$id,'category'=>'infrastructure','purpose'=>'local','statement'=>'SYNTHETIC QA: capacidade fictícia em '.$pilot.'; não é diagnóstico económico real.','interpretation'=>'Hipótese sintética, sem contrato ou oferta comprovados.','source'=>'SYNTHETIC QA exemplar público fictício '.$pilot,'locator'=>'Fixture QA, linha1','period'=>'Exercício2026 — cenário sintético','scope'=>$pilot.' — contexto piloto','coverage'=>'Cobertura fictícia; informação real por confirmar','verified_on'=>'2026-10-27','access'=>'public','document_id'=>null],
['id'=>$private,'category'=>'supply','purpose'=>'local','statement'=>'PRIVATE-QA-UNIQUE-SUPPLY-'.Str::uuid(),'interpretation'=>'PRIVATE-QA-INTERPRETATION-'.Str::uuid(),'source'=>'PRIVATE-QA-COMPANY-LIST-'.Str::uuid(),'locator'=>'PRIVATE-QA-LOCATOR-'.Str::uuid(),'period'=>'2026','scope'=>'Análise interna','coverage'=>'Não divulgar','verified_on'=>'2026-10-27','access'=>'internal','document_id'=>null],
],'need'=>['state'=>'hypothesis','problem'=>'SYNTHETIC QA lacuna por verificar em '.$pilot,'observed_supply'=>'Oferta real desconhecida; não equivale a ausência.','observed_demand'=>'Procura real não comprovada; cenário sintético.','type'=>'support','clients'=>'Beneficiários hipotéticos, sem encomenda ou compromisso.','evidence_ids'=>[$id],'limitations'=>'Não demonstra mercado nem viabilidade.','missing'=>'Dados locais reais por confirmar.','confirmation'=>'Fundamento de hipótese sintética, não confirmação económica.'],
'idea'=>['solution'=>'SYNTHETIC QA: '.match($pilot){'Montepuez'=>'fornecimentos e manutenção mineira','Gurué'=>'serviços da cadeia do chá',default=>'serviços complementares de energia'},'activity'=>'Actividade hipotética de apoio','chain_id'=>null,'stage'=>'Serviços de apoio — por corresponder','coverage'=>$pilot,'mode'=>'create','resources'=>'Não demonstrados; por inventariar','assumptions'=>'Não há acesso garantido a energia, terra ou cliente.','conditions'=>'Confirmar dados e autorizações aplicáveis.','pending'=>'Evidência económica real','next_action'=>'Recolher dados locais e testar a hipótese.'],'relations'=>[]];
}
public static function input(string $pilot='Cahora Bassa',bool $analysis=true): array
{
self::guard();
$t=DB::table('geo_locations')->whereNull('deleted_at')->where('data_classification','public')->where('location_type','district')->where('name','ilike',$pilot==='Gurué'?'Guru%':($pilot==='Cahora Bassa'?'CAHORA-BASSA':$pilot))->first();
throw_unless($t,\RuntimeException::class,'Existing pilot territory required:'.$pilot);
$path=app(\App\Services\InvestmentGeography::class)->path($t->id,true);$geo=[];foreach($path as $n)if(in_array($n->location_type,['province','district','admin_post','locality']))$geo[$n->location_type.'_id']=$n->id;
$s=Sector::choices()->whereNotNull('published_at')->where('slug',match($pilot){'Cahora Bassa'=>'energia-especifica','Montepuez'=>'mineracao',default=>'agronegocio'})->firstOrFail();
return ['creation_key'=>(string)Str::uuid(),'reason'=>'SYNTHETIC QA creating an independent territorial analysis, no active publication','title'=>'SYNTHETIC QA — '.$pilot,'summary'=>'Piloto técnico sintético; conclusões económicas reais por confirmar.','description'=>'Preparação editorial sintética em QA.','sector_id'=>$s->id,'opportunity_category'=>'investment_project',...$geo,...($analysis?['proactive'=>self::analysis($pilot)]:[])];
}
public static function publication(array $analysis): array
{
return ['proactive_reviewed'=>true,'proactive_public'=>['group'=>$analysis['kind'],'state'=>'hypothesis','need'=>'SYNTHETIC QA necessidade por verificar.','proposal'=>'SYNTHETIC QA solução em preparação.','basis'=>'Fixture pública sintética; não é evidência de procura local.','limitations'=>'Sem validação económica, contrato, terra ou energia garantidos.','next_action'=>'Confirmar evidências reais antes de decidir.','evidence_ids'=>[$analysis['evidence'][0]['id']],'chain_id'=>null,'stage'=>'Apoio — correspondência real por confirmar']];
}
}
@@ -24,6 +24,10 @@ class OpportunityCorridorExplorerTest extends TestCase
$catalogue->shouldReceive('query')->with($filters)->once()->andReturn($query);
$catalogue->shouldReceive('context')->with(101)->once()->andReturn(['corridors' => collect([(object)['id'=>'a','name'=>'Corredor A'],(object)['id'=>'b','name'=>'Corredor B']])]);
$catalogue->shouldReceive('context')->with(102)->once()->andReturn(['corridors'=>collect()]);
$first->setAttribute('territorial_summary','Sofala / Dondo');
$other->setAttribute('territorial_summary','Localização não indicada');
$catalogue->shouldReceive('presentLocation')->with($first)->once()->andReturn($first);
$catalogue->shouldReceive('presentLocation')->with($other)->once()->andReturn($other);
$this->app->instance(InvestmentCatalogue::class, $catalogue);
$groups = app(OpportunityCorridorExplorer::class)->groups($filters);
$this->assertSame(['Corredor A','Corredor B','Outras localizações'], array_column($groups,'name'));
@@ -32,5 +36,6 @@ class OpportunityCorridorExplorerTest extends TestCase
$this->assertSame(['Sofala / Dondo'], $groups[0]['locations']);
$this->assertSame([], $groups[2]['locations']);
$this->assertSame([], $groups[0]['opportunities'][0]['stages']);
$this->assertSame('Sofala / Dondo',$groups[0]['opportunities'][0]['territorial_summary']);
}
}
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
const require=createRequire(import.meta.url);
const source=fs.readFileSync(new URL('../../resources/js/components/catalogue-guide-links.tsx',import.meta.url),'utf8');
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const module={exports:{}};
new Function('require','module','exports',compiled)(name=>name==='@inertiajs/react'?{Link:({href,children,...props})=>React.createElement('a',{href,...props},children)}:require(name),module,module.exports);
const render=articles=>renderToStaticMarkup(React.createElement(module.exports.default,{articles}));
test('catalogue guides are closed optional links, not educational bodies or nationwide opportunity shortcuts',()=>{
const html=render([{title:'Turismo',href:'/artigos/turismo',summary:'EDUCATIONAL-BODY-NOT-A-RESULT',image:{href:'/photo.jpg',alt:'ILLUSTRATION'},opportunities_href:'/oportunidades?sector=turismo',opportunities_label:'ALL-NATIONAL-OPPORTUNITIES',has_opportunities:true}]);
assert.match(html,/<details\b/);assert.doesNotMatch(html,/<details[^>]*\bopen\b/);
assert.match(html,/apoio opcional/);assert.match(html,/href="\/artigos\/turismo"/);
for(const forbidden of ['EDUCATIONAL-BODY-NOT-A-RESULT','ALL-NATIONAL-OPPORTUNITIES','/oportunidades?sector=turismo','photo.jpg','<img'])assert.ok(!html.includes(forbidden),forbidden);
});
test('no educational substitute is rendered when no guides are available',()=>{
assert.equal(render([]),'');
});
@@ -0,0 +1,101 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
assessmentReady,
diagnosticReady,
forces,
newCompetitivenessDraft,
readCompetitivenessDraft,
} from '../../resources/js/lib/cluster-competitiveness.ts';
function complete() {
const draft = newCompetitivenessDraft('cluster-a', 'Energia');
draft.region = 'Distrito de demonstração';
for (const force of forces)
Object.assign(draft.forces[force], {
pressure: 'medium',
evidence: 'Observação de demonstração',
source: 'Fonte de demonstração, p. 2',
period: '2025',
rationale: 'Pressão fundamentada na observação',
limitations: 'Amostra parcial',
opportunity: 'Investigar capacidade de armazenamento',
});
return draft;
}
test('ausência de dados permanece indeterminada nas cinco forças independentes', () => {
const draft = newCompetitivenessDraft('cluster-a', 'Energia');
assert.equal(forces.length, 5);
assert.equal(diagnosticReady(draft), false);
draft.forces.buyers.evidence = 'Observação';
assert.equal(draft.forces.suppliers.evidence, '');
assert.ok(forces.every((force) => draft.forces[force].pressure === ''));
});
test('classificação exige todos os fundamentos; âmbito é necessário para o diagnóstico', () => {
const draft = complete();
assert.equal(diagnosticReady(draft), true);
for (const field of [
'evidence',
'source',
'period',
'rationale',
'limitations',
'opportunity',
]) {
const row = { ...draft.forces.buyers, [field]: ' ' };
assert.equal(assessmentReady(row), false, field);
}
draft.region = '';
assert.equal(diagnosticReady(draft), false);
});
test('ficheiro conserva rascunhos, dados e incertezas sem modificar o original', () => {
const draft = complete();
draft.forces.entrants.pressure = '';
const restored = readCompetitivenessDraft(
JSON.stringify(draft),
'cluster-a',
);
assert.deepEqual(restored, draft);
restored.forces.buyers.source = 'Outra fonte';
assert.notEqual(restored.forces.buyers.source, draft.forces.buyers.source);
assert.equal(diagnosticReady(restored), false);
});
test('recuperação rejeita outro cluster, versões, forças ausentes e campos inválidos', () => {
assert.throws(() =>
readCompetitivenessDraft(JSON.stringify(complete()), 'cluster-b'),
);
for (const mutate of [
(d) => {
d.version = 2;
},
(d) => {
delete d.forces.buyers;
},
(d) => {
d.forces.buyers.pressure = 'zero';
},
(d) => {
d.forces.buyers.source = 42;
},
(d) => {
d.forces.buyers.source = 'x'.repeat(5001);
},
(d) => {
d.region = null;
},
]) {
const draft = complete();
mutate(draft);
assert.throws(() =>
readCompetitivenessDraft(JSON.stringify(draft), 'cluster-a'),
);
}
assert.throws(() => readCompetitivenessDraft('null', 'cluster-a'));
assert.throws(() =>
readCompetitivenessDraft('x'.repeat(250001), 'cluster-a'),
);
});
+34
View File
@@ -0,0 +1,34 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import * as forms from '../../resources/js/lib/fiscal-form-preparation.ts';
const require=createRequire(import.meta.url);
const source=fs.readFileSync(new URL('../../resources/js/pages/portal/fiscal-forms.tsx',import.meta.url),'utf8');
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const module={exports:{}};
new Function('require','module','exports',compiled)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({href,children,...props})=>React.createElement('a',{href,...props},children),router:{on:()=>()=>{}}}:name==='@/layouts/portal-navigation'?{PortalMenu:()=>null}:name==='@/lib/fiscal-form-preparation'?forms:require(name),module,module.exports);
test('fiscal entry renders labelled editable fields and a guided preparation rather than legislation',()=>{
const html=renderToStaticMarkup(React.createElement(module.exports.default));
for(const label of ['Identificação e período','Nome ou designação social','NUIT','Período — ano e mês','Conferir e guardar','Guardar ficheiro para continuar','Recuperar ficheiro guardado','Confirmar recuperação']) {
// Confirmation appears only after a compatible file is selected.
if(label !== 'Confirmar recuperação') assert.ok(html.includes(label),label);
}
assert.equal((html.match(/inputMode="decimal"/g)??[]).length,27);
assert.match(html,/sem submissão à Autoridade Tributária/);
assert.match(html,/Não há gravação automática/);
assert.doesNotMatch(html,/value="0"/);
});
test('IRPC entry opens the requested form directly and keeps its partial coverage explicit',()=>{
const html=renderToStaticMarkup(React.createElement(module.exports.default,{initialTax:'irpc'}));
assert.match(html,/IRPC — Modelo 22/);
assert.match(html,/L8 permanece parcial/);
assert.match(html,/303 — Dupla tributação económica/);
assert.match(html,/304 — Dupla tributação internacional/);
assert.match(html,/Não utilize a mesma parcela/);
assert.equal((html.match(/inputMode="decimal"/g)??[]).length,24);
assert.doesNotMatch(html,/01 — Base das operações/);
});
@@ -0,0 +1,34 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {newFiscalPreparation, readFiscalPreparation, fiscalPreparationIssues, fiscalFormFields} from '../../resources/js/lib/fiscal-form-preparation.ts';
test('IVA and IRPC preparations round trip deliberate entries and unknown amounts without zero defaults', () => {
for (const kind of ['iva', 'irpc']) {
const p = newFiscalPreparation(kind);
p.name = 'Investidor de teste'; p.nuit = '123456789'; p.period = kind === 'iva' ? '2026-10' : '2026';
const [first, second] = fiscalFormFields(kind);
p.values[first.code] = '123456789012345678901234,01';
p.values[second.code] = '';
const restored = readFiscalPreparation(JSON.stringify(p));
assert.deepEqual(restored, p);
assert.deepEqual(fiscalPreparationIssues(restored), []);
assert.equal(restored.values[second.code], '');
assert.equal(Object.values(restored.values).includes('0'), false);
}
});
test('foreign editions, unknown fields and malformed or oversized files cannot replace preparation', () => {
const p = newFiscalPreparation('iva');
for (const v of [{...p, sourceHash: 'different'}, {...p, values: {'999': '1'}}, {...p, values: []}, {...p, nuit: 123}, {...p, version: 'later'}]) assert.throws(() => readFiscalPreparation(JSON.stringify(v)));
assert.throws(() => readFiscalPreparation('{broken'));
assert.throws(() => readFiscalPreparation('x'.repeat(100001)));
assert.equal(p.kind, 'iva'); assert.deepEqual(p.values, {});
});
test('invalid format is identified without rounding or discarding an incomplete preparation', () => {
const p = newFiscalPreparation('irpc');
p.values['303'] = '1.234'; p.values['304'] = '25,00';
const restored = readFiscalPreparation(JSON.stringify(p));
assert.equal(restored.values['303'], '1.234');
assert.ok(fiscalPreparationIssues(restored).some(i => i.startsWith('Campo 303:')));
assert.equal(fiscalFormFields('irpc').find(f => f.code === '303').label, 'Dupla tributação económica');
assert.equal(fiscalFormFields('irpc').find(f => f.code === '304').label, 'Dupla tributação internacional');
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
{
"fixture_notice": "Synthetic public projection of accepted API QA consumers;no profiles,real pilots,credentials,dossier or personal data. Test input only;never import into application data.",
"consumer_contexts": [
{
"prefill": {
"simulator": {
"context_applicability_snapshot": "{\"version\":1,\"consumer_opportunity\":\"opportunity:399\",\"opportunity_choices\":[{\"opportunity\":{\"key\":\"opportunity:399\",\"title\":\"SYNTHETIC QA — Cahora Bassa\",\"version\":2,\"analysis\":{\"proactive\":{\"need\":\"SYNTHETIC QA necessidade por verificar.\",\"basis\":\"Fixture pública sintética; não é evidência de procura local.\",\"group\":\"ideas\",\"stage\":\"Apoio — correspondência real por confirmar\",\"state\":\"hypothesis\",\"notice\":\"A ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.\",\"version\":1,\"chain_id\":null,\"evidence\":[{\"id\":\"3587d2e4-7428-4cd7-994b-60607b20d7da\",\"scope\":\"Cahora Bassa — contexto piloto\",\"period\":\"Exercício2026 — cenário sintético\",\"source\":\"SYNTHETIC QA exemplar público fictício Cahora Bassa\",\"locator\":\"Fixture QA, linha1\",\"purpose\":\"local\",\"category\":\"infrastructure\",\"coverage\":\"Cobertura fictícia; informação real por confirmar\",\"statement\":\"SYNTHETIC QA: capacidade fictícia em Cahora Bassa; não é diagnóstico económico real.\",\"verified_on\":\"2026-10-27\"}],\"proposal\":\"SYNTHETIC QA solução em preparação.\",\"relations\":[],\"limitations\":\"Sem validação económica, contrato, terra ou energia garantidos.\",\"next_action\":\"Confirmar evidências reais antes de decidir.\",\"evidence_ids\":[\"3587d2e4-7428-4cd7-994b-60607b20d7da\"],\"economic_validation\":false,\"availability_confirmed\":false}}}}]}"
}
}
},
{
"prefill": {
"simulator": {
"context_applicability_snapshot": "{\"version\":1,\"consumer_opportunity\":\"opportunity:400\",\"opportunity_choices\":[{\"opportunity\":{\"key\":\"opportunity:400\",\"title\":\"SYNTHETIC QA — Gurué\",\"version\":2,\"analysis\":{\"proactive\":{\"need\":\"SYNTHETIC QA necessidade por verificar.\",\"basis\":\"Fixture pública sintética; não é evidência de procura local.\",\"group\":\"ideas\",\"stage\":\"Apoio — correspondência real por confirmar\",\"state\":\"hypothesis\",\"notice\":\"A ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.\",\"version\":1,\"chain_id\":null,\"evidence\":[{\"id\":\"1421bb80-78c0-40c9-9de0-f45e18c5a093\",\"scope\":\"Gurué — contexto piloto\",\"period\":\"Exercício2026 — cenário sintético\",\"source\":\"SYNTHETIC QA exemplar público fictício Gurué\",\"locator\":\"Fixture QA, linha1\",\"purpose\":\"local\",\"category\":\"infrastructure\",\"coverage\":\"Cobertura fictícia; informação real por confirmar\",\"statement\":\"SYNTHETIC QA: capacidade fictícia em Gurué; não é diagnóstico económico real.\",\"verified_on\":\"2026-10-27\"}],\"proposal\":\"SYNTHETIC QA solução em preparação.\",\"relations\":[],\"limitations\":\"Sem validação económica, contrato, terra ou energia garantidos.\",\"next_action\":\"Confirmar evidências reais antes de decidir.\",\"evidence_ids\":[\"1421bb80-78c0-40c9-9de0-f45e18c5a093\"],\"economic_validation\":false,\"availability_confirmed\":false}}}}]}"
}
}
}
]
}
@@ -0,0 +1,28 @@
import {internationalCase} from './irpc-international-case.mjs';
import {prepareCreditCarry} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newCarryLot} from '../../../resources/js/lib/irpc-credit-carry.ts';
export function carryCase() {
const c = internationalCase();
c.b = prepareCreditCarry(c.b);
Object.assign(c.b.creditCarry, {
state: 'known', reference: 'SYNTHETIC QA complete prior balance inventory',
currentInternational: '8', currentPec: '200', assessmentKey: c.b.assessmentKey,
allocationReference: 'SYNTHETIC QA explicit allocation: current international8, prior international50, currentPEC200, priorPEC100',
lots: [
{...newCarryLot('international-2021'), kind: 'international', country: 'ZA', subjectNuit: c.b.subjectNuit,
year: '2021', ordinary: 'yes', originDocument: 'SYNTHETIC foreign-credit-assessment2021',
originReference: 'SYNTHETIC QA origin inclusion, eligible tax, national/CDT limits, insufficient collecta',
ruleReference: 'SYNTHETIC QA documented ordinary origin rule art65, no real foreign treaty claim',
eligible: '200', originDeducted: '100', insufficient: 'yes', usesState: 'known',
usesReference: 'SYNTHETIC QA all actual uses reconciled through current exercise',
uses: [{id: 'use-2023', year: '2023', amount: '30', document: 'SYNTHETIC use2023', reference: 'SYNTHETIC QA actual documented use, not simulated'}], selected: '50'},
{...newCarryLot('pec-2023'), kind: 'pec', subjectNuit: c.b.subjectNuit,
year: '2023', ordinary: 'yes', originDocument: 'SYNTHETIC PEC2023 assessment and receipts',
originReference: 'SYNTHETIC QA actual PEC paid, initial deduction and collecta insufficiency',
ruleReference: 'SYNTHETIC QA origin ordinary art66', eligible: '300', originDeducted: '150',
insufficient: 'yes', usesState: 'absent', usesReference: 'SYNTHETIC QA no actual subsequent use', selected: '100'},
],
});
return c;
}
@@ -0,0 +1,25 @@
import {internationalCase} from './irpc-international-case.mjs';
import {carryCase} from './irpc-carry-case.mjs';
import {prepareEconomicBook} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newEconomicPosition} from '../../../resources/js/lib/irpc-economic-credit.ts';
export function economicCase(withCarry = false) {
const c = withCarry ? carryCase() : internationalCase();
c.b = prepareEconomicBook(c.b);
Object.assign(c.b.economicCredits, {state: 'known', reference: 'SYNTHETIC QA full ordinary economic-credit inventory', rows: [{
...newEconomicPosition('economic-1'), subjectNuit: c.b.subjectNuit, year: c.b.year, assessmentKey: c.b.assessmentKey,
kind: 'ordinary_profit', payerNuit: '111222333', recipientResident: 'yes', payerResident: 'yes', payerIrpc: 'yes', payerExempt: 'no',
article40: 'no', qualificationReference: 'SYNTHETIC QA entities seated/effectively managed in MZ; payer subject nonexempt IRPC',
article40Reference: 'SYNTHETIC QA explicit nonapplication of40(1), including2009 participation/period/recipient conditions',
distributionDocument: 'SYNTHETIC-DIVIDEND2026', distributionParcel: 'distribution-1', distributionDate: '2026-09-30',
income: '100', incomeIncluded: 'yes', incomeReference: 'SYNTHETIC QA100 gross of beneficiary withholding already included in current L6; no actual dividend assertion',
taxIssuer: 'SYNTHETIC domestic IRPC issuer', taxDocument: 'SYNTHETIC-SOURCE-IRPC2026', taxParcel: 'underlying-tax-1',
parcelReference: 'SYNTHETIC QA underlying domestic IRPC parcel, distinct from recipient withholding or foreign income tax',
proofAmount: '20', taxAmount: '20', attributionReference: 'SYNTHETIC QA documented IRPC20 corresponding to included profits100, not inferred by applying a rate to dividends',
previouslyAttributed: '0', priorAttributionReference: 'SYNTHETIC QA actual previous attribution to other profits known zero',
grossUpIncluded: '12', grossUpReference: 'SYNTHETIC QA53 nominal credit12 already included in reconciled current base4400; no base mutation',
nationalLimit: '32', nationalLimitReference: 'SYNTHETIC QA documented proportional current collecta ceiling32 after credit addition; not an automatic fraction derivation',
previousUse: {state: 'known', amount: '0', reference: 'SYNTHETIC QA actual prior economic-credit use known zero'},
}]});
return c;
}
@@ -0,0 +1,9 @@
import {pecCase} from './irpc-pec-case.mjs';
import {prepareInternationalBook,newInternationalProof} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newInternationalPosition} from '../../../resources/js/lib/irpc-international-credit.ts';
export function internationalCase(tax='10',national='8',pec='200',ppc='500',rf='0') {
const c=pecCase(pec,ppc,rf);c.b=prepareInternationalBook(c.b);
const p={...newInternationalPosition('country-za'),country:'ZA',qualificationCountry:'ZA',subjectNuit:c.b.subjectNuit,year:c.b.year,assessmentKey:c.b.assessmentKey,incomeAmount:'100',incomeIncluded:'yes',incomeReference:'SYNTHETIC QA: foreign income included in valid 2026 base',nationalLimit:national,nationalLimitReference:'SYNTHETIC QA: documented national fraction, no automatic derivation',treaty:'no',treatyReference:'SYNTHETIC QA assumption only; not an assertion about actual ZA convention applicability',previousUse:{state:'known',amount:'0',reference:'SYNTHETIC QA: actual previous credit use known zero'},payments:[{...newInternationalProof('foreign-tax'),document:'SYNTHETIC-FOREIGN-TAX-2026',issuer:'SYNTHETIC QA foreign authority',date:'2026-09-30',subjectNuit:c.b.subjectNuit,year:c.b.year,proofAmount:tax,amount:tax,effective:'yes',incomeTax:'yes',reference:'SYNTHETIC QA paid MZN tax amount, no real receipt',taxReference:'SYNTHETIC QA income tax nature and entitlement'}]};
Object.assign(c.b.internationalCredits,{state:'known',reference:'SYNTHETIC QA complete inventory for entity/year',positions:[p]});
return c;
}
+25
View File
@@ -0,0 +1,25 @@
import {irpcBook,newIrpcRow,saveIrpcBook,recordIrpc} from '../../../resources/js/lib/irpc-withholdings.ts';
import fs from 'node:fs';
import {newPecPayment} from '../../../resources/js/lib/irpc-pec.ts';
import {newAnnualBook,newAnnualPpc,reuseAnnualAssessment,reuseAnnualWithholding} from '../../../resources/js/lib/irpc-annual-settlement.ts';
export function pecCase(amount='200',ppc='500',rf='0') {
const d=JSON.parse(fs.readFileSync(new URL('../fixtures/irpc-assessment-preserved.json',import.meta.url),'utf8'));
if(rf!=='0'){
const l5=irpcBook(d);
l5.rows.push({...newIrpcRow('pec-rf'),direction:'received',origin:'effective',counterparty:'QA AT',documentRef:'QA rendimento próprio 2026',base:String(Number(rf)*5),currency:'MZN',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'resident',mozambique:'yes',qualificationRef:'QA royalties próprios incluídos no resultado anual',specialRelief:'none',eventsComplete:'yes',events:[{kind:'payment',date:'2026-10-02',reference:'QA rendimento primeiro evento'}],settlement:'complete',settlementRef:'QA retenção sofrida reconciliada',allocations:[{paymentId:'pec-rf-proof',amount:rf}],ordinary:{nonDigital:'yes',capitalType:'',rentOwnPropertyManagement:'',specialFacts:'no',capitalSeatAndManagement:'',reference:'QA royalties não digitais de residente'}});
l5.payments.push({id:'pec-rf-proof',kind:'withholding_proof',reference:'QA-RF-PEC-2026',date:'2026-10-02',amount:rf,currency:'MZN'});
d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,l5,'2026-10-27T15:00:00+02:00'));
}
let b=reuseAnnualAssessment(d,newAnnualBook());
b.reference='QA PEC: inventário completo da entidade/exercício';b.assessmentReference='QA colecta válida já demonstrada';
b.withholdings={state:'absent',reference:'QA sem retenções próprias neste caso',rows:[]};
b.ppc={state:ppc==='0'?'absent':'known',reference:'QA inventário de PPC efectivo',rows:ppc==='0'?[]:[{...newAnnualPpc('ppc'),document:'QA-PPC-PEC-2026',issuer:'QA AT',date:'2026-06-30',subjectNuit:b.subjectNuit,year:b.year,proofAmount:ppc,amount:ppc,effective:'yes',reference:'QA prova efectiva de natureza PPC'}]};
b.pec={state:'absent',amount:'',reference:'QA resumo anterior preservado'};
b.credits={state:'known',amount:'0',reference:'QA créditos precedentes conhecidos zero'};
b.pecPayments={version:1,state:'known',reference:'QA inventário de pagamentos PEC 2026',rows:[{...newPecPayment('pec'),document:'QA-PEC-2026',issuer:'QA AT',date:'2026-08-31',subjectNuit:b.subjectNuit,year:b.year,proofAmount:amount,amount,effective:'yes',previouslyUsed:'0',priorUseReference:'QA reconciliação: zero utilizações fora destas parcelas',reference:'QA comprovativo pago de natureza PEC'}]};
if(rf!=='0'){
b.withholdings.state='known';b=reuseAnnualWithholding(d,b,'pec-rf','pec-rf-proof','rf-claim');
Object.assign(b.withholdings.rows[0],{incomeIncluded:'yes',reference:'QA titular e rendimento incluído no exercício'});
}
return {d,b};
}
@@ -0,0 +1,80 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import * as flow from '../../resources/js/lib/opportunity-fill-flow.ts';
import { fiscalPreparationGuide } from '../../resources/js/lib/fiscal-preparation-guide.ts';
const require=createRequire(import.meta.url);
function load(path,mocks={},exportName='default',extra=''){
const source=fs.readFileSync(new URL(path,import.meta.url),'utf8');
const output=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const module={exports:{}};new Function('require','module','exports',output+extra)(name=>mocks[name]??(name.startsWith('@/')?{default:()=>null}:require(name)),module,module.exports);return module.exports[exportName];
}
function stateHooks(){const memory=[];let cursor=0;return{hooks:{...React,useState(initial){const i=cursor++;if(!(i in memory))memory[i]=typeof initial==='function'?initial():initial;return[memory[i],next=>memory[i]=typeof next==='function'?next(memory[i]):next];},useRef:()=>({current:null}),useEffect:()=>{}},reset:()=>cursor=0};}
const flat=node=>!node||typeof node!=='object'?[]:[node,...React.Children.toArray(node.props?.children).flatMap(flat)];
test('free navigation keeps every field mounted and reports resource work in the current group',()=>{
const state=stateHooks();const Component=load('../../resources/js/components/opportunity-fill-flow.tsx',{'react':state.hooks,'@/lib/opportunity-fill-flow':flow});
const groups=flow.opportunityFillGroups(['official_title','locations','documents']);const saved={official_title:'Deliberate user title',locations:['saved-site']};
const opened=[];let tree;
const render=()=>{state.reset();tree=Component({groups,errors:{},renderField:field=>React.createElement('input',{name:field,value:JSON.stringify(saved[field]??''),readOnly:true}),onOpenResources:group=>opened.push(group)});};
const priorWindow=globalThis.window,priorFrame=globalThis.requestAnimationFrame;
globalThis.window={history:{state:{existing:'inertia-state'},replaceState(state,_title,url){assert.deepEqual(state,{existing:'inertia-state'});this.url=url;}}};globalThis.requestAnimationFrame=callback=>callback();
try{
render();assert.equal(flat(tree).filter(node=>node.type==='input').length,3);
flat(tree).find(node=>node.type==='a'&&node.props.href==='#fill-territory').props.onClick({preventDefault(){}});render();
const sections=flat(tree).filter(node=>node.type==='section');
assert.equal(sections.find(node=>node.props.id==='fill-territory').props.hidden,false);
assert.equal(sections.find(node=>node.props.id==='fill-idea').props.hidden,true);
assert.equal(flat(tree).filter(node=>node.type==='input').length,3);
flat(tree).find(node=>node.type==='button').props.onClick();assert.deepEqual(opened,['territory']);
assert.equal(saved.official_title,'Deliberate user title');assert.deepEqual(saved.locations,['saved-site']);
assert.equal(globalThis.window.history.url,'#fill-territory');
}finally{globalThis.window=priorWindow;globalThis.requestAnimationFrame=priorFrame;}
});
test('profile request renders readable labels without dumping every available document into the form',()=>{
const Picker=load('../../resources/js/components/profile-proof-picker.tsx');
const Page=load('../../resources/js/pages/profiles/request.tsx',{
'@/lib/operation-key':{createOperationKey:()=> 'synthetic-key'},
'@inertiajs/react':{Head:()=>null,Link:({href,children})=>React.createElement('a',{href},children),router:{}},
'@/components/profile-navigation':{default:()=>null},'@/lib/profile-json':{},
'@/pages/management/shared':{button:'button',control:'control'},
'@/components/opportunity-search-filters':{modalityLabels:{investment_project:'Projecto de investimento',land_rights:'Direitos fundiários'}},
'@/components/profile-proof-picker':{default:Picker},
});
const html=renderToStaticMarkup(React.createElement(Page,{requests:[],types:['investment_project','land_rights'],organizations:[],documents:Array.from({length:80},(_,i)=>({id:`doc-${i}`,file_name:`UNSELECTED-DOCUMENT-${i}.txt`}))}));
assert.match(html,/Em nome próprio/);assert.match(html,/Entidade pública/);assert.match(html,/Projecto de investimento/);assert.match(html,/Direitos fundiários/);
assert.doesNotMatch(html,/UNSELECTED-DOCUMENT/);assert.match(html,/Pesquisar nos meus ficheiros/);
const visible=html.replace(/<[^>]+>/g,'');assert.doesNotMatch(visible,/investment_project|land_rights|public_entity/);
});
test('proof selection is explicit and retained when the filename search changes',()=>{
const state=stateHooks();const Component=load('../../resources/js/components/profile-proof-picker.tsx',{'react':state.hooks});
const documents=[{id:'identity',file_name:'Documento próprio'},{id:'law',file_name:'Lei.pdf'}];const values=[];let tree;
const render=()=>{state.reset();tree=Component({documents,value:values,onChange:ids=>{values.splice(0,values.length,...ids);}});};
render();assert.equal(flat(tree).filter(node=>node.type==='input'&&node.props.type==='checkbox').length,0);
flat(tree).find(node=>node.type==='input').props.onChange({target:{value:'próprio'}});render();
flat(tree).find(node=>node.type==='input'&&node.props.type==='checkbox').props.onChange();render();assert.deepEqual(values,['identity']);
flat(tree).find(node=>node.type==='input'&&node.props.type==='search').props.onChange({target:{value:'Lei'}});render();
assert.deepEqual(values,['identity']);assert.equal(flat(tree).filter(node=>node.type==='input'&&node.props.type==='checkbox')[0].props.checked,false);
});
test('document entry leads to existing fillable preparation and keeps sources optional',()=>{
const context='10000000-0000-4000-8000-000000000001';
const inertia={Link:({href,children})=>React.createElement('a',{href},children),usePage:()=>({url:`/documentos?context=${context}`})};
const Hub=load('../../resources/js/components/journey-document-resources.tsx',{'@inertiajs/react':inertia,'@/lib/fiscal-preparation-guide':{fiscalPreparationGuide}},'JourneyDocumentResources');
const hub=renderToStaticMarkup(React.createElement(Hub,{}));
assert.match(hub,new RegExp(`/investimentos/${context}/contexto/editar#preparation-questions`));assert.match(hub,/href="\/terra-duat"/);
assert.match(hub,/Preparação digital e exemplar oficial/);assert.doesNotMatch(hub,/<details[^>]*\bopen\b/);
const Editor=load('../../resources/js/components/private-investment-preparation.tsx',{'@inertiajs/react':inertia,'@/pages/investment/shared':{card:'card',input:'input'}},'PreparationEditor');
const html=renderToStaticMarkup(React.createElement(Editor,{data:{questions:[{key:'investment_preparation',label:'Preparar pedido?',options:{yes:'Sim',no:'Não'},source:'SYNTHETIC QA'},{key:'declared_amount',label:'Valor do investimento',type:'number',source:'SYNTHETIC QA'}],requirements:[],enabled:true,mode:'authorization',missing_fields:['declared_amount']},fields:{investment_preparation:'yes'},changes:{},documents:[],links:{},onChange(){},onLink(){},disabled:false}));
assert.match(html,/<input/);assert.match(html,/Valor do investimento/);assert.match(html,/id="preparation-questions"/);
});
test('investment stages expose readable next actions while internal codes and sources remain consultable',()=>{
const useForm=data=>({data,errors:{},processing:false,setData(){},transform(){return this;},post(){}});
const Stage=load('../../resources/js/pages/investor-path/show.tsx',{'@inertiajs/react':{useForm,Link:({href,children})=>React.createElement('a',{href},children)},'@/lib/investor-path':{pathStates:{not_started:'Por iniciar'}}},'Stage','\nmodule.exports.Stage=Stage;');
const step={code:'S01',title:'Sistematizar a ideia',state:'not_started',detail:{scope:'promoter_preparation'},preparation_allowed:true,action_gaps:{},source_paragraph:22,applicability:'applicable'};
const html=renderToStaticMarkup(React.createElement(Stage,{step,path:{case:{id:'synthetic',version:1},next:{code:'S01'},canWrite:true,canReview:false,steps:[step],documents:[]}}));
assert.match(html,/<details[^>]*id="S01"[^>]*open/);assert.match(html,/<summary[^>]*>Sistematizar a ideia/);
assert.doesNotMatch(html,/<summary[^>]*>S01/);assert.match(html,/Fonte e referência desta etapa/);
});
@@ -0,0 +1,105 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,saveNormalBook} from '../../resources/js/lib/irpc-normal.ts';
import {irpcBook,newIrpcRow,saveIrpcBook,recordIrpc} from '../../resources/js/lib/irpc-withholdings.ts';
import {annualBook,newAnnualBook,newAnnualPpc,saveAnnualBook,reuseAnnualAssessment,annualAssessment,annualWithholdingOptions,reuseAnnualWithholding,calculateAnnual,recordAnnual,annualCurrent,annualReportRows,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const read=n=>JSON.parse(fs.readFileSync(new URL('./fixtures/'+n,import.meta.url),'utf8'));
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
const now='2026-10-26T18:00:00+02:00';
const original=()=>read('irpc-assessment-preserved.json');
function acceptedL7(expense=false){const d=read('irpc-l7-preserved.json')[expense?'expense':'acceleration'];if(!expense){const f=read('irpc-l7-continuation-preserved.json');d.business_plan_snapshot=f.plan;d.modelo_a_snapshot=saveNormalBook(d,f.book);}return d;}
function setup(rf='300',ppc='500'){
const d=original(),l5=irpcBook(d);
if(rf!==null){
const row={...newIrpcRow('L8-received'),direction:'received',origin:'effective',counterparty:'QA pagador A',documentRef:'QA rendimento próprio 2026',base:String(Number(rf)*5),currency:'MZN',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'resident',mozambique:'yes',qualificationRef:'QA royalties próprios incluídos no resultado anual',specialRelief:'none',eventsComplete:'yes',events:[{kind:'payment',date:'2026-10-02',reference:'QA rendimento e primeiro evento 2026'}],settlement:'complete',settlementRef:'QA retenção efectivamente sofrida e reconciliada',allocations:[{paymentId:'RF-L8',amount:rf}],ordinary:{nonDigital:'yes',capitalType:'',rentOwnPropertyManagement:'',specialFacts:'no',capitalSeatAndManagement:'',reference:'QA royalties não digitais de residente'}};
l5.rows.push(row);l5.payments.push({id:'RF-L8',kind:'withholding_proof',reference:'QA-RF-2026-1',date:'2026-10-02',amount:rf,currency:'MZN'});
d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,l5,now));
}
let b=reuseAnnualAssessment(d,newAnnualBook());b.reference='QA entidade123456789,exercício2026,declaração integral dos abatimentos';b.assessmentReference='QA colecta válida já demonstrada1408';
b.withholdings={state:rf===null?'absent':'known',reference:'QA inventário completo das retenções efectivamente sofridas por este sujeito no exercício',rows:[]};
b.ppc={state:ppc===null?'absent':'known',reference:'QA inventário completo dos PPC efectuados no exercício',rows:[]};
b.pec={state:'absent',amount:'',reference:'QA ausência de PEC'};b.credits={state:'known',amount:'0',reference:'QA créditos com regras próprias conhecidos zero'};
if(rf!==null){b=reuseAnnualWithholding(d,b,'L8-received','RF-L8','claim-rf');Object.assign(b.withholdings.rows[0],{incomeIncluded:'yes',reference:'QA próprio beneficiário NUIT123456789;rendimento incluído na base anual2026'});}
if(ppc!==null)b.ppc.rows.push({...newAnnualPpc('claim-ppc'),document:'QA-PPC-2026-1',issuer:'QA AT',date:'2026-06-30',subjectNuit:b.subjectNuit,year:b.year,proofAmount:ppc,amount:ppc,effective:'yes',reference:'QA recibo de PPC efectivo e afectação ao sujeito/exercício'});
return{d,b};
}
const model=(d,b)=>calculateAnnual(d,b);
const pending=(d,b)=>{const m=model(d,b);assert.equal(m.status,'indeterminate',m.pending.join(' | '));assert.equal(m.arithmeticBalance,null);assert.equal(m.payable,null);assert.equal(m.recoverable,null);return m;};
test('optional L8 leaves accepted assessment, other books and report rows untouched',()=>{
const d=original(),before=JSON.stringify(d),b=normalBook(d);assert.equal(normalCurrent(d,b),true);assert.equal(val(annualAssessment(d).colecta),1408);assert.equal(annualBook(d),undefined);assert.deepEqual(annualReportRows(d),[]);assert.equal(JSON.stringify(d),before);
const old=acceptedL7();assert.deepEqual(annualReportRows(old),[]);assert.equal(normalBook(old).result.model.version,'IRPC-normal-adjustments/13');
});
test('1408 minus actual received300 and provenPPC500 yields608 payable without consuming official balances',()=>{
const{d,b}=setup(),before=d.modelo_a_snapshot,m=model(d,b);assert.deepEqual(m.pending,[]);assert.equal(m.status,'payable');assert.equal(val(m.knownWithholdings),300);assert.equal(val(m.knownPpc),500);assert.equal(val(m.payable),608);assert.equal(val(m.recoverable),0);assert.equal(m.officialBalancesConsumed,false);assert.equal(m.submitted,false);assert.equal(d.modelo_a_snapshot,before);
});
test('1408 minus1000 and600 yields192 recoverable;408 and1000 balances exactly',()=>{
for(const[rf,ppc,status,balance]of[['1000','600','recoverable',-192],['408','1000','balanced',0]]){const{d,b}=setup(rf,ppc),m=model(d,b);assert.deepEqual(m.pending,[]);assert.equal(m.status,status);assert.equal(val(m.arithmeticBalance),balance);assert.equal(val(m.recoverable),balance<0?-balance:0);}
});
test('declared absence yields1408;known zero has its own proof and missing never becomes zero',()=>{
const{d,b}=setup(null,null);assert.equal(val(model(d,b).payable),1408);b.credits.state='';assert.match(pending(d,b).pending.join(' '),/Créditos/);b.credits={state:'absent',amount:'',reference:'QA ausência'};b.ppc.state='';assert.match(pending(d,b).pending.join(' '),/PPC/);
const zero=setup('0','0');assert.equal(val(model(zero.d,zero.b).payable),1408);assert.equal(model(zero.d,zero.b).input.withholdings.state,'known');
});
test('effective received L5 proof is required, not a projection,remittance or definitive withholding',()=>{
for(const patch of[{origin:'projected'},{direction:'paid'},{residence:'nonresident',permanentEstablishment:'no'}]){const{d,b}=setup(),l5=irpcBook(d);Object.assign(l5.rows.find(r=>r.id==='L8-received'),patch);d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,l5,now));const option=annualWithholdingOptions(d).find(o=>o.rowId==='L8-received');assert.equal(option.ready,false);pending(d,b);}
const{d,b}=setup(),l5=irpcBook(d);l5.payments.find(p=>p.id==='RF-L8').kind='remittance';d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,l5,now));pending(d,b);
});
test('beneficiary,income inclusion,proof date,entity,exercise and actualPPC are mandatory',()=>{
for(const change of[b=>b.withholdings.rows[0].incomeIncluded='',b=>b.withholdings.rows[0].reference='',b=>b.withholdings.rows[0].subjectNuit='999999999',b=>b.year='2025',b=>b.ppc.rows[0].subjectNuit='999999999',b=>b.ppc.rows[0].year='2025',b=>b.ppc.rows[0].currency='USD',b=>b.ppc.rows[0].date='2026-02-30',b=>b.ppc.rows[0].date='2027-01-01',b=>b.ppc.rows[0].effective='no',b=>b.ppc.rows[0].reference='',b=>b.reference='']){const{d,b}=setup();change(b);pending(d,b);}
});
test('selected current source and reusing collection never overwrite deliberate entity/year edits',()=>{
const{d,b}=setup();b.subjectNuit='999999999';b.year='2025';const next=reuseAnnualAssessment(d,b);assert.equal(next.subjectNuit,'999999999');assert.equal(next.year,'2025');pending(d,next);
const other=setup();other.b.assessmentKey='old-source';pending(other.d,other.b);
});
test('absence contradiction and incomplete group inventories are indeterminate,not assumed totals',()=>{
for(const change of[b=>{b.withholdings={state:'absent',reference:'QA ausência contraditória',rows:[]};},b=>b.ppc.rows=[],b=>b.ppc.state='absent',b=>b.withholdings.reference='']){const{d,b}=setup();change(b);pending(d,b);}
});
test('duplicate pair,duplicatePPC,overclaim and cross-kind physical proof cannot be deducted twice',()=>{
for(const change of[b=>b.withholdings.rows.push({...b.withholdings.rows[0],id:'duplicate'}),b=>b.ppc.rows.push({...b.ppc.rows[0],id:'duplicate'}),b=>b.withholdings.rows[0].amount='301',b=>b.ppc.rows[0].amount='501',b=>Object.assign(b.ppc.rows[0],{document:' QA-RF-2026-1 ',issuer:'QA pagador A',date:'2026-10-02',amount:'100',proofAmount:'300'})]){const{d,b}=setup();change(b);pending(d,b);}
const{d,b}=setup();assert.throws(()=>reuseAnnualWithholding(d,b,'L8-received','RF-L8','again'),/já seleccionada/);
});
test('partial proof allocations allowed only within its available amount across context maps',()=>{
const{d,b}=setup(null,'500');b.ppc.rows[0].amount='300';const other=structuredClone(b);other.ppc.rows[0].id='other';other.ppc.rows[0].amount='200';const s=JSON.parse(d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};d.modelo_a_snapshot=JSON.stringify(s);assert.equal(val(model(d,b).payable),1108);other.ppc.rows[0].amount='201';d.modelo_a_snapshot=JSON.stringify(s);pending(d,b);
other.ppc.rows[0].amount='100';other.ppc.rows[0].subjectNuit='999999999';d.modelo_a_snapshot=JSON.stringify(s);pending(d,b);
other.ppc.rows[0].subjectNuit=b.subjectNuit;other.ppc.rows[0].year='2025';d.modelo_a_snapshot=JSON.stringify(s);pending(d,b);
});
test('positive PEC or specialcredit stays separated with null final balances,explicitzero permitted',()=>{
for(const key of['pec','credits']){const{d,b}=setup();b[key]={state:'known',amount:'10',reference:'QA saldo positivo exige regra própria'};const m=pending(d,b);assert.match(m.pending.join(' '),/fora deste recorte/);assert.equal(val(m.knownPpc),500);b[key].amount='0';assert.equal(model(d,b).status,'payable');b[key]={state:'absent',amount:'0',reference:'QA ausência contradiz montante'};pending(d,b);}
});
test('changing receipt date cannot evade identity checks;copied L5 allocation cannot consume larger proof twice',()=>{
const p=setup();p.b.ppc.rows.push({...p.b.ppc.rows[0],id:'other-date',date:'2026-07-01',amount:'100'});pending(p.d,p.b);
const{d,b}=setup();const l5=irpcBook(d);l5.payments.find(p=>p.id==='RF-L8').amount='1000';d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,l5,now));b.withholdings.rows=[];const fresh=reuseAnnualWithholding(d,b,'L8-received','RF-L8','rf');Object.assign(fresh.withholdings.rows[0],{amount:'200',incomeIncluded:'yes',reference:'QA próprio beneficiário e rendimento incluído'});
const other=structuredClone(fresh);other.withholdings.rows[0].amount='101';other.ppc.rows=[];other.ppc.state='absent';const s=JSON.parse(d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};s.irpcWithholdings['opportunity:other']=structuredClone(l5);d.modelo_a_snapshot=JSON.stringify(s);d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,irpcBook(d),now));fresh.withholdings.rows[0].sourceKey=annualWithholdingOptions(d).find(o=>o.rowId==='L8-received').key;other.withholdings.rows[0].sourceKey=fresh.withholdings.rows[0].sourceKey;const current=JSON.parse(d.modelo_a_snapshot);current.irpcAnnual['opportunity:other']=other;d.modelo_a_snapshot=JSON.stringify(current);assert.match(pending(d,fresh).pending.join(' '),/Mesma afectação L5/);
other.withholdings.rows[0].amount='100';d.modelo_a_snapshot=JSON.stringify(current);assert.equal(model(d,fresh).status,'payable');
});
test('annual accepted L7 accelerated assessment is reused once rather than summed with L6',()=>{
const d=read('irpc-l7-preserved.json').acceleration,b=reuseAnnualAssessment(d,newAnnualBook());b.reference='QA entidade/exercício';b.assessmentReference='QA colecta L7';for(const key of['withholdings','ppc','pec','credits']){b[key].state='absent';b[key].reference='QA ausência declarada';}
const m=model(d,b);assert.deepEqual(m.pending,[]);assert.equal(m.assessment.kind,'special');assert.equal(val(m.payable),1996.8);assert.notEqual(val(m.payable),val(normalBook(d).result.model.colecta)+1996.8);
});
test('100 minimum is applied symmetrically;99.99 kept exact without invented rounding or carry',()=>{
for(const[ppc,status,balance]of[['1308.01','below_minimum',99.99],['1308','payable',100],['1507.99','below_minimum',-99.99],['1508','recoverable',-100]]){const{d,b}=setup(null,ppc),m=model(d,b);assert.equal(m.status,status);assert.equal(val(m.arithmeticBalance),balance);if(status==='below_minimum'){assert.deepEqual(m.belowMinimum,{numerator:'9999',denominator:'100'});assert.equal(val(m.payable),0);assert.equal(val(m.recoverable),0);}}
const{d,b}=setup();b.ppc.rows[0].amount='0.001';pending(d,b);
});
test('existing L7 source cannot be bypassed by choosing L6 and positiveautonomous/shortperiod remain scoped',()=>{
for(const expense of[false,true]){const d=acceptedL7(expense);let b=reuseAnnualAssessment(d,newAnnualBook());b.reference='QA fecho';b.assessmentReference='QA colecta';for(const key of['withholdings','ppc','pec','credits']){b[key].state='absent';b[key].reference='QA sem movimentos';}assert.equal(b.assessment,'special');let m=pending(d,b);if(!expense)assert.match(m.pending.join(' '),/períodos especiais/);if(expense)assert.match(m.pending.join(' '),/Tributação autónoma/);b.assessment='normal';assert.match(pending(d,b).pending.join(' '),/não corrente/);}
});
test('record/recover/recalculate appends history unchanged,preserves all old books/plan,no double deduction',()=>{
const{d,b}=setup(),before=JSON.parse(d.modelo_a_snapshot),plan=d.business_plan_snapshot;d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,b,now));const once=annualBook(d),old=structuredClone(once.result);assert.equal(annualCurrent(d,once),true);d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,once,now));const back=annualBook(JSON.parse(JSON.stringify(d)));assert.deepEqual(back.history,[old]);assert.equal(val(back.result.model.payable),608);assert.equal(d.business_plan_snapshot,plan);const snapshot=JSON.parse(d.modelo_a_snapshot);delete snapshot.irpcAnnual;assert.deepEqual(snapshot,before);
b.ppc.rows[0].amount='400';assert.equal(annualCurrent(d,b),false);
});
test('changed L5 or assessment invalidates current L8 while retaining historical report and references',()=>{
const{d,b}=setup();d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,b,now));const back=annualBook(d),old=structuredClone(back.result);back.reference='NEW reference';d.modelo_a_snapshot=saveAnnualBook(d,back);assert.equal(annualCurrent(d,back),false);assert.match(annualReportRows(d).flat().join(' '),/Histórico/);assert.doesNotMatch(annualReportRows(d).flat().join(' '),/NEW reference/);assert.throws(()=>annualHtml(d),/fontes actuais/);assert.deepEqual(back.result,old);
const n=normalBook(d);n.base.identityReference+=' changed';d.modelo_a_snapshot=saveNormalBook(d,n);assert.equal(annualCurrent(d,back),false);pending(d,back);
const changed=setup();changed.d.modelo_a_snapshot=saveAnnualBook(changed.d,recordAnnual(changed.d,changed.b,now));const saved=annualBook(changed.d),prior=structuredClone(saved.result);const l5=irpcBook(changed.d);l5.payments.find(p=>p.id==='RF-L8').reference+=' corrected';changed.d.modelo_a_snapshot=saveIrpcBook(changed.d,recordIrpc(changed.d,l5,now));assert.equal(annualCurrent(changed.d,saved),false);pending(changed.d,saved);assert.deepEqual(saved.result,prior);
});
test('malformed optionalbook never resets accepted books;report explicitly preserves missing states',()=>{
const{d,b}=setup();b.credits.state='';d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,b,now));assert.match(annualHtml(d),/Indeterminado/);assert.match(annualHtml(d),/Informação em falta/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/Saldo anual simulado/);
const old=d.modelo_a_snapshot;b.ppc.rows.push({...b.ppc.rows[0]});assert.throws(()=>saveAnnualBook(d,b),/identificador repetido/);assert.equal(d.modelo_a_snapshot,old);
});
test('prepare only L8 delta cases for guarded API and report recovery',()=>{
const cases={payable:setup(),recoverable:setup('1000','600'),balanced:setup('408','1000'),absent:setup(null,null),missing:setup(),duplicate:setup(),pec:setup()};cases.missing.b.ppc.rows[0].effective='';cases.duplicate.b.ppc.rows.push({...cases.duplicate.b.ppc.rows[0],id:'duplicate'});cases.pec.b.pec={state:'known',amount:'10',reference:'QA PEC separado'};
fs.mkdirSync('outputs/irpc-l8-settlement',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,b,now));fs.writeFileSync('outputs/irpc-l8-settlement/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l8-settlement/'+name+'.html',annualHtml(d));}
const{d}=setup();fs.writeFileSync('outputs/irpc-l8-settlement/initial.json',JSON.stringify(d));
});
+124
View File
@@ -0,0 +1,124 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {carryCase} from './helpers/irpc-carry-case.mjs';
import {internationalCase} from './helpers/irpc-international-case.mjs';
import {newCarryUse, calculateCarryLot} from '../../resources/js/lib/irpc-credit-carry.ts';
import {calculateAnnual, prepareCreditCarry, recordAnnual, annualBook, annualCurrent, saveAnnualBook, annualHtml, annualReportRows} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const number = v => v ? Number(v.numerator) / Number(v.denominator) : null;
const calc = c => calculateAnnual(c.d, c.b);
const lot = (c, i = 0) => c.b.creditCarry.lots[i];
const pending = c => {const m = calc(c); assert.equal(m.status, 'indeterminate', JSON.stringify(m.pending)); assert.equal(m.arithmeticBalance, null); assert.equal(m.internationalCredit.deducted, null); assert.equal(m.pecDeduction.deducted, null); return m;};
test('2021 international and2023 PEC remain eligible in2026; origin deductions/uses count once:1408-8-50-200-100-500=550', () => {
const m = calc(carryCase()); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 550);
assert.equal(number(m.creditCarry.lots[0].opening), 100); assert.equal(number(m.creditCarry.lots[0].used), 30);
assert.equal(number(m.creditCarry.lots[0].available), 70); assert.equal(number(m.creditCarry.lots[0].remaining), 20);
assert.equal(m.creditCarry.lots[0].lastYear, 2026); assert.equal(m.creditCarry.lots[1].lastYear, 2026);
assert.equal(number(m.creditCarry.lots[1].remaining), 50); assert.equal(m.creditCarry.carryCreated, false); assert.equal(m.creditCarry.officialBalancesConsumed, false);
});
test('five subsequent exercises differs from PEC three; expired source retained with zero selection', () => {
for (const [i, year] of [[0, '2020'], [1, '2022']]) {
const c = carryCase(); lot(c, i).year = year;
if (i === 0) lot(c).uses[0].year = '2023';
assert.match(pending(c).pending.join(' '), /Prazo ordinário terminado/);
lot(c, i).selected = '0'; const m = calc(c);
assert.deepEqual(m.pending, []); assert.equal(m.creditCarry.lots[i].expired, true);
assert.equal(number(m.creditCarry.lots[i].available), 0); assert.ok(number(m.creditCarry.lots[i].remaining) > 0);
}
});
test('future/current origin, nonordinary periods, currency and wrong subject do not create an eligible carry', () => {
for (const patch of [{year: '2026'}, {year: '2027'}, {year: ''}, {ordinary: ''}, {ordinary: 'no'}, {currency: 'USD'}, {subjectNuit: '999999999'}, {country: 'MZ'}, {country: 'ZZ'}, {kind: ''}]) {
const c = carryCase(); Object.assign(lot(c), patch); pending(c);
}
const c = carryCase(); lot(c, 1).country = 'ZA'; pending(c);
});
test('origin eligibility, insufficient collecta and applicable rule require concrete evidence', () => {
for (const patch of [{originDocument: ''}, {originReference: ''}, {ruleReference: ''}, {insufficient: ''}, {insufficient: 'no'}, {eligible: ''}, {originDeducted: ''}, {originDeducted: '201'}, {selected: ''}, {selected: '70.01'}]) {
const c = carryCase(); Object.assign(lot(c), patch); pending(c);
}
});
test('unknown uses are not zero; every actual use must fit its origin/window and documentary identity', () => {
for (const patch of [{usesState: ''}, {usesReference: ''}, {usesState: 'absent'}, {uses: []}, {uses: [{...newCarryUse('u'), year: '2021', amount: '1', document: 'D', reference: 'R'}]}]) {
const c = carryCase(); Object.assign(lot(c), patch); pending(c);
}
for (const patch of [{year: '2027'}, {amount: '101'}, {document: ''}, {reference: ''}, {amount: ''}]) {
const c = carryCase(); Object.assign(lot(c).uses[0], patch); pending(c);
}
const c = carryCase(); lot(c).uses.push({...lot(c).uses[0], id: 'copy', year: '2024', document: ' synthetic USE2023 '});
assert.match(pending(c).pending.join(' '), /repetido/);
});
test('actual current-year use is subtracted before the proposed new deduction', () => {
const c = carryCase(); lot(c).uses.push({id: 'current-actual', year: '2026', amount: '10', document: 'SYNTHETIC current actual use', reference: 'SYNTHETIC actual assessment, not this simulation'});
const m = calc(c); assert.equal(number(m.creditCarry.lots[0].available), 60); assert.equal(number(m.creditCarry.lots[0].remaining), 10); assert.equal(number(m.payable), 550);
});
test('original country/nature/year cannot be duplicated by changing document or id', () => {
const c = carryCase(); c.b.creditCarry.lots.push({...structuredClone(lot(c)), id: 'other', originDocument: 'OTHER', uses: [] , usesState: 'absent'});
assert.match(pending(c).pending.join(' '), /mesma natureza/);
});
test('cross-context duplicate origin blocks and external edits invalidate saved current result', () => {
const c = carryCase(); c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b));
const b = annualBook(c.d); assert.equal(annualCurrent(c.d, b), true);
const s = JSON.parse(c.d.modelo_a_snapshot), other = structuredClone(c.b);
other.creditCarry.lots = [{...structuredClone(lot(c)), id: 'copied-origin', originDocument: 'OTHER', uses: [], usesState: 'absent'}];
s.irpcAnnual['opportunity:other'] = other; c.d.modelo_a_snapshot = JSON.stringify(s);
assert.equal(annualCurrent(c.d, b), false); assert.match(pending(c).pending.join(' '), /noutro mapa/);
});
test('explicit allocation can favour older balances without automatic reduction or FIFO', () => {
const c = carryCase(); Object.assign(c.b.creditCarry, {currentInternational: '0', currentPec: '100'});
const m = calc(c); assert.equal(number(m.payable), 658); assert.equal(number(m.internationalCredit.deducted), 0);
assert.equal(number(m.internationalCredit.notSelected), 8); assert.equal(m.internationalCredit.insufficientCollecta, null);
assert.equal(number(m.pecDeduction.deducted), 100); assert.equal(number(m.pecDeduction.remaining), 100);
});
test('current selection cannot exceed known availability or total collecta; planned choices are kept on failure', () => {
for (const patch of [{currentInternational: '9'}, {currentPec: '201'}, {currentInternational: ''}, {currentPec: ''}, {allocationReference: ''}, {assessmentKey: 'stale'}]) {
const c = carryCase(); Object.assign(c.b.creditCarry, patch); pending(c);
}
const c = carryCase(); lot(c).eligible = '5000'; lot(c).selected = '1400';
const before = structuredClone(c.b); assert.match(pending(c).pending.join(' '), /ultrapassam a colecta/); assert.deepEqual(c.b, before);
});
test('no credit or PEC reimbursement is generated by old balances; PPC refund stays separate', () => {
const c = carryCase(); Object.assign(c.b.creditCarry, {currentInternational: '8', currentPec: '0'}); lot(c).eligible = '2000'; lot(c).selected = '1300'; lot(c, 1).selected = '100';
const m = calc(c); assert.equal(number(m.recoverable), 500); assert.equal(number(m.pecDeduction.deducted), 0);
assert.equal(m.creditCarry.carryCreated, false); assert.equal(number(m.creditCarry.lots[0].remaining), 570);
});
test('exact foreign-credit fractions are preserved; decimal presentation is never fiscal rounding', () => {
const c = carryCase(); Object.assign(lot(c), {eligible: '2/3', originDeducted: '1/3', selected: '1/3', usesState: 'absent', uses: []});
Object.assign(c.b.creditCarry, {currentInternational: '1/3'});
const m = calc(c); assert.deepEqual(m.pending, []); assert.deepEqual(m.creditCarry.lots[0].available, {numerator: '1', denominator: '3'});
assert.deepEqual(m.payable, {numerator: '1822', denominator: '3'});
lot(c).selected = '1/0'; pending(c);
});
test('missing/absent/known-zero remain distinct; absence does not demand unused allocation fields', () => {
const c = internationalCase(), baseline = calc(c); c.b = prepareCreditCarry(c.b); pending(c);
c.b.creditCarry.state = 'absent'; c.b.creditCarry.reference = 'SYNTHETIC QA documented absence';
let m = calc(c); assert.equal(number(m.payable), number(baseline.payable)); assert.deepEqual(m.internationalCredit, baseline.internationalCredit);
c.b.creditCarry.lots.push({...carryCase().b.creditCarry.lots[0]}); pending(c);
const z = carryCase(); Object.assign(lot(z), {eligible: '0', originDeducted: '0', selected: '0', usesState: 'absent', uses: []});
m = calc(z); assert.deepEqual(m.pending, []); assert.equal(number(m.creditCarry.lots[0].available), 0);
});
test('optional preparation, recording, JSON recovery, HTML and dossier preserve accepted books and frozen history', () => {
const c = internationalCase(); c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b, '2026-10-27T17:00:00+02:00'));
const old = annualBook(c.d), frozen = structuredClone(old.result), existing = JSON.parse(c.d.modelo_a_snapshot), plan = c.d.business_plan_snapshot;
const next = prepareCreditCarry(old); next.creditCarry = carryCase().b.creditCarry;
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, next, '2026-10-28T12:00:00+02:00'));
const recovered = annualBook(JSON.parse(JSON.stringify(c.d))); assert.deepEqual(recovered.history, [frozen]); assert.equal(number(recovered.result.model.payable), 550);
assert.equal(annualCurrent(c.d, recovered), true); assert.equal(c.d.business_plan_snapshot, plan);
const after = JSON.parse(c.d.modelo_a_snapshot); delete after.irpcAnnual; delete existing.irpcAnnual; assert.deepEqual(after, existing);
assert.match(annualHtml(c.d), /Saldo anterior — Crédito internacional ZA — 2021/);
assert.match(annualHtml(c.d), /550.00/); assert.match(annualHtml(c.d), /2026/);
assert.match(JSON.stringify(createProjectDossierReport(c.d, {selectedSections: new Set(['finance'])})), /Afectação escolhida entre exercícios/);
recovered.creditCarry.lots[0].selected = '49'; c.d.modelo_a_snapshot = saveAnnualBook(c.d, recovered);
assert.equal(annualCurrent(c.d, recovered), false); assert.deepEqual(recovered.result.model, annualBook(c.d).result.model);
assert.throws(() => annualHtml(c.d), /fontes actuais/); assert.match(annualReportRows(c.d).flat().join(' '), /Histórico/);
});
test('malformed detail and duplicate internal ids cannot overwrite original snapshots', () => {
for (const mutate of [c => c.b.creditCarry.lots[0].uses = null, c => c.b.creditCarry.version = 2, c => c.b.creditCarry.lots[0].uses[0].amount = null, c => c.b.creditCarry.lots[1].id = c.b.creditCarry.lots[0].id]) {
const c = carryCase(), before = c.d.modelo_a_snapshot; mutate(c);
assert.throws(() => saveAnnualBook(c.d, c.b), /recuper|repetido/); assert.equal(c.d.modelo_a_snapshot, before);
}
});
test('standalone expired PEC keeps origin and is not silently converted to international five-year window', () => {
const c = carryCase(), r = {...lot(c, 1), year: '2022', selected: '0'};
const m = calculateCarryLot(r, c.b.subjectNuit, c.b.year); assert.equal(m.expired, true); assert.equal(m.lastYear, 2025); assert.equal(number(m.remaining), 150);
});
@@ -0,0 +1,141 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {economicCase} from './helpers/irpc-economic-case.mjs';
import {internationalCase} from './helpers/irpc-international-case.mjs';
import {calculateAnnual, prepareEconomicBook, annualBook, recordAnnual, saveAnnualBook, annualCurrent, annualHtml, annualReportRows} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
import {irpcBook, recordIrpc, saveIrpcBook} from '../../resources/js/lib/irpc-withholdings.ts';
const number = v => v ? Number(v.numerator) / Number(v.denominator) : null;
const row = c => c.b.economicCredits.rows[0];
const foreign = c => c.b.internationalCredits.positions[0].payments[0];
const calc = c => calculateAnnual(c.d, c.b);
const pending = c => {const m = calc(c); assert.equal(m.status, 'indeterminate', JSON.stringify(m.pending)); assert.equal(m.arithmeticBalance, null); assert.equal(m.economicCredit.deducted, null); assert.equal(m.internationalCredit.deducted, null); assert.equal(m.pecDeduction.deducted, null); return m;};
test('economic60% of documented sourceIRPC20 yields12 before international8,PEC200 andPPC500:688', () => {
const m = calc(economicCase()); assert.deepEqual(m.pending, []); assert.equal(number(m.economicCredit.rows[0].nominal), 12);
assert.equal(number(m.economicCredit.deducted), 12); assert.equal(number(m.economicCredit.afterCredit), 1396);
assert.equal(number(m.internationalCredit.deducted), 8); assert.equal(number(m.internationalCredit.afterCredit), 1388); assert.equal(number(m.payable), 688);
assert.equal(m.economicCredit.carryCreated, false); assert.equal(m.economicCredit.officialBalancesConsumed, false); assert.equal(m.submitted, false);
});
test('documented proportional ceiling and prior actual use limit economic deduction,not a second tax rate on dividends', () => {
const c = economicCase(); row(c).nationalLimit = '5'; let m = calc(c); assert.equal(number(m.economicCredit.rows[0].nominal), 12); assert.equal(number(m.payable), 695);
row(c).previousUse.amount = '3'; m = calc(c); assert.equal(number(m.economicCredit.deducted), 2); assert.equal(number(m.payable), 698);
row(c).previousUse.amount = '6'; pending(c);
});
test('article40 exclusion, payer exemption/foreign residence and special distributions do not become international credit', () => {
for (const patch of [{article40: ''}, {article40: 'yes'}, {article40Reference: ''}, {payerResident: ''}, {payerResident: 'no'}, {recipientResident: 'no'}, {payerIrpc: 'no'}, {payerExempt: ''}, {payerExempt: 'yes'}, {kind: 'liquidation'}, {kind: 'association'}, {kind: ''}, {qualificationReference: ''}]) {
const c = economicCase(); Object.assign(row(c), patch); const m = pending(c); assert.equal(m.internationalCredit.positions.length, 1); assert.equal(number(m.internationalCredit.positions[0].eligible), 8);
if (patch.article40 === 'yes' || patch.kind === 'liquidation' || patch.kind === 'association') assert.equal(m.economicCredit.rows[0].status, 'outside_scope');
}
});
test('inclusion,credit gross-up and current assessment must be reconciled without silently changing L6', () => {
for (const patch of [{incomeIncluded: ''}, {incomeIncluded: 'no'}, {incomeReference: ''}, {assessmentKey: ''}, {assessmentKey: 'stale'}, {grossUpIncluded: ''}, {grossUpIncluded: '0'}, {grossUpIncluded: '11.99'}, {grossUpReference: ''}, {nationalLimit: ''}, {nationalLimit: '1409'}, {nationalLimitReference: ''}, {income: '0'}]) {
const c = economicCase(), before = c.d.modelo_a_snapshot; Object.assign(row(c), patch); pending(c); assert.equal(c.d.modelo_a_snapshot, before);
}
});
test('dividend/source tax identity,date,year,subject and actual allocated source amount require support', () => {
for (const patch of [{subjectNuit: '999999999'}, {payerNuit: ''}, {year: '2025'}, {distributionDate: '2026-02-30'}, {distributionDate: '2025-12-31'}, {distributionDocument: ''}, {distributionParcel: ''}, {taxIssuer: ''}, {taxDocument: ''}, {taxParcel: ''}, {parcelReference: ''}, {currency: 'USD'}, {proofAmount: ''}, {taxAmount: '21'}, {taxAmount: ''}, {attributionReference: ''}, {previouslyAttributed: ''}, {previouslyAttributed: '1'}, {priorAttributionReference: ''}]) {
const c = economicCase(); Object.assign(row(c), patch); pending(c);
}
});
test('same distributed profit parcel cannot be re-used by changing source-tax document,id or date', () => {
const c = economicCase(); c.b.economicCredits.rows.push({...structuredClone(row(c)), id: 'other', taxDocument: 'OTHER', distributionDate: '2026-10-01', distributionDocument: ' synthetic-dividend2026 ', distributionParcel: ' DISTRIBUTION-1 '});
assert.match(pending(c).pending.join(' '), /Mesma parcela de lucros/);
});
test('one source-tax parcel can support distinct documented distributions only within its actual available amount', () => {
const c = economicCase(), other = {...structuredClone(row(c)), id: 'second', distributionParcel: 'distribution-2'};
c.b.economicCredits.rows.push(other); assert.match(pending(c).pending.join(' '), /disponível/);
for (const r of c.b.economicCredits.rows) {r.taxAmount = '10'; r.grossUpIncluded = '6';}
let m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.economicCredit.deducted), 12);
row(c).previouslyAttributed = '1'; other.previouslyAttributed = '1'; pending(c);
row(c).taxAmount = '9'; other.taxAmount = '9'; row(c).grossUpIncluded = '5.4'; other.grossUpIncluded = '5.4'; m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.economicCredit.deducted), 10.8);
other.previouslyAttributed = '0'; assert.match(pending(c).pending.join(' '), /divergente/);
});
test('economic and international use of the same actual tax parcel is rejected even with changed receipt date', () => {
const c = economicCase(); Object.assign(foreign(c), {issuer: row(c).taxIssuer, document: row(c).taxDocument, parcel: ' UNDERLYING-TAX-1 ', parcelReference: 'SYNTHETIC same parcel', proofAmount: '20', date: '2026-10-01'});
assert.match(pending(c).pending.join(' '), /natureza|naturezas/);
});
test('an old proof with no parcel overlaps the whole document;new names require supported partition on both sides', () => {
const c = economicCase(); Object.assign(foreign(c), {issuer: row(c).taxIssuer, document: row(c).taxDocument, proofAmount: '20'}); pending(c);
foreign(c).parcel = 'foreign-tax-line'; pending(c); foreign(c).parcelReference = 'SYNTHETIC evidence of a distinct foreign-tax line in shared document';
const m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 688);
});
test('economic source IRPC is not beneficiary withholding,PPC or PEC;distinct supported parcels may share a document', () => {
for (const kind of ['ppc', 'pec']) {
const c = economicCase(), proof = kind === 'ppc' ? c.b.ppc.rows[0] : c.b.pecPayments.rows[0];
Object.assign(row(c), {taxIssuer: proof.issuer, taxDocument: proof.document, proofAmount: proof.proofAmount}); pending(c);
Object.assign(proof, {parcel: 'own-' + kind, parcelReference: 'SYNTHETIC distinct payment parcel'}); assert.deepEqual(calc(c).pending, []);
proof.parcel = row(c).taxParcel; pending(c);
}
});
test('underlying domestic IRPC and beneficiary withholding remain separate even when supported by the same paper', () => {
const c = internationalCase('10', '8', '200', '500', '300'); c.b = prepareEconomicBook(c.b); c.b.economicCredits = economicCase().b.economicCredits;
const withholding = c.b.withholdings.rows[0], proof = calc(c).claims.find(x => x.kind === 'withholding');
Object.assign(row(c), {taxIssuer: proof.issuer, taxDocument: proof.document, proofAmount: '300'}); pending(c);
Object.assign(withholding, {parcel: 'beneficiary-withholding', parcelReference: 'SYNTHETIC separate recipient-tax parcel'});
assert.equal(number(calc(c).payable), 388);
const other = structuredClone(c.b), s = JSON.parse(c.d.modelo_a_snapshot);
other.economicCredits.rows = []; other.economicCredits.state = 'absent'; other.internationalCredits.positions = []; other.internationalCredits.state = 'absent';
other.ppc.rows = []; other.ppc.state = 'absent'; other.pecPayments.rows = []; other.pecPayments.state = 'absent';
other.withholdings.rows[0].parcel = 'relabelled-same-L5-allocation';
s.irpcAnnual = {'opportunity:other': other}; s.irpcWithholdings['opportunity:other'] = structuredClone(Object.values(s.irpcWithholdings)[0]); c.d.modelo_a_snapshot = JSON.stringify(s);
c.d.modelo_a_snapshot = saveIrpcBook(c.d, recordIrpc(c.d, irpcBook(c.d), '2026-10-28T14:00:00+02:00'));
assert.match(pending(c).pending.join(' '), /calculada e reconciliada emL5|afectação L5/);
});
test('documented economic inclusion is not rejected merely because fiscal losses reduce total taxable base below dividends', () => {
const c = economicCase(); row(c).income = '5000';
const m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 688);
});
test('cross-context source capacity and distributed-income duplication cannot be evaded with new ids/documents', () => {
const c = economicCase(), other = structuredClone(c.b), s = JSON.parse(c.d.modelo_a_snapshot);
other.internationalCredits.positions = []; other.internationalCredits.state = 'absent'; other.ppc.rows = []; other.ppc.state = 'absent'; other.pecPayments.rows = []; other.pecPayments.state = 'absent';
other.economicCredits.rows[0].id = 'other-id'; other.economicCredits.rows[0].taxDocument = 'OTHER';
s.irpcAnnual = {'opportunity:other': other}; c.d.modelo_a_snapshot = JSON.stringify(s); assert.match(pending(c).pending.join(' '), /lucros económicos já/);
other.economicCredits.rows[0].distributionParcel = 'other-distribution'; other.economicCredits.rows[0].taxDocument = row(c).taxDocument; c.d.modelo_a_snapshot = JSON.stringify(s); pending(c);
other.economicCredits.rows[0].taxAmount = '10'; row(c).taxAmount = '10'; row(c).grossUpIncluded = '6'; c.d.modelo_a_snapshot = JSON.stringify(s);
assert.deepEqual(calc(c).pending, []);
});
test('economic and accepted carry increments compose:1408-12-8-50-200-100-500=538', () => {
const m = calc(economicCase(true)); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 538);
assert.equal(number(m.creditCarry.afterInternational), 1338); assert.equal(number(m.creditCarry.afterPec), 1038);
});
test('economic priority leaves prior explicit allocations intact when remaining collecta becomes insufficient', () => {
const c = economicCase(true); c.b.creditCarry.lots[0].eligible = '2000'; c.b.creditCarry.lots[0].selected = '1300'; c.b.creditCarry.currentPec = '0';
const before = structuredClone(c.b.creditCarry); assert.match(pending(c).pending.join(' '), /ultrapassam a colecta/); assert.deepEqual(c.b.creditCarry, before);
c.b.creditCarry.lots[0].selected = '1288'; const m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.recoverable), 500); assert.equal(m.economicCredit.carryCreated, false);
});
test('zero,absence and missing differ; earlier deliberate economic summary remains frozen and does not block detailed calculation', () => {
const c = economicCase(); c.b.internationalCredits.economic = {state: 'known', amount: '999', reference: 'SYNTHETIC previous deliberate summary'};
assert.equal(number(calc(c).payable), 688); assert.equal(c.b.internationalCredits.economic.amount, '999');
c.b.economicCredits.state = 'absent'; pending(c); c.b.economicCredits.rows = []; assert.equal(number(calc(c).payable), 700);
c.b.economicCredits.state = ''; pending(c);
const z = economicCase(); Object.assign(row(z), {taxAmount: '0', proofAmount: '0', grossUpIncluded: '0', income: '0', nationalLimit: '0'}); assert.equal(number(calc(z).payable), 700);
});
test('exact60% arithmetic and fractional quota never invent fiscal rounding', () => {
const c = economicCase(); Object.assign(row(c), {taxAmount: '0.01', proofAmount: '0.01', grossUpIncluded: '0.006'});
const m = calc(c); assert.deepEqual(m.economicCredit.deducted, {numerator: '3', denominator: '500'}); assert.deepEqual(m.payable, {numerator: '349997', denominator: '500'});
row(c).nationalLimit = '1/1000'; assert.deepEqual(calc(c).economicCredit.deducted, {numerator: '1', denominator: '1000'});
});
test('save/recover/history,HTML and dossier preserve current L6,original snapshots and accepted carry results', () => {
const c = economicCase(true), prior = structuredClone(c.b); delete prior.economicCredits;
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, prior, '2026-10-28T12:00:00+02:00'));
const old = annualBook(c.d), frozen = structuredClone(old.result), base = JSON.parse(c.d.modelo_a_snapshot), plan = c.d.business_plan_snapshot;
const next = prepareEconomicBook(old); next.economicCredits = c.b.economicCredits;
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, next, '2026-10-28T14:00:00+02:00'));
const back = annualBook(JSON.parse(JSON.stringify(c.d))); assert.equal(annualCurrent(c.d, back), true); assert.deepEqual(back.history, [frozen]); assert.equal(number(back.result.model.payable), 538);
const after = JSON.parse(c.d.modelo_a_snapshot); delete after.irpcAnnual; delete base.irpcAnnual; assert.deepEqual(after, base); assert.equal(c.d.business_plan_snapshot, plan);
assert.match(annualHtml(c.d), /Crédito económico — antes do internacional/); assert.match(annualHtml(c.d), /538.00/);
assert.match(JSON.stringify(createProjectDossierReport(c.d, {selectedSections: new Set(['finance'])})), /Crédito económico/);
back.economicCredits.rows[0].nationalLimit = '5'; c.d.modelo_a_snapshot = saveAnnualBook(c.d, back); assert.equal(annualCurrent(c.d, back), false); assert.deepEqual(back.result, annualBook(c.d).result); assert.throws(() => annualHtml(c.d), /fontes actuais/); assert.match(annualReportRows(c.d).flat().join(' '), /Histórico/);
});
test('malformed optional evidence and parcels are rejected without resetting snapshots', () => {
for (const mutate of [c => c.b.economicCredits.version = 2, c => row(c).previousUse = null, c => row(c).article40 = null, c => foreign(c).parcel = 1]) {
const c = economicCase(), before = c.d.modelo_a_snapshot; mutate(c); assert.throws(() => saveAnnualBook(c.d, c.b), /recuperável/); assert.equal(c.d.modelo_a_snapshot, before);
}
const c = internationalCase(); const once = prepareEconomicBook(c.b); assert.equal(prepareEconomicBook(once), once);
});
test('a partially identified document parcel remains recoverable and indeterminate rather than acquiring absent fields', () => {
const c = economicCase(); foreign(c).parcel = 'foreign-tax-line';
const model = pending(c); c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b));
const back = annualBook(JSON.parse(JSON.stringify(c.d))); assert.deepEqual(calculateAnnual(c.d, back), back.result.model); assert.equal(back.result.model.status, model.status);
});
+40
View File
@@ -0,0 +1,40 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {irpcBook,newIrpcRow,recordIrpc,saveIrpcBook,emptyIrpcOrdinaryFacts,irpcCurrent,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
import {convertIrpcAmount,irpcFxSources,updateIrpcFx} from '../../resources/js/lib/irpc-fx.ts';
import {emptyFxSelection,selectFx} from '../../resources/js/lib/iva-fx-selection.ts';
import {currencyEventBook,newCurrencyEvent,saveCurrencyEventBook} from '../../resources/js/lib/iva-currency-events.ts';
const accepted=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l5-preserved.json',import.meta.url),'utf8'));
const draft=()=>structuredClone(accepted);
const row=(id='normal')=>({...newIrpcRow(id),counterparty:'SYNTHETIC IRPC counterparty',documentRef:'SYNTHETIC-'+id,base:'100000',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'SYNTHETIC classified non-digital income,IRPCpayer/recipient,Mozambique source;residence andEP explicitly recorded',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'SYNTHETIC applicable cost'},{kind:'payment',date:'2026-11-02',reference:'SYNTHETIC payment'}],settlement:'none',settlementRef:'SYNTHETIC no remittance',ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'SYNTHETIC exact category facts;CIRPC62/67'}});
const book=(r=row())=>({version:1,rows:[r],payments:[]});
const calc=r=>recordIrpc(draft(),book(r)).result.rows[0];
const fx=(patch={})=>{const s={...emptyFxSelection(),nature:'commercial',submissionDate:'2026-10-20',bulletinDate:'2026-10-20',bulletinRef:'SYNTHETIC fictional BM bulletin for QA',source:'SYNTHETIC archived QA exemplar',issuer:'Banco de Moçambique',sourceCurrency:'USD',direction:'mzn_per_unit',mean:'64',...patch};return{...s,applied:selectFx(s)};};
test('ordinary nonresident royalties equipment services and nonlisted capitals use20%,not digital10%',()=>{for(const kind of ['royalty','equipment','services','capital']){const r=row();r.kind=kind;if(kind==='capital')r.ordinary.capitalType='unlisted_interest';const v=calc(r);assert.equal(v.status,'calculated');assert.equal(v.rate,20);assert.equal(v.tax.numerator,'20000');assert.equal(v.nature,'definitive');}});
test('resident royalties and ordinary rent are on account;predial nonresident is also on account',()=>{for(const patch of [{kind:'royalty',residence:'resident'},{kind:'rent',residence:'resident'},{kind:'rent',residence:'nonresident'}]){const r={...row(),...patch};r.ordinary.rentOwnPropertyManagement='no';const v=calc(r);assert.equal(v.status,'calculated');assert.equal(v.nature,'on_account');assert.equal(v.tax.numerator,'20000');assert.equal('annualCredit'in v,false);}});
test('identified dispensa branches and unknown facts block only their own income line',()=>{const d=draft(),rs=[{...row('resident-services'),residence:'resident',kind:'services'},{...row('property'),kind:'rent'},{...row('capital'),residence:'resident',kind:'capital',ordinary:{...row().ordinary,capitalType:'unlisted_interest'}},row('independent')];const out=recordIrpc(d,{version:1,rows:rs,payments:[]}).result.rows;assert.ok(out.slice(0,3).every(v=>v.status==='indeterminate'&&v.tax===null));assert.equal(out[3].tax.numerator,'20000');});
test('special categories62(4)a-c retain10% with exact facts,not generic20% or genericinstallation10%',()=>{for(const kind of ['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage']){const r={...row(),kind};r.ordinary.specialFacts='yes';const v=calc(r);assert.equal(v.rate,10);assert.equal(v.tax.numerator,'10000');assert.match(v.reason,/62\(4\)/);}const unknown={...row(),kind:'connected_installation'};assert.equal(calc(unknown).tax,null);const generic={...row(),kind:'services'};assert.equal(calc(generic).rate,20);});
test('special62(4) entity condition is distinct from digital noEP/nonattributable branch',()=>{for(const patch of [{residence:'resident'},{permanentEstablishment:'yes',attributable:'no'}]){const r={...row(),kind:'telecom',...patch};r.ordinary.specialFacts='yes';assert.equal(calc(r).tax,null);}});
test('listed debt and other BMsecurities remain distinct;liquidity repos are not assumed',()=>{for(const[capitalType,rate]of [['listed_debt_mz',20],['listed_other_mz',10]]){const r={...row(),kind:'capital'};r.ordinary.capitalType=capitalType;r.ordinary.specialFacts='yes';assert.equal(calc(r).rate,rate);}const resident={...row(),kind:'capital',residence:'resident'};resident.ordinary.capitalType='listed_debt_mz';resident.ordinary.capitalSeatAndManagement='yes';assert.equal(calc(resident).rate,20);assert.equal(calc(resident).nature,'definitive');resident.ordinary.capitalType='liquidity_repo';assert.equal(calc(resident).rate,20);assert.equal(calc(resident).nature,'definitive');});
test('expired2025 agricultural measures do not extend into2026 foreign services/interest',()=>{for(const kind of ['services','capital']){const r={...row(),kind,description:'SYNTHETIC nationalagriculturalcompany / external agricultural financing'};r.ordinary.capitalType='unlisted_interest';const v=calc(r);assert.equal(v.rate,20);assert.match(v.reason,/31\/12\/2025/);}const r=row();r.events[0].date='2025-12-31';assert.equal(calc(r).tax,null);});
test('applied existing BMmean converts base, preserves original and keeps event/date separate',()=>{const r={...row(),currency:'USD',base:'1000',fxSelection:fx()};const v=calc(r);assert.equal(v.base.numerator,'64000');assert.equal(v.tax.numerator,'12800');assert.equal(v.period,'2026-10');assert.equal(v.eventDate,'2026-10-02');assert.equal(v.conversion.date,'2026-10-20');assert.equal(v.conversion.originalAmount,'1000');assert.equal(v.conversion.originalCurrency,'USD');assert.equal(r.base,'1000');assert.equal(r.currency,'USD');});
test('purchase/sale perspective,inverse quote and fiscalexception use deliveredselector',()=>{const opposite=fx({nature:'purchase',relation:'opposite',bulletinPerspective:'SYNTHETIC issuer',buy:'63',sell:'65'});let r={...row(),currency:'USD',base:'100',fxSelection:opposite};assert.equal(calc(r).base.numerator,'6500');const inverse=fx({direction:'units_per_mzn',mean:'0.02'});r.fxSelection=inverse;assert.equal(calc(r).base.numerator,'5000');r.fxSelection=fx({mode:'exception',exceptionRef:'SYNTHETIC identifiedfiscalprovision',exceptionScope:'SYNTHETIC retention scope',exceptionDate:'2026-10-02',exceptionRate:'66'});assert.equal(calc(r).base.numerator,'6600');assert.equal(calc(r).conversion.column,'exception');});
test('unapplied,stale,missingmean,wrongcurrency or undated FX remains indeterminate',()=>{const samples=[undefined,{...fx(),applied:undefined},{...fx(),mean:'65'},{...fx(),sourceCurrency:'EUR'},{...fx(),bulletinDate:'2026-10-19'}];for(const f of samples){const v=calc({...row(),currency:'USD',base:'1000',fxSelection:f});assert.equal(v.tax,null);assert.match(v.reason,/conversão/);}});
test('existing saved FX selections are linked,reused,and changed source marks only linked results stale',()=>{const d=draft(),cb=currencyEventBook(d),s=fx(),f={...newCurrencyEvent('shared-fx','MZN'),fxSelection:s,sourceCurrency:'USD',fxDirection:s.direction,fxRate:s.applied.rate,fxDate:s.applied.date,fxSource:s.applied.source,description:'SYNTHETIC existing FX'};d.modelo_a_snapshot=saveCurrencyEventBook(d,{...cb,rows:[...cb.rows,f]});const source=irpcFxSources(d).find(s=>s.id===f.id),r={...row(),currency:'USD',base:'1000',fxSelection:source.selection,fxLink:{id:source.id,fingerprint:source.fingerprint}},b=recordIrpc(d,book(r));d.modelo_a_snapshot=saveIrpcBook(d,b);assert.equal(irpcCurrent(d,b),true);const current=currencyEventBook(d);d.modelo_a_snapshot=saveCurrencyEventBook(d,{...current,rows:current.rows.map(v=>v.id===f.id?{...v,description:'SYNTHETIC revisedsource'}:v)});assert.equal(irpcCurrent(d,b),false);assert.match(recordIrpc(d,b).result.rows[0].reason,/origem alterada/);const m=recordIrpc(d,book(row('independentMZN')));assert.equal(m.result.rows[0].tax.numerator,'20000');});
test('foreign withholding proof converts available amount once;allocations remainMZN',()=>{const r={...row(),direction:'received',settlement:'complete',settlementRef:'SYNTHETIC provedpartialwithholding',allocations:[{paymentId:'p',amount:'640'}]},p={id:'p',kind:'withholding_proof',reference:'SYNTHETIC-USDproof',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()};const d=draft(),b=recordIrpc(d,{...book(r),payments:[p]});assert.equal(b.result.rows[0].settled.numerator,'640');assert.equal(b.result.rows[0].outstanding.numerator,'19360');assert.equal(b.result.paymentConversions.p.originalAmount,'10');d.modelo_a_snapshot=saveIrpcBook(d,b);assert.match(irpcReportRows(d)[0][3],/SYNTHETIC-USDproof/);assert.match(irpcReportRows(d)[0][3],/Original: 10 USD/);});
test('foreign payment to beneficiary or purported ATremittance is never converted into taxproof',()=>{for(const kind of ['beneficiary','remittance']){const r={...row(),settlement:'complete',allocations:[{paymentId:'p',amount:'640'}]},p={id:'p',kind,reference:'SYNTHETIC-wrongpayment',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()};assert.equal(recordIrpc(draft(),{...book(r),payments:[p]}).result.rows[0].status,'indeterminate');}});
test('converted shared proof cannot exceed availableMZN or change FX between contexts',()=>{const d=draft(),p={id:'p',kind:'withholding_proof',reference:'SYNTHETIC-sharedUSD',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()},r={...row('one'),direction:'received',settlement:'complete',allocations:[{paymentId:'p',amount:'400'}]};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,{...book(r),payments:[p]}));d.context_opportunity='other';const other={...row('two'),direction:'received',settlement:'complete',allocations:[{paymentId:'p',amount:'300'}]};assert.equal(recordIrpc(d,{...book(other),payments:[p]}).result.rows[0].status,'indeterminate');other.allocations[0].amount='200';assert.equal(recordIrpc(d,{...book(other),payments:[p]}).result.rows[0].status,'calculated');assert.equal(recordIrpc(d,{...book(other),payments:[{...p,fxSelection:fx({mean:'65'})}]}).result.rows[0].status,'indeterminate');});
test('accepted v1MZN10% results remain current and bytepreserved without recomputation',()=>{const d=draft(),b=irpcBook(d),before=JSON.stringify(b);assert.equal(irpcCurrent(d,b),true);assert.equal(JSON.stringify(irpcBook(d)),before);assert.deepEqual(irpcBook({...d,modelo_a_snapshot:saveIrpcBook(d,b)}),b);});
test('new normal/FX result preserves old maps and acceptedresult in history,report frozen after change',()=>{const d=draft(),b=irpcBook(d),old=structuredClone(b.result),s=JSON.parse(d.modelo_a_snapshot);const next=recordIrpc(d,{...b,rows:[...b.rows,{...row('fx'),currency:'USD',base:'1000',fxSelection:fx()}]});d.modelo_a_snapshot=saveIrpcBook(d,next);for(const[k,v]of Object.entries(s))if(k!=='irpcWithholdings')assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v);assert.deepEqual(next.history.at(-1),old);next.rows.at(-1).fxSelection.mean='65';assert.equal(irpcCurrent(d,next),false);d.modelo_a_snapshot=saveIrpcBook(d,next);const report=irpcReportRows(d).at(-1);assert.match(report[2],/12800.00/);assert.match(report[3],/Histórico/);assert.match(report[3],/taxa: 64/);});
test('export keeps currencies,normal/special distinctions,payment refs and externalformalities separate',()=>{const d=draft(),r={...row(),currency:'USD',base:'1000',fxSelection:fx()};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const h=irpcPreparationHtml(d);assert.match(h,/Original: 1000 USD/);assert.ok(!h.includes("selectionKey"));assert.match(h,/20%/);assert.match(h,/CER/);assert.match(h,/sem submissão/);assert.match(h,/2026-10/);});
if(process.env.IRPC_GROUPED_FIXTURE_OUTPUT){
const d=draft(),old=irpcBook(d),s=fx(),f={...newCurrencyEvent('retention-fx','MZN'),description:'SYNTHETIC existing BMselection for retention',fxSelection:s,sourceCurrency:'USD',fxDirection:s.direction,fxRate:s.applied.rate,fxDate:s.applied.date,fxSource:s.applied.source};const cb=currencyEventBook(d);d.modelo_a_snapshot=saveCurrencyEventBook(d,{...cb,rows:[...cb.rows,f]});const source=irpcFxSources(d).find(v=>v.id===f.id);
const foreign={...row('foreign-royalty'),currency:'USD',base:'1000',fxSelection:source.selection,fxLink:{id:source.id,fingerprint:source.fingerprint},settlement:'complete',settlementRef:'SYNTHETIC IRPC remittance5000',allocations:[{paymentId:'p3',amount:'5000'}]},special={...row('special-telecom'),kind:'telecom',ordinary:{...row().ordinary,specialFacts:'yes'}},resident={...row('resident-royalty'),residence:'resident',base:'50000'},received={...row('received-foreign-royalty'),direction:'received',currency:'USD',base:'100',fxSelection:s,settlement:'complete',settlementRef:'SYNTHETIC proof10USD converted640MZN',allocations:[{paymentId:'p4',amount:'640'}]};
const b=recordIrpc(d,{...old,rows:[...old.rows,foreign,special,resident,received],payments:[...old.payments,{id:'p3',kind:'remittance',reference:'SYNTHETIC-IRPC-AT5000',date:'2026-10-20',amount:'5000',currency:'MZN'},{id:'p4',kind:'withholding_proof',reference:'SYNTHETIC-IRPC-USD10',date:'2026-10-20',amount:'10',currency:'USD',fxSelection:s}]});d.modelo_a_snapshot=saveIrpcBook(d,b);fs.writeFileSync(process.env.IRPC_GROUPED_FIXTURE_OUTPUT,JSON.stringify(d,null,2)+'\n');
}
test("62(3) definitive capital needs exact seat/management facts and is not an annual credit",()=>{const r={...row(),residence:"resident",kind:"capital"};r.ordinary.capitalType="listed_debt_mz";assert.equal(calc(r).tax,null);r.ordinary.capitalSeatAndManagement="yes";assert.equal(calc(r).nature,"definitive");assert.equal(calc(r).rate,20);assert.equal("annualCredit"in calc(r),false);});
test('confirming unchanged FX preserves source link;deliberate input change detaches only that selection',()=>{const input={currency:'USD',fxSelection:fx(),fxLink:{id:'saved',fingerprint:'original'}};const confirmed={...input.fxSelection,applied:selectFx(input.fxSelection)};assert.deepEqual(updateIrpcFx(input,confirmed).fxLink,input.fxLink);assert.equal(updateIrpcFx(input,{...input.fxSelection,mean:'65'}).fxLink,undefined);assert.equal(input.fxSelection.mean,'64');assert.equal(input.fxLink.id,'saved');});
+63
View File
@@ -0,0 +1,63 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newIrpcRow,emptyIrpcOrdinaryFacts,recordIrpc,saveIrpcBook,irpcBook,irpcCurrent,irpcReportRows} from '../../resources/js/lib/irpc-withholdings.ts';
import {newMrftaInput,recordMrfta,mrftaCurrent,mrftaHtml,prepareMrfta} from '../../resources/js/lib/irpc-mrfta.ts';
import {newIrpcInterestRule,calculateIrpcInterest} from '../../resources/js/lib/irpc-interest.ts';
import {mrftaCases,mrftaCaseHtml} from '../../resources/js/lib/irpc-mrfta-cases.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const draft=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-grouped-preserved.json',import.meta.url),'utf8'));
const rule=()=>({...newIrpcInterestRule(),annualRate:'10',rateSource:'QA parâmetro fictício; não é taxa fiscal2026',fiscalBasis:'QA fundamento sintético para ensaio aritmético',validFrom:'2026-01-01',validTo:'2026-12-31',dayBasis:'365',dayBasisRef:'QA fonte sintética da base365',rounding:'total_cent_half_up',roundingRef:'QA fonte sintética do arredondamento do total',retention:'withheld',occurrenceRef:'QA retenção e atraso documentados'});
const row=(id='one')=>({...newIrpcRow(id),counterparty:'QA contraparte',documentRef:'QA-'+id,base:'36500',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'QA incidência e residência',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'QA custo'}],settlement:'none',settlementRef:'QA sem entregas',ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'QA royalties não digitais'}});
const book=(payments=[])=>{const r=row();if(payments.length){r.settlement='complete';r.allocations=payments.map(p=>({paymentId:p.id,amount:p.amount}));}return recordIrpc(draft(),{version:1,rows:[r],payments});};
const payment=(amount,date='2026-11-25',id='p')=>({id,kind:'remittance',reference:'QA entrega-'+id,date,amount,currency:'MZN'});
const calc=(b=book(),r=rule(),due='2026-11-20',end='2026-11-30')=>calculateIrpcInterest(b,true,'2026-10',due,'QA prazo documentado',end,r);
const input=()=>({...newMrftaInput(),period:'2026-10',declaration:'initial',timing:'late',nuit:'999999999',name:'QA empresa',taxArea:'QA área fiscal',address:'QA domicílio',completeness:'yes',completenessRef:'QA reconciliação do período',otherRubrics:'none',otherRubricsRef:'QA outras rubricas não devidas',codes:[{rowId:'one',cer:'999',reference:'QA CER fictício, não oficial'}],interest:{...newMrftaInput().interest,mode:'',calculation:'automatic',period:'2026-10',date:'2026-11-30',dueDate:'2026-11-20',dueReference:'QA prazo documentado',reference:'QA ocorrência',automatic:rule()}});
test('daily interest uses documented parameters and explains principal,days and exact arithmetic',()=>{
const v=calc();assert.deepEqual(v.pending,[]);assert.equal(v.total.numerator,'20');assert.equal(v.rows[0].segments[0].from,'2026-11-21');assert.equal(v.rows[0].segments[0].to,'2026-11-30');assert.equal(v.rows[0].segments[0].days,10);assert.equal(v.rows[0].segments[0].principal.numerator,'7300');
});
test('partial remittance reduces principal after its payment day without duplicate daily interest',()=>{
const v=calc(book([payment('3650')]));assert.equal(v.total.numerator,'15');assert.deepEqual(v.rows[0].segments.map(s=>[s.days,s.principal.numerator]),[[5,'7300'],[5,'3650']]);
const sameDay=calc(book([payment('1825','2026-11-25','p1'),payment('1825','2026-11-25','p2')]));assert.equal(sameDay.total.numerator,'15');assert.equal(sameDay.rows[0].segments.length,2);
});
test('full payment stops charges and pre-deadline payment reduces the opening debt',()=>{
assert.equal(calc(book([payment('7300')])).total.numerator,'10');assert.equal(calc(book([payment('3650','2026-11-18')])).total.numerator,'10');
});
test('missing rate,reference,day basis or applicable version never becomes a default tax rate',()=>{
for(const patch of [{annualRate:''},{annualRate:'0'},{annualRate:'-1'},{rateSource:''},{fiscalBasis:''},{dayBasis:''},{dayBasisRef:''},{rounding:''},{roundingRef:''},{validTo:'2017-12-31'},{occurrenceRef:''}]){const v=calc(book(),{...rule(),...patch});assert.equal(v.total,null);assert.ok(v.pending.length);}
assert.equal(calculateIrpcInterest(book(),false,'2026-10','2026-11-20','QA prazo','2026-11-30',rule()).total,null);
});
test('on-account not-withheld cases remain separate;definitive not-withheld uses its own supported branch',()=>{
const r=row();r.residence='resident';const b=recordIrpc(draft(),{version:1,rows:[r],payments:[]});assert.equal(calc(b,{...rule(),retention:'not_withheld'}).total,null);assert.match(calc(b,{...rule(),retention:'not_withheld'}).pending.join(' '),/declaração anual/);assert.equal(calc(b).total.numerator,'20');assert.equal(calc(book(),{...rule(),retention:'not_withheld'}).total.numerator,'20');
});
test('documented actual-year basis splits year boundary including leap year without floating money',()=>{
const r={...rule(),validTo:'2028-12-31',dayBasis:'actual_year'},v=calc(book(),r,'2027-12-30','2028-01-02');assert.deepEqual(v.rows[0].segments.map(s=>[s.days,s.denominator]),[[1,365],[2,366]]);assert.deepEqual(v.total,{numerator:'599',denominator:'100'});
});
test('rounding is explicit,on total only,and matches the documented convention',()=>{
assert.deepEqual(calc(book(),{...rule(),annualRate:'10.0025'}).total,{numerator:'2001',denominator:'100'});assert.equal(calc(book(),{...rule(),annualRate:'10.0025',rounding:'total_cent_down'}).total.numerator,'20');
});
test('unreconciled payments and payments after liquidation keep the affected calculation indeterminate',()=>{
assert.equal(calc(book([payment('100','2026-12-01')])).total,null);const b=book();b.result.rows[0].settled=null;assert.equal(calc(b).total,null);
});
test('distinct rates and CER produce stable working cases,not invented declarations or merged rates',()=>{
const b={...book(),mrfta:input()};const second={...b.result.rows[0],id:'two',rate:10};b.result.rows.push(second);b.result.inputs.push(row('two'));b.mrfta.codes.push({rowId:'two',cer:'998',reference:'QA second fictionalCER'});const m=prepareMrfta(b,true),cases=mrftaCases(m);assert.equal(cases.length,2);assert.notEqual(cases[0].key,cases[1].key);assert.deepEqual(cases.map(c=>[c.rate,c.cer]),[[20,'999'],[10,'998']]);assert.equal(m.total,null);const html=mrftaCaseHtml(m,cases[0].key);assert.match(html,/Não é uma declaração autónoma/);assert.ok(html.includes('Pendências do período'));assert.deepEqual(mrftaCases({...m,lines:[...m.lines].reverse()}).map(c=>c.key).sort(),cases.map(c=>c.key).sort());
});
test('missing CER references remain visible within each case sheet without presumed correspondence',()=>{
const b={...book(),mrfta:input()};b.mrfta.codes[0].reference='';const m=prepareMrfta(b,true);assert.match(mrftaCases(m)[0].pending,/referência por corresponder/);b.mrfta.codes=[];assert.match(mrftaCases(prepareMrfta(b,true))[0].pending,/sem preenchimento presumido/);
});
test('automatic result persists,recovers and reaches dossier with traceable parameters and history',()=>{
const d=draft(),b=recordMrfta({...book([payment('3650')]),mrfta:input()},true);d.modelo_a_snapshot=saveIrpcBook(d,b);const recovered=JSON.parse(JSON.stringify(d)),saved=irpcBook(recovered);assert.equal(irpcCurrent(recovered,saved),true);assert.equal(mrftaCurrent(saved,true),true);assert.equal(saved.mrftaResult.model.interest.numerator,'15');assert.equal(saved.mrftaResult.model.total.numerator,'3665');assert.equal(saved.mrftaResult.model.automatic.rows[0].segments.length,2);
const report=JSON.stringify(irpcReportRows(recovered));assert.match(report,/Taxa anual:10%/);assert.match(report,/QA parâmetro fictício/);assert.match(report,/5dias\/365/);assert.match(report,/metade por excesso/);assert.ok(!report.includes('total_cent_half_up'));assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/3665/);assert.match(mrftaHtml(saved,true),/Juros calculados por parâmetros documentados/);assert.match(mrftaHtml(saved,true),/simulação pelos pressupostos introduzidos, sem confirmação fiscal/);assert.match(mrftaHtml(saved,true),/Códigos provisórios ou de simulação não são correspondências oficiais/);assert.match(report,/sem validação por catálogo oficial/);assert.match(mrftaCases(saved.mrftaResult.model)[0].pending,/Código provisório ou de simulação não é correspondência oficial/);
saved.mrfta.interest.automatic.annualRate='';assert.equal(mrftaCurrent(saved,true),false);const changed=recordMrfta(saved,true);assert.equal(changed.mrftaResult.model.interest,null);assert.deepEqual(changed.mrftaHistory.at(-1),b.mrftaResult);
fs.mkdirSync('outputs/irpc-interest-cer',{recursive:true});fs.writeFileSync('outputs/irpc-interest-cer/calculated.json',JSON.stringify(recovered));fs.writeFileSync('outputs/irpc-interest-cer/preparation.html',mrftaHtml(b,true));
});
test('new parameters reject malformed persisted values while the installed snapshot remains current',()=>{
const d=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-mrfta-preserved.json',import.meta.url),'utf8'));const b=irpcBook(d);assert.equal(irpcCurrent(d,b),true);assert.equal(mrftaCurrent(b,true),true);const invalid={...book(),mrfta:input()};invalid.mrfta.interest.automatic.annualRate=10;assert.throws(()=>saveIrpcBook(draft(),invalid));
});
test('two calculated incomes with different treaty/internal rates remain separate without an invented declaration total',()=>{
const d=draft(),first={...row(),settlement:'complete',allocations:[{paymentId:'p',amount:'3650'}]},second={...row('two'),specialRelief:'claimed',reliefPreparation:{kind:'treaty',rule:'ptMz12',instrument:'QA CDT PT-MZ e Protocolo',article:'12',country:'PT',residenceDocument:'QA certificado Portugal2026',validFrom:'2026-01-01',validTo:'2026-12-31',conditions:'QA royalties qualificadas semEP',evidence:'QA contrato e residência',facts:{treatyResident:'yes',beneficialOwner:'yes',payerResidentMz:'yes',armLength:'yes',royaltyDefinition:'yes'}}};
const b=recordMrfta({...recordIrpc(d,{version:1,rows:[first,second],payments:[payment('3650')]}),mrfta:{...input(),codes:[...input().codes,{rowId:'two',cer:'998',reference:'QA CER998 fictício, não oficial'}]}},true);const m=b.mrftaResult.model;
assert.deepEqual(b.result.rows.map(r=>r.rate),[20,10]);assert.equal(m.interest.numerator,'25');assert.equal(m.total,null);assert.deepEqual(mrftaCases(m).map(c=>[c.rate,c.cer]),[[20,'999'],[10,'998']]);assert.match(m.pending.join(' '),/taxas ou CER distintos/);
d.modelo_a_snapshot=saveIrpcBook(d,b);fs.writeFileSync('outputs/irpc-interest-cer/mixed.json',JSON.stringify(d));
});
@@ -0,0 +1,91 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import crypto from 'node:crypto';
import {countryIsoCodes} from '../../resources/js/lib/country-iso-codes.ts';
import {internationalCase} from './helpers/irpc-international-case.mjs';
import {calculateAnnual,annualBook,recordAnnual,saveAnnualBook,annualCurrent,annualHtml,annualReportRows,prepareInternationalBook} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const value=v=>v?Number(v.numerator)/Number(v.denominator):null;
const now='2026-10-27T17:00:00+02:00';
const calc=({d,b})=>calculateAnnual(d,b);
const pending=c=>{const m=calc(c);assert.equal(m.status,'indeterminate',JSON.stringify(m.pending));assert.equal(m.arithmeticBalance,null);assert.equal(m.internationalCredit.deducted,null);assert.equal(m.pecDeduction.deducted,null);return m;};
const position=c=>c.b.internationalCredits.positions[0];
const proof=c=>position(c).payments[0];
test('country code projection exactly reuses the existing master and its source hash',()=>{
const raw=Buffer.from(fs.readFileSync(new URL('../../resources/data/iso3166.tab',import.meta.url),'utf8').replace(/\r\n/g,'\n'));
const codes=raw.toString('utf8').split(/\r?\n/).filter(l=>l&&!l.startsWith('#')).map(l=>l.split('\t')[0]);
assert.deepEqual(countryIsoCodes,codes);const source=fs.readFileSync(new URL('../../resources/js/lib/country-iso-codes.ts',import.meta.url),'utf8');assert.ok(source.includes(crypto.createHash('sha256').update(raw).digest('hex')));
});
test('10 paid capped at documented national8 precedes PEC200/PPC500:1408-8-200-500=700',()=>{
const m=calc(internationalCase());assert.deepEqual(m.pending,[]);const p=m.internationalCredit.positions[0];
assert.equal(value(p.paid),10);assert.equal(value(p.eligible),8);assert.equal(value(p.aboveLimit),2);assert.equal(value(m.internationalCredit.deducted),8);assert.equal(value(m.internationalCredit.afterCredit),1400);assert.equal(value(m.pecDeduction.deducted),200);assert.equal(value(m.payable),700);assert.equal(m.internationalCredit.carryCreated,false);assert.equal(m.officialBalancesConsumed,false);
});
test('documented treaty limit6 caps credit without country-based automatic entitlement',()=>{
const c=internationalCase();Object.assign(position(c),{treaty:'yes',treatyLimit:'6',treatyReference:'SYNTHETIC QA convention/article/exercise/limit, not real treaty data'});
const m=calc(c);assert.equal(value(m.internationalCredit.deducted),6);assert.equal(value(m.payable),702);assert.equal(value(m.internationalCredit.positions[0].aboveLimit),4);
position(c).treatyLimit='';pending(c);position(c).treatyLimit='6';position(c).treatyReference='';pending(c);
});
test('multiple actual receipts share one country limit;two countries keep separate limits',()=>{
const c=internationalCase();proof(c).amount='6';proof(c).proofAmount='6';position(c).payments.push({...proof(c),id:'foreign-second',document:'OTHER-RECEIPT',amount:'4',proofAmount:'4'});assert.equal(value(calc(c).internationalCredit.deducted),8);
const other=structuredClone(position(c));other.id='country-pt';other.country='PT';other.qualificationCountry='PT';other.nationalLimit='4';other.payments=[{...other.payments[0],id:'pt-tax',document:'PT-OTHER-RECEIPT',proofAmount:'5',amount:'5'}];c.b.internationalCredits.positions.push(other);assert.equal(value(calc(c).internationalCredit.deducted),12);assert.equal(value(calc(c).payable),696);
});
test('same country cannot multiply the national fraction across positions or other context maps',()=>{
const a=internationalCase(),copy=structuredClone(position(a));copy.id='duplicate-country';copy.payments[0].id='other-proof';copy.payments[0].document='OTHER';a.b.internationalCredits.positions.push(copy);assert.match(pending(a).pending.join(' '),/país\/exercício/);
const b=internationalCase(),other=structuredClone(b.b);other.internationalCredits.positions[0].payments[0].id='elsewhere';other.internationalCredits.positions[0].payments[0].document='ELSEWHERE-RECEIPT';const s=JSON.parse(b.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};b.d.modelo_a_snapshot=JSON.stringify(s);assert.match(pending(b).pending.join(' '),/noutro mapa/);
});
test('actual previous credit use reduces eligible credit,not gross tax or simulated history',()=>{
const c=internationalCase();position(c).previousUse.amount='3';let m=calc(c);assert.equal(value(m.internationalCredit.positions[0].eligible),8);assert.equal(value(m.internationalCredit.positions[0].available),5);assert.equal(value(m.payable),703);
position(c).previousUse.amount='9';assert.match(pending(c).pending.join(' '),/excede/);position(c).previousUse={state:'absent',amount:'',reference:'QA no actual previous use'};assert.equal(value(calc(c).payable),700);
});
test('lack of collecta is separated from tax above eligibility limits and never refunds credit or PEC',()=>{
const c=internationalCase('2000','2000','200','0'),m=calc(c);assert.equal(m.status,'balanced');assert.equal(value(m.internationalCredit.deducted),1408);assert.equal(value(m.internationalCredit.insufficientCollecta),592);assert.equal(value(m.internationalCredit.positions[0].aboveLimit),0);assert.equal(value(m.pecDeduction.deducted),0);assert.equal(value(m.pecDeduction.remaining),200);assert.equal(value(m.recoverable),0);assert.equal(m.internationalCredit.countryAllocationCreated,false);
c.b.ppc={...internationalCase().b.ppc};const recovered=calc(c);assert.equal(value(recovered.recoverable),500);assert.equal(value(recovered.internationalCredit.insufficientCollecta),592);assert.equal(recovered.internationalCredit.carryCreated,false);
});
test('known zero,declared absence and missing retain distinct inputs and outcomes',()=>{
const zero=internationalCase('0');assert.equal(value(calc(zero).internationalCredit.deducted),0);assert.equal(value(calc(zero).payable),708);
const a=internationalCase();a.b.internationalCredits.state='absent';a.b.internationalCredits.positions=[];assert.equal(value(calc(a).payable),708);
a.b.internationalCredits.state='';pending(a);a.b.internationalCredits.state='known';pending(a);
const b=internationalCase();b.b.internationalCredits.state='absent';pending(b);position(b).previousUse={state:'absent',amount:'0',reference:'QA contradiction'};pending(b);
});
test('country,entity,year,inclusion,current assessment,domestic fraction and convention require support',()=>{
for(const patch of [{country:'MZ'},{country:'ZZ'},{country:''},{country:'PT'},{qualificationCountry:''},{subjectNuit:'999999999'},{year:'2025'},{incomeIncluded:''},{incomeIncluded:'no'},{incomeAmount:''},{incomeAmount:'0'},{incomeReference:''},{nationalLimit:''},{nationalLimitReference:''},{assessmentKey:'stale'},{treaty:''},{treaty:'no',treatyLimit:'0'},{treatyReference:''}]){const c=internationalCase();Object.assign(position(c),patch);pending(c);}
for(const patch of [{state:''},{amount:''},{reference:''}]){const c=internationalCase();Object.assign(position(c).previousUse,patch);pending(c);}
});
test('actual foreign income tax proof is required,not projections,other taxes,wrong exercise or unsupported currency',()=>{
for(const patch of [{effective:''},{effective:'no'},{incomeTax:''},{incomeTax:'no'},{taxReference:''},{document:''},{issuer:''},{reference:''},{subjectNuit:'999999999'},{year:'2025'},{date:'2026-02-30'},{date:'2025-09-30'},{currency:'USD'},{amount:'10.01'},{amount:'0.001'}]){const c=internationalCase();Object.assign(proof(c),patch);pending(c);}
const c=internationalCase();position(c).payments=[];pending(c);
});
test('payment date differs from income exercise only with explicit correspondence,never an automatic carry',()=>{
const c=internationalCase();proof(c).date='2027-01-15';pending(c);proof(c).exerciseReference='SYNTHETIC QA own-exercise2026 assessment and payment relation';const m=calc(c);assert.equal(value(m.payable),700);assert.equal(m.internationalCredit.carryCreated,false);
});
test('same physical foreign receipt cannot be duplicated,re-dated or used as PPC/PEC/RF',()=>{
const a=internationalCase();position(a).payments.push({...proof(a),id:'other-date',date:'2026-10-01'});pending(a);
for(const kind of ['ppc','pec','rf']){const c=internationalCase('10','8','200','500',kind==='rf'?'300':'0');const physical=kind==='ppc'?c.b.ppc.rows[0]:kind==='pec'?c.b.pecPayments.rows[0]:{issuer:'QA AT',document:'QA-RF-PEC-2026',proofAmount:'300'};Object.assign(proof(c),{issuer:physical.issuer,document:physical.document,proofAmount:physical.proofAmount});assert.match(pending(c).pending.join(' '),/naturez|comprovativo/i);}
});
test('a foreign receipt cannot move country/entity across context maps;external changes invalidate current result',()=>{
const c=internationalCase(),other=structuredClone(c.b);other.ppc.rows=[];other.ppc.state='absent';other.pecPayments.rows=[];other.pecPayments.state='absent';other.internationalCredits.positions[0].country='PT';const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);pending(c);
other.internationalCredits.positions[0].payments[0].document='OTHER-PT';c.d.modelo_a_snapshot=JSON.stringify(s);assert.equal(value(calc(c).payable),700);c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const saved=annualBook(c.d);assert.equal(annualCurrent(c.d,saved),true);
const edit=JSON.parse(c.d.modelo_a_snapshot);edit.irpcAnnual['opportunity:other'].internationalCredits.positions[0].country='ZA';c.d.modelo_a_snapshot=JSON.stringify(edit);assert.equal(annualCurrent(c.d,saved),false);pending({...c,b:saved});
});
test('economic/benefit positive or missing states block only the dependent annual balance,not known eligibility',()=>{
for(const kind of ['economic','benefits'])for(const patch of [{state:''},{state:'known',amount:'1'},{reference:''}]){const c=internationalCase();Object.assign(c.b.internationalCredits[kind],patch);const m=pending(c);assert.equal(value(m.internationalCredit.positions[0].eligible),8);assert.equal(value(m.assessment.colecta),1408);}
});
test('exact decimals and withholding are included once after credit and PEC',()=>{
const c=internationalCase('10.25','8.15','200','500','300');position(c).previousUse.amount='3.01';const m=calc(c);assert.deepEqual(m.internationalCredit.deducted,{numerator:'257',denominator:'50'});assert.equal(value(m.payable),402.86);assert.equal(value(m.knownWithholdings),300);
});
test('documented fractional ceilings retain exact arithmetic without assumed tax rounding',()=>{
const c=internationalCase('10','1/3');const m=calc(c);assert.deepEqual(m.internationalCredit.deducted,{numerator:'1',denominator:'3'});assert.deepEqual(m.payable,{numerator:'2123',denominator:'3'});
position(c).nationalLimit='1/0';pending(c);
});
test('prepare preserves deliberate prior summary and history;save/recover/report appends frozen records',()=>{
const c=internationalCase(),detail=structuredClone(c.b.internationalCredits);delete c.b.internationalCredits;const original=JSON.parse(c.d.modelo_a_snapshot),plan=c.d.business_plan_snapshot;c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const prior=structuredClone(annualBook(c.d).result);assert.equal(value(prior.model.payable),708);
let next=prepareInternationalBook(annualBook(c.d));next.internationalCredits=detail;c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,next,now));const back=annualBook(JSON.parse(JSON.stringify(c.d)));assert.deepEqual(back.history,[prior]);assert.equal(annualCurrent(c.d,back),true);assert.equal(value(back.result.model.payable),700);assert.deepEqual(back.credits,{state:'known',amount:'0',reference:'QA créditos precedentes conhecidos zero'});
const after=JSON.parse(c.d.modelo_a_snapshot);delete after.irpcAnnual;assert.deepEqual(after,original);assert.equal(c.d.business_plan_snapshot,plan);
const html=annualHtml(c.d);assert.match(html,/Crédito internacional — antes do PEC/);assert.match(html,/700.00/);assert.match(html,/1400.00/);assert.match(JSON.stringify(createProjectDossierReport(c.d,{selectedSections:new Set(['finance'])})),/Crédito internacional — ZA/);
const frozen=structuredClone(back.result);back.internationalCredits.positions[0].nationalLimit='7';c.d.modelo_a_snapshot=saveAnnualBook(c.d,back);assert.equal(annualCurrent(c.d,back),false);assert.deepEqual(back.result,frozen);assert.match(annualReportRows(c.d).flat().join(' '),/Histórico/);assert.throws(()=>annualHtml(c.d),/fontes actuais/);
});
test('malformed optional international detail never resets existing snapshots',()=>{
const c=internationalCase(),snapshot=c.d.modelo_a_snapshot;proof(c).incomeTax=null;assert.throws(()=>saveAnnualBook(c.d,c.b),/não recuperável/);assert.equal(c.d.modelo_a_snapshot,snapshot);
});
+54
View File
@@ -0,0 +1,54 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newIrpcRow,emptyIrpcOrdinaryFacts,recordIrpc,saveIrpcBook,irpcBook,irpcCurrent,irpcReportRows} from '../../resources/js/lib/irpc-withholdings.ts';
import {newMrftaInput,prepareMrfta,recordMrfta,mrftaCurrent,mrftaHtml} from '../../resources/js/lib/irpc-mrfta.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const draft=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-grouped-preserved.json',import.meta.url),'utf8'));
const row=(id='principal')=>({...newIrpcRow(id),counterparty:'QA contraparte',documentRef:'QA-'+id,base:'100000',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'QA contrato e residência',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'QA custo'}],settlement:'complete',settlementRef:'QA reconciliação',allocations:[{paymentId:'p',amount:'3000'}],ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'QA royalties não digitais'}});
const input=()=>({...newMrftaInput(),period:'2026-10',declaration:'initial',timing:'late',nuit:'999999999',name:'QA sujeito passivo',commercialName:'QA',taxArea:'QA área fiscal',address:'QA domicílio',contact:'qa@example.invalid',completeness:'yes',completenessRef:'QA conferência do período e livros',otherRubrics:'none',otherRubricsRef:'QA ausência documentada de IRPS/tributação autónoma/outros valores',codes:[{rowId:'principal',cer:'999',reference:'QA CER fictício, apenas teste'}],interest:{...newMrftaInput().interest,mode:'assessment',period:'2026-10',amount:'500',date:'2026-11-25',reference:'QA liquidação de juros compensatórios'}});
function book(rows=[row()]){return {...recordIrpc(draft(),{version:1,rows,payments:[{id:'p',kind:'remittance',reference:'QA entrega IRPC',date:'2026-11-15',amount:'3000',currency:'MZN'}]}),mrfta:input()};}
test('MRFTA fills one period with principal, documented interest, payment and balance separately',()=>{
const b=book(),m=prepareMrfta(b,true);assert.deepEqual(m.pending,[]);assert.equal(m.principal.numerator,'20000');assert.equal(m.paid.numerator,'3000');assert.equal(m.interest.numerator,'500');assert.equal(m.total.numerator,'17500');assert.equal(m.lines[0].rate,20);assert.equal(m.lines[0].cer,'999');assert.match(m.interestReason,/transcritos/);
});
test('unknown MAIBOR2026 never becomes zero or MIMO; independent principal remains calculated',()=>{
const b=book();b.mrfta.interest.mode='';const m=prepareMrfta(b,true);assert.equal(m.interest,null);assert.equal(m.total,null);assert.equal(m.principal.numerator,'20000');assert.equal(b.result.rows[0].status,'calculated');assert.match(m.pending.join(' '),/MAIBOR2026/);
});
test('insufficient CER or contradictory rate per rubric blocks total without losing rows',()=>{
const b=book();b.mrfta.codes=[];let m=prepareMrfta(b,true);assert.equal(m.total,null);assert.equal(m.lines.length,1);assert.match(m.pending.join(' '),/CER/);
const other=structuredClone(b.result.rows[0]);other.id='other';other.rate=10;other.tax={numerator:'10000',denominator:'1'};b.result.rows.push(other);b.result.inputs.push({...row('other'),allocations:[]});b.mrfta.codes=[{rowId:'principal',cer:'999',reference:'QA ref'},{rowId:'other',cer:'999',reference:'QA ref'}];m=prepareMrfta(b,true);assert.equal(m.lines.length,2);assert.match(m.pending.join(' '),/taxas ou CER distintos/);assert.equal(m.total,null);
});
test('other months,suffered withholdings and documented dispensas stay outside payable rows',()=>{
const b=book();for(const[id,patch,original]of [['other-month',{period:'2026-11'},{}],['suffered',{}, {direction:'received'}],['dispensed',{treatment:'dispensed',tax:{numerator:'0',denominator:'1'}},{}]]){b.result.rows.push({...b.result.rows[0],id,...patch});b.result.inputs.push({...row(id),...original});}
const m=prepareMrfta(b,true);assert.equal(m.lines.length,1);assert.equal(m.excluded.length,3);assert.equal(m.principal.numerator,'20000');assert.equal(m.total.numerator,'17500');
});
test('unsupported digital and indeterminate cases retain explicit gaps without guessed rubric',()=>{
const b=book();b.result.inputs[0].kind='digital_services';let m=prepareMrfta(b,true);assert.equal(m.lines.length,0);assert.equal(m.total,null);assert.match(m.pending.join(' '),/digital/);b.result.inputs[0].kind='royalty';b.result.rows[0].status='indeterminate';b.result.rows[0].tax=null;m=prepareMrfta(b,true);assert.equal(m.total,null);assert.match(m.pending.join(' '),/indeterminado/);
});
test('timely remittance only yields zero interest with complete proof and explicit applicable deadline',()=>{
const r=row();r.base='15000';const b=book([r]);b.mrfta.interest={...b.mrfta.interest,mode:'timely',period:'2026-10',dueDate:'2026-11-20',dueReference:'QA prazo documentado e calendário'};assert.equal(prepareMrfta(b,true).interest.numerator,'0');assert.equal(prepareMrfta(b,true).total.numerator,'0');
b.mrfta.interest.dueDate='2026-11-10';assert.equal(prepareMrfta(b,true).interest,null);b.mrfta.interest.dueDate='2026-11-20';b.result.rows[0].settled=null;assert.equal(prepareMrfta(b,true).interest,null);
});
test('assessment cannot leak into another period or accept negative amount and missing reference',()=>{
const b=book();for(const patch of [{period:'2026-11'},{amount:'-10'},{reference:''},{date:'2026-02-30'}]){const copy=structuredClone(b);Object.assign(copy.mrfta.interest,patch);assert.equal(prepareMrfta(copy,true).interest,null);assert.equal(prepareMrfta(copy,true).total,null);}
});
test('MRFTA save,recover,history and dossier preserve existing tax maps and freeze evidence',()=>{
const d=draft(),prior=irpcBook(d),original=JSON.parse(d.modelo_a_snapshot);let b=book();b.history=[prior.result];b=recordMrfta(b,true);d.modelo_a_snapshot=saveIrpcBook(d,b);const recovered=JSON.parse(JSON.stringify(d)),saved=irpcBook(recovered);assert.deepEqual(saved,b);assert.equal(irpcCurrent(recovered,saved),true);assert.equal(mrftaCurrent(saved,true),true);
for(const[k,v]of Object.entries(original))if(k!=='irpcWithholdings')assert.deepEqual(JSON.parse(recovered.modelo_a_snapshot)[k],v,k);
const report=irpcReportRows(recovered);assert.match(JSON.stringify(report),/17500/);assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/MRFTA/);
assert.match(mrftaHtml(saved,true),/Quadro7/);assert.match(mrftaHtml(saved,true),/QA CER fictício/);const before=structuredClone(saved.mrftaResult);saved.mrfta.interest.amount='600';assert.equal(mrftaCurrent(saved,true),false);assert.throws(()=>mrftaHtml(saved,true));const next=recordMrfta(saved,true);assert.deepEqual(next.mrftaHistory.at(-1),before);assert.equal(next.mrftaResult.model.total.numerator,'17600');
fs.mkdirSync('outputs/irpc-mrfta',{recursive:true});fs.writeFileSync('outputs/irpc-mrfta/calculated.json',JSON.stringify(recovered));fs.writeFileSync('outputs/irpc-mrfta/preparation.html',mrftaHtml(b,true));
});
test('malformed persisted MRFTA rejects recovery without replacing originals',()=>{
const b=book();b.mrfta.codes.push({...b.mrfta.codes[0]});assert.throws(()=>saveIrpcBook(draft(),b));b.mrfta.codes=[];b.mrfta.interest.amount=5;assert.throws(()=>saveIrpcBook(draft(),b));
});
test('export escapes identities,documents and assessment references',()=>{
const b=book();b.mrfta.name='<script>QA</script>';b.mrfta.interest.reference='<img src=x onerror=QA>';const html=mrftaHtml(recordMrfta(b,true),true);assert.ok(!html.includes('<script>QA'));assert.ok(html.includes('&lt;script&gt;'));assert.ok(html.includes('&lt;img'));
});
test('other tax rubrics cannot silently become zero and partial rows remain available',()=>{
const b=book();b.mrfta.otherRubrics='pending';const m=prepareMrfta(b,true);assert.equal(m.total,null);assert.equal(m.principal.numerator,'20000');assert.match(m.pending.join(' '),/IRPS,tributação autónoma/);assert.throws(()=>mrftaHtml(recordMrfta(b,false),false));
});
test('same rubric rate and documented CER aggregate without losing document references',()=>{
const other={...row('other'),allocations:[],settlement:'none'},b=book([row(),other]);b.mrfta.codes.push({rowId:'other',cer:'999',reference:'QA segundo documento de CER fictício'});const m=prepareMrfta(b,true);assert.equal(m.lines.length,1);assert.equal(m.lines[0].base.numerator,'200000');assert.equal(m.principal.numerator,'40000');assert.equal(m.total.numerator,'37500');assert.equal(m.lines[0].references.length,2);assert.equal(m.lines[0].documents.length,2);
});
@@ -0,0 +1,16 @@
import test from'node:test';import assert from'node:assert/strict';import fs from'node:fs';
import{normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalReportRows}from'../../resources/js/lib/irpc-normal.ts';
import{newNormalAssessmentInput,calculateNormalAssessment}from'../../resources/js/lib/irpc-normal-assessment.ts';
import{fraction}from'../../resources/js/lib/fiscal-rational.ts';import{createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-base-preserved.json',import.meta.url),'utf8'));
const input=()=>({...newNormalAssessmentInput(),treatment:'general_61_1',reference:'QA sujeito residente,actividade ordinária,sem taxa reduzida/benefício ou tributação autónoma neste recorte'});
const context=()=>({year:'2026',scope:'resident_general',currency:'MZN'});
test('current base4400 times statutory general32percent yields1408 colecta with explicit2026 period and L8 unknown balance',()=>{const d=seed(),b=normalBook(d);b.assessment=input();const m=calculateNormal(d,b);assert.equal(m.taxableProfit.numerator,'6700');assert.equal(m.taxableBase.numerator,'4400');assert.equal(m.colecta.numerator,'1408');assert.equal(m.tax,null);assert.equal(m.assessment.rate.numerator,'32');assert.deepEqual(m.assessment.period,{year:'2026',from:'2026-01-01',to:'2026-12-31'});assert.match(m.assessment.classification,/artigo61\(1\)/);assert.equal(m.assessment.finalBalance.payable,null);assert.equal(m.assessment.finalBalance.recoverable,null);assert.equal(m.assessment.finalBalance.scope,'L8');});
test('missing classification and special cases do not silently inherit32percent;known upstream base remains',()=>{for(const patch of[{treatment:''},{treatment:'other'},{reference:''}]){const m=calculateNormalAssessment({...input(),...patch},fraction(4400n),context());assert.equal(m.colecta,null);assert.equal(m.taxableBase.numerator,'4400');assert.ok(m.pending.length>0);if(patch.treatment==='other')assert.equal(m.rate,null);}});
test('rate is scoped by period,residence regime and currency,not carried across years or contexts',()=>{for(const patch of[{year:'2025'},{year:'2027'},{scope:'other'},{currency:'USD'}]){const m=calculateNormalAssessment(input(),fraction(4400n),{...context(),...patch});assert.equal(m.colecta,null);if(patch.year)assert.equal(m.rate,null);}});
test('unknown base is not replaced with6700profit;known zero stays zero and fractional cents remain exact',()=>{assert.equal(calculateNormalAssessment(input(),null,context()).colecta,null);assert.equal(calculateNormalAssessment(input(),fraction(0n),context()).colecta.numerator,'0');const m=calculateNormalAssessment(input(),fraction(1n,100n),context());assert.deepEqual(m.colecta,{numerator:'2',denominator:'625'});assert.equal(m.centExact,false);assert.equal(calculateNormalAssessment(input(),fraction(-1n),context()).colecta,null);});
test('an incomplete base calculation only blocks colecta;profit and valid source amounts remain visible',()=>{const d=seed(),b=normalBook(d);b.assessment=input();b.base.benefits={state:'',amount:'',reference:''};const m=calculateNormal(d,b);assert.equal(m.taxableProfit.numerator,'6700');assert.equal(m.taxableBase,null);assert.equal(m.colecta,null);assert.equal(m.assessment.rate.numerator,'32');});
test('retentions,payments and annual credits are not subtracted or resolved by this L6 scope',()=>{const d=seed(),b=normalBook(d);b.assessment=input();const m=calculateNormal(d,b);d.annual_withholdings='999999';d.annual_payments='999999';d.annual_credits='999999';const s=JSON.parse(d.modelo_a_snapshot);for(const w of Object.values(s.irpcWithholdings??{})){w.payments=[];if(w.mrfta)w.mrfta.codes=[];}d.modelo_a_snapshot=JSON.stringify(s);const changed=calculateNormal(d,b);assert.deepEqual(changed.colecta,m.colecta);assert.deepEqual(changed.assessment.finalBalance,m.assessment.finalBalance);assert.equal(changed.tax,null);});
test('legacy3 remains current without recalculation;version4 roundtrip/history/report/dossier preserve prior books and stale guards',()=>{const d=seed(),old=normalBook(d);assert.equal(normalCurrent(d,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/3');const originalStore=JSON.parse(d.modelo_a_snapshot);const b=recordNormal(d,{...old,assessment:input()},'2026-10-25T23:30:00Z');assert.deepEqual(b.history.at(-1),old.result);assert.equal(b.result.model.version,'IRPC-normal-adjustments/4');d.modelo_a_snapshot=saveNormalBook(d,b);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,originalStore.irpcWithholdings);const back=JSON.parse(JSON.stringify(d));assert.equal(normalCurrent(back,normalBook(back)),true);const html=normalHtml(back);assert.match(html,/colecta simulada:1408/);assert.match(html,/saldo final a pagar ou recuperar:Indeterminado \(L8\)/);assert.match(html,/taxa:32%/);assert.match(JSON.stringify(createProjectDossierReport(back,{selectedSections:new Set(['finance'])})),/1408/);assert.match(JSON.stringify(normalReportRows(back)),/Saldo final — âmbito L8/);const changed=normalBook(back);changed.assessment.reference='';back.modelo_a_snapshot=saveNormalBook(back,changed);assert.equal(normalCurrent(back,normalBook(back)),false);assert.throws(()=>normalHtml(back));const again=recordNormal(back,normalBook(back));assert.equal(again.result.model.colecta,null);assert.deepEqual(again.history.at(-1),b.result);
fs.mkdirSync('outputs/irpc-l6-colecta',{recursive:true});fs.writeFileSync('outputs/irpc-l6-colecta/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-colecta/preparation.html',normalHtml(d));const md=structuredClone(d),missing=normalBook(md);missing.assessment.treatment='';md.modelo_a_snapshot=saveNormalBook(md,recordNormal(md,missing));fs.writeFileSync('outputs/irpc-l6-colecta/missing.json',JSON.stringify(md));});
test('unsupported input versions and malformed classification fail without replacing originals',()=>{const d=seed(),b=normalBook(d);b.assessment=input();b.assessment.version=2;assert.throws(()=>saveNormalBook(d,b));b.assessment.version=1;b.assessment.treatment='general20';assert.throws(()=>saveNormalBook(d,b));});
+18
View File
@@ -0,0 +1,18 @@
import test from'node:test';import assert from'node:assert/strict';import fs from'node:fs';
import{normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml}from'../../resources/js/lib/irpc-normal.ts';
import{newNormalBaseInput,newNormalLoss,calculateNormalBase}from'../../resources/js/lib/irpc-normal-base.ts';
import{fraction}from'../../resources/js/lib/fiscal-rational.ts';import{createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-profit-preserved.json',import.meta.url),'utf8'));
const loss=(id,year,assessed,used,requested)=>({...newNormalLoss(id),year,subjectNuit:'123456789',assessed,previouslyUsed:used,requested,reference:'QA saldo fiscal corrigido,sujeito/exercício '+year,kind:'ordinary',annualPeriods:'yes',generalActivity:'yes',balanceConfirmed:'yes'});
const input=()=>({...newNormalBaseInput(),subjectNuit:'123456789',identityReference:'QA sujeito do lucro e dos saldos',method:'direct',methodReference:'QA declaração e contabilidade organizada',lossesState:'known',lossesReference:'QA inventário completo dos saldos fiscais do sujeito',unchangedObject:'yes',objectReference:'QA actividade e objecto mantidos ao termo2026',losses:[loss('old','2021','1000','200','800'),loss('new','2024','3000','500','1500')],benefits:{state:'known',amount:'0',reference:'QA deduções/benefícios à base,valor conhecido zero'}});
test('6700 is reused as profit;2300 documented loss use yields4400 taxable base,never tax payable',()=>{const d=seed(),b=normalBook(d);b.base=input();const m=calculateNormal(d,b);assert.equal(m.taxableProfit.numerator,'6700');assert.equal(m.base.requestedLosses.numerator,'2300');assert.equal(m.base.deductedLosses.numerator,'2300');assert.equal(m.taxableBase.numerator,'4400');assert.equal(m.tax,null);assert.deepEqual(m.base.losses.map(l=>l.available.numerator),['800','2500']);assert.deepEqual(m.base.losses.map(l=>l.remaining.numerator),['0','1000']);assert.deepEqual(m.base.losses.map(l=>l.lastOrdinaryYear),[2026,2029]);});
test('supported absence of losses and benefits retains6700 base;missing benefit information does not mean zero',()=>{const i={...input(),lossesState:'absent',losses:[],unchangedObject:'',objectReference:'',benefits:{state:'absent',amount:'',reference:'QA sem benefício aplicável'}};let m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.taxableBase.numerator,'6700');i.benefits={state:'',amount:'',reference:''};m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.taxableBase,null);assert.equal(m.taxableProfit.numerator,'6700');});
test('expired ordinary2020 loss cannot be deducted;2021 remains within last ordinary year2026',()=>{const i=input();i.losses=[loss('expired','2020','500','0','0')];i.unchangedObject='';i.objectReference='';let m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.losses[0].status,'excluded');assert.equal(m.losses[0].lastOrdinaryYear,2025);assert.equal(m.taxableBase.numerator,'6700');i.losses[0].requested='1';assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);});
test('losses cannot exceed available balance or profit;no implicit allocation order or percentage cap',()=>{const i=input();i.losses[0].requested='801';assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);i.losses=[loss('a','2021','5000','0','5000'),loss('b','2024','5000','0','4000')];let m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.taxableBase,null);assert.match(m.pending.join(' '),/reparta explicitamente/);i.losses[0].requested='2700';m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.taxableBase.numerator,'0');assert.equal(m.deductedLosses.numerator,'6700');});
test('same taxpayer,annual periods,ordinary origin,reconciled balance and activity facts are specific dependencies',()=>{for(const patch of [{subjectNuit:'987654321'},{annualPeriods:'no'},{generalActivity:'no'},{balanceConfirmed:''},{kind:'transfer'},{year:'2026'},{previouslyUsed:'1100'},{requested:''}]){const i=input();Object.assign(i.losses[0],patch);assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);}});
test('changed object explicitly excludes deduction;duplicate annual balances and indirect methods remain pending',()=>{const i=input();i.unchangedObject='no';i.losses.forEach(l=>l.requested='0');let m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.taxableBase.numerator,'6700');assert.ok(m.losses.every(l=>l.status==='excluded'));i.unchangedObject='yes';i.losses[1].year='2021';assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);i.losses[1].year='2024';i.method='indirect';assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);});
test('positive known benefits are preserved without free subtraction;absence contradictions remain explicit',()=>{const i=input();i.benefits={state:'known',amount:'200',reference:'QA benefício positivo,regra por corresponder'};let m=calculateNormalBase(i,fraction(6700n),'2026');assert.equal(m.input.benefits.amount,'200');assert.equal(m.taxableBase,null);i.benefits={state:'absent',amount:'200',reference:'QA ausência contraditória'};assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);i.benefits={state:'known',amount:'0',reference:'QA zero conhecido'};i.lossesState='absent';assert.equal(calculateNormalBase(i,fraction(6700n),'2026').taxableBase,null);});
test('unknown or negative current profit is preserved,not transformed to zero or current-year loss carryforward',()=>{for(const p of [null,fraction(-100n)]){const m=calculateNormalBase(input(),p,'2026');assert.equal(m.taxableBase,null);assert.deepEqual(m.taxableProfit,p);}});
test('legacy current result and all books survive;new3 history,save/recovery/dossier and repeated simulation do not consume official balances',()=>{const d=seed(),legacy=normalBook(d);assert.equal(normalCurrent(d,legacy),true);assert.equal(legacy.result.model.version,'IRPC-normal-adjustments/2');const b={...legacy,base:input()},before=JSON.parse(d.modelo_a_snapshot),saved=recordNormal(d,b,'2026-10-25T23:00:00Z');assert.deepEqual(saved.history.at(-1),legacy.result);assert.equal(saved.result.model.version,'IRPC-normal-adjustments/3');d.modelo_a_snapshot=saveNormalBook(d,saved);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);const back=JSON.parse(JSON.stringify(d));assert.equal(normalCurrent(back,normalBook(back)),true);assert.match(normalHtml(back),/matéria colectável simulada:4400/);assert.match(normalHtml(back),/imposto a pagar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(back,{selectedSections:new Set(['finance'])})),/4400/);const repeated=recordNormal(back,normalBook(back));assert.equal(repeated.result.model.taxableBase.numerator,'4400');assert.equal(repeated.base.losses[1].previouslyUsed,'500');assert.equal(repeated.base.losses[1].requested,'1500');const changed=normalBook(back);changed.base.losses[0].requested='700';back.modelo_a_snapshot=saveNormalBook(back,changed);assert.equal(normalCurrent(back,normalBook(back)),false);assert.throws(()=>normalHtml(back));
fs.mkdirSync('outputs/irpc-l6-base',{recursive:true});fs.writeFileSync('outputs/irpc-l6-base/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-base/preparation.html',normalHtml(d));const missing=normalBook(d);missing.base.benefits={state:'',amount:'',reference:''};const md=structuredClone(d);md.modelo_a_snapshot=saveNormalBook(md,recordNormal(md,missing));fs.writeFileSync('outputs/irpc-l6-base/missing.json',JSON.stringify(md));});
test('malformed future input versions and duplicate IDs fail without resetting originals',()=>{const d=seed(),b=normalBook(d);b.base=input();b.base.version=2;assert.throws(()=>saveNormalBook(d,b));b.base.version=1;b.base.losses[1].id=b.base.losses[0].id;assert.throws(()=>saveNormalBook(d,b));});
+40
View File
@@ -0,0 +1,40 @@
import{calculateNormalBase}from'../../resources/js/lib/irpc-normal-base.ts';import{fraction}from'../../resources/js/lib/fiscal-rational.ts';
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import{normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalReportRows,normalFinancialSource}from'../../resources/js/lib/irpc-normal.ts';
import{newNormalCosts,newCostRow,calculateNormalCosts,reusableCreditBalance,costRules,normalCostReport}from'../../resources/js/lib/irpc-normal-costs.ts';
import{createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-schedules-preserved.json',import.meta.url),'utf8'));const val=f=>f?Number(f.numerator)/Number(f.denominator):null;
function row(id,rule,amount,facts={},sourceCode='operating_expenses'){return{...newCostRow(id),rule,amount,sourceCode,document:'QA-'+id,line:'1',reference:'QA documento e factos qualificados para o período anual2026; valores sintéticos',conditions:'yes',facts};}
function reconcile(b){const totals={};for(const r of b.schedules.rows){const k={tax_own:'expenseCorrections',tax_other:'expenseCorrections',undocumented:'expenseCorrections',capital_in:'positivePatrimonial',patrimonial_gain:'positivePatrimonial',capital_out:'negativePatrimonial',patrimonial_cost:'negativePatrimonial',future_cost:'timing',future_income:'timing',current_cost:'timing',current_income:'timing'}[r.rule];totals[k]=(totals[k]??0)+Number(r.amount);}for(const r of b.costs.rows){const k=costRules[r.rule][1];totals[k]=(totals[k]??0)+Number(r.amount);}for(const[k,a]of Object.entries(totals))b.profit.topics[k]={state:'known',amount:String(a),reference:'QA reconciliação aritmética por documento/linha dos mapas'};}
function setup(rows){const d=seed(),b=normalBook(d);b.costs=newNormalCosts();b.costs.rows=rows;b.costs.advertisingReference='QA publicidade completa e vendas normais12000 integralmente reconciliadas';b.costs.salesOrdinary='yes';b.costs.creditRegime='ordinary';b.costs.creditBase='10000';b.costs.creditPrevious='550';b.costs.creditReference='QA créditos ordinários finais10000 e provisão fiscal anterior550 documentados, sem regime financeiro';reconcile(b);return{d,b};}
function joint(){return setup([
row('allowance','allowance','100',{billed:'20',taxedNotBilled:'20'}),row('representation','representation','50'),row('advertising','advertising','80',{campaign:'no'}),row('credit','credit','200'),
row('stock','stock','60',{balanceKey:'inventory-A',lossObserved:'yes',observedLoss:'40',previous:'10'}),row('litigation','litigation','90',{balanceKey:'court-A',caseOngoing:'yes',chargeDeductible:'yes',obligation:'100',previous:'20'}),row('fuel','fuel','50',{supportedNormal:'40',passenger:'no'}),row('prior','prior_cost','80',{originalYear:'2025',unknownAtClose:'no',closingEvidence:'QA factura conhecida no fecho2025'}),
row('work','work','1000',{projectKey:'project-A',method:'percentage',price:'2000',totalCost:'1000',incurred:'500',priorRevenue:'600',priorCost:'300',bookCost:'200',costSource:'cost_of_sales',partialBilling:'yes',ownFractionalDelivery:'no',consistentMethod:'yes',deferredReceipt:'0'},'revenue'),
]);}
function model(r){const{d,b}=setup([r]);return calculateNormal(d,b);}
const item=m=>m.costs.rows[0];
test('compatible costs/provisions/prior timing/work produce exact profit6730 base4430 colecta1417.60 from existing6240 result',()=>{const{d,b}=joint(),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.schedules.net),520);assert.equal(val(m.costs.net),-250);assert.equal(val(m.taxableProfit),6730);assert.equal(val(m.taxableBase),4430);assert.equal(val(m.colecta),1417.6);assert.equal(m.version,'IRPC-normal-adjustments/6');assert.equal(m.tax,null);assert.equal(m.assessment.finalBalance.payable,null);assert.deepEqual(m.costs.rows.map(r=>val(r.adjustment)),[30,40,0,150,30,10,10,80,-600]);assert.equal(m.costs.pending.length,0);});
test('allowance exceptions are disjoint amounts; accepted70 plus addback30; known zero differs from missing',()=>{let m=model(row('e','allowance','100',{billed:'20',taxedNotBilled:'20'}));assert.equal(val(item(m).accepted),70);assert.equal(val(item(m).adjustment),30);for(const facts of[{billed:'0',taxedNotBilled:'0'},{billed:'100',taxedNotBilled:'0'}])assert.notEqual(model(row('e','allowance','100',facts)).taxableProfit,null);for(const facts of[{billed:'',taxedNotBilled:'0'},{billed:'60',taxedNotBilled:'50'}])assert.equal(model(row('e','allowance','100',facts)).taxableProfit,null);});
test('representation20percent accepted; fractional cents remain exact without fiscal rounding or autonomous tax',()=>{const m=model(row('f','representation','0.01'));assert.deepEqual(item(m).accepted,{numerator:'1',denominator:'500'});assert.deepEqual(item(m).adjustment,{numerator:'1',denominator:'125'});assert.equal(m.tax,null);assert.equal(m.assessment.finalBalance.scope,'L8');});
test('advertising shares one annual1percent cap across documents and has no per-invoice duplicate allowance',()=>{const{d,b}=setup([row('n1','advertising','100',{campaign:'no'}),row('n2','advertising','100',{campaign:'no'})]);let m=calculateNormal(d,b);assert.deepEqual(m.costs.rows.map(r=>val(r.accepted)),[60,60]);assert.equal(val(m.costs.net),80);assert.deepEqual(m.costs.rows.map(r=>val(r.limit)),[120,120]);b.costs.rows.reverse();assert.equal(val(calculateNormal(d,b).costs.net),80);b.costs.salesOrdinary='';m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.schedules.net.numerator,'520');const c=setup([row('ad-a','advertising','100',{campaign:'no'}),row('ad-b','advertising','250',{campaign:'no'})]);const f=calculateNormal(c.d,c.b);assert.match(JSON.stringify(normalCostReport(f.costs)),/exacto 240\/7/);});
test('campaign36A never silently receives ordinary advertising treatment',()=>{const m=model(row('campaign','advertising','100',{campaign:'yes'}));assert.equal(m.taxableProfit,null);assert.equal(item(m).accepted,null);assert.match(item(m).reason,/36-A/);});
test('vehicle costs distinguish ordinary passenger50percent, documented public/rental exception and nonpassenger',()=>{for(const[facts,accepted]of[[{passenger:'yes',publicOrRental:'no'},50],[{passenger:'yes',publicOrRental:'yes'},100],[{passenger:'no'},100]])assert.equal(val(item(model(row('v','vehicle','100',facts))).accepted),accepted);assert.equal(model(row('v','vehicle','100',{passenger:'yes'})).taxableProfit,null);});
test('fuel and rent cover independent rules; overlapping36h-i and36-4 stays pending without an invented combination',()=>{let m=model(row('fuel','fuel','100',{supportedNormal:'80',passenger:'yes',publicOrRental:'no'}));assert.equal(m.taxableProfit,null);assert.match(item(m).reason,/Articulação/);m=model(row('fuel','fuel','100',{supportedNormal:'100',passenger:'yes',publicOrRental:'no'}));assert.equal(val(item(m).accepted),50);m=model(row('rent','rental','100',{nonAcceptedQuota:'20',quotaReference:'QA quota36h documentada; não calculada automaticamente',passenger:'yes',publicOrRental:'yes'}));assert.equal(val(item(m).accepted),80);assert.equal(val(item(m).adjustment),20);assert.match(item(m).reason,/não apura automaticamente/);assert.equal(model(row('fuel','fuel','100',{supportedNormal:'101',passenger:'no'})).taxableProfit,null);assert.equal(model(row('rent','rental','100',{nonAcceptedQuota:'',quotaReference:'QA',passenger:'yes',publicOrRental:'no'})).taxableProfit,null);});
test('credit provision caps both annual1.5percent and cumulative6percent; multiple rows share headroom and do not consume source balance',()=>{const{d,b}=setup([row('c1','credit','100'),row('c2','credit','100')]);let m=calculateNormal(d,b);assert.deepEqual(m.costs.rows.map(r=>val(r.accepted)),[25,25]);assert.equal(val(m.costs.net),150);assert.equal(b.costs.creditPrevious,'550');b.costs.creditPrevious='0';m=calculateNormal(d,b);assert.equal(val(m.costs.net),50);b.costs.creditPrevious='600';assert.equal(val(calculateNormal(d,b).costs.net),200);b.costs.creditPrevious='601';assert.equal(calculateNormal(d,b).taxableProfit,null);b.costs.creditRegime='regulated';assert.equal(calculateNormal(d,b).taxableProfit,null);b.costs.creditRegime='ordinary';b.costs.creditPrevious='';assert.equal(calculateNormal(d,b).taxableProfit,null);});
test('inventory allowance respects observed loss less existing fiscal provision; absent observation is known rejection, not unknown zero',()=>{let m=model(row('s','stock','60',{balanceKey:'stock',lossObserved:'yes',observedLoss:'40',previous:'10'}));assert.equal(val(item(m).accepted),30);assert.equal(val(item(m).adjustment),30);m=model(row('s','stock','60',{lossObserved:'no'}));assert.equal(val(item(m).accepted),0);assert.equal(val(item(m).adjustment),60);m=model(row('s','stock','60',{balanceKey:'stock',lossObserved:'yes',observedLoss:'40',previous:''}));assert.equal(m.taxableProfit,null);});
test('litigation needs current court process and deductible underlying charge; known failure adds back full amount',()=>{for(const facts of[{caseOngoing:'no'},{caseOngoing:'yes',chargeDeductible:'no'}]){const m=model(row('l','litigation','90',facts));assert.equal(val(item(m).accepted),0);assert.equal(val(item(m).adjustment),90);}const m=model(row('l','litigation','90',{caseOngoing:'yes',chargeDeductible:'yes',balanceKey:'court',obligation:'100',previous:'20'}));assert.equal(val(item(m).accepted),80);assert.equal(model(row('l','litigation','90',{caseOngoing:'yes',chargeDeductible:''})).taxableProfit,null);});
test('reversal before deducted is taxed once; taxed-provision reversal is excluded only with prior fiscal evidence',()=>{let m=model(row('r','provision_reversal','50',{balanceKey:'old',previous:'70',eventEnded:'yes',inResult:'yes'},'revenue'));assert.equal(val(item(m).adjustment),0);m=model(row('r','provision_reversal','50',{balanceKey:'old',previous:'70',eventEnded:'yes',inResult:'no'},''));assert.equal(val(item(m).adjustment),50);m=model(row('r','taxed_reversal','50',{balanceKey:'old',previous:'70',eventEnded:'yes',inResult:'yes'},'revenue'));assert.equal(val(item(m).adjustment),-50);assert.equal(model(row('r','taxed_reversal','80',{balanceKey:'old',previous:'70',eventEnded:'yes',inResult:'yes'},'revenue')).taxableProfit,null);});
test('same fiscal object cannot reuse stock/litigation/reversal balance with another document',()=>{const{d,b}=setup([row('s1','stock','50',{balanceKey:'same',lossObserved:'yes',observedLoss:'100',previous:'0'}),row('s2','stock','50',{balanceKey:'same',lossObserved:'yes',observedLoss:'100',previous:'0'})]);const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.ok(m.costs.rows.every(r=>r.adjustment===null));});
test('prior-period cost and income distinguish knowledge at previous closing; never edit earlier returns or recorded histories',()=>{for(const[rule,src,unknown,expected]of[['prior_cost','operating_expenses','yes',0],['prior_cost','operating_expenses','no',100],['prior_income','revenue','yes',0],['prior_income','revenue','no',-100]]){const m=model(row('p',rule,'100',{originalYear:'2025',unknownAtClose:unknown,closingEvidence:'QA fecho anterior'},src));assert.equal(val(item(m).adjustment),expected);}assert.equal(model(row('p','prior_cost','100',{originalYear:'2025',unknownAtClose:'',closingEvidence:'QA'})).taxableProfit,null);});
test('work percentage uses accumulated performance less prior recognized revenue and cost, no double recognition',()=>{const{d,b}=joint(),m=calculateNormal(d,b),w=m.costs.rows.at(-1);assert.equal(val(w.limit),400);assert.equal(val(w.accepted),200);assert.equal(val(w.adjustment),-600);assert.match(w.reason,/grau=incorrido/);assert.equal(b.costs.rows.at(-1).facts.priorRevenue,'600');});
test('closing work respects mandatory percentage,95percent threshold and missing facts; estimated loss is separate full addback',()=>{const{d,b}=joint(),w=b.costs.rows.at(-1);w.facts.method='closing';assert.equal(calculateNormal(d,b).taxableProfit,null);w.facts.partialBilling='no';w.facts.priorRevenue='0';w.facts.priorCost='0';w.facts.publicReception='no';let m=calculateNormal(d,b);assert.equal(val(m.costs.rows.at(-1).limit),0);w.facts.incurred='950';m=calculateNormal(d,b);assert.equal(val(m.costs.rows.at(-1).limit),2000);w.facts.deferredReceipt='1';assert.equal(calculateNormal(d,b).taxableProfit,null);assert.equal(val(item(model(row('estimate','estimated_work_loss','100'))).adjustment),100);});
test('cross-map duplicate identity and cumulative source over-allocation block only dependent totals',()=>{const{d,b}=joint();b.costs.rows[0].document=b.rows[0].document+' ';b.costs.rows[0].line=b.rows[0].line;let m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.match(m.costs.rows[0].reason,/repetidos/);assert.equal(m.costs.rows[0].adjustment,null);assert.equal(val(m.costs.rows[1].adjustment),40);const c=joint();c.b.costs.rows[0].amount='1000';reconcile(c.b);m=calculateNormal(c.d,c.b);assert.equal(m.taxableProfit,null);assert.match(m.costs.pending.join(' '),/excedem a linha/);});
test('missing one credit parameter retains known independent corrections and makes downstream totals unknown; upstream records remain',()=>{const{d,b}=joint();b.costs.creditPrevious='';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.taxableBase,null);assert.equal(m.colecta,null);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.costs.rows[1].adjustment),40);assert.equal(val(m.schedules.net),520);assert.equal(m.schedules.pending.length,0);});
test('credit reuse requires known financial closing balances; absent schedule never creates zero',()=>{assert.equal(reusableCreditBalance(null),null);assert.equal(reusableCreditBalance({cashSchedule:[{salesReceivablesBalance:null,openingReceivablesBalance:'0'}]}),null);assert.equal(val(reusableCreditBalance({cashSchedule:[{salesReceivablesBalance:'100',openingReceivablesBalance:'20'}]})),120);});
test('installed5 stays current without recalculation; version6 API-ready snapshots preserve books/history and export exact rules/amounts',()=>{const old=seed(),before=normalBook(old);assert.equal(normalCurrent(old,before),true);assert.equal(before.result.model.version,'IRPC-normal-adjustments/5');const{d,b}=joint(),store=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T00:30:00+02:00'));const back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),before.result);const saved=JSON.parse(d.modelo_a_snapshot);assert.deepEqual(saved.books,store.books);assert.deepEqual(saved.irpcWithholdings,store.irpcWithholdings);assert.match(normalHtml(d),/1417.60/);assert.match(normalHtml(d),/parcela aceite/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/36\(1\)n/);assert.match(JSON.stringify(normalReportRows(d)),/M20 — correspondência/);fs.mkdirSync('outputs/irpc-l6-costs',{recursive:true});fs.writeFileSync('outputs/irpc-l6-costs/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-costs/preparation.html',normalHtml(d));const missing=normalBook(d);missing.costs.creditPrevious='';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,missing));fs.writeFileSync('outputs/irpc-l6-costs/missing.json',JSON.stringify(d));const changed=normalBook(d);changed.costs.rows[0].facts.billed='30';d.modelo_a_snapshot=saveNormalBook(d,changed);assert.equal(normalCurrent(d,normalBook(d)),false);assert.throws(()=>normalHtml(d));});
test('malformed versions, unknown rules and non-string facts are refused with saved originals intact',()=>{const{d,b}=joint();for(const change of[x=>x.version=2,x=>x.rows[0].rule='__proto__',x=>x.rows[0].facts.billed=20]){const c=structuredClone(b);change(c.costs);assert.throws(()=>saveNormalBook(d,c));}assert.equal(normalCurrent(d,normalBook(d)),true);});
test('additional categories,shared limits and closing method generate separate recoverable compatible cases',()=>{const cases={additional:setup([row('vehicle','vehicle','100',{passenger:'yes',publicOrRental:'no'}),row('rental','rental','100',{nonAcceptedQuota:'20',quotaReference:'QA quota36h documentada',passenger:'yes',publicOrRental:'yes'}),row('estimate','estimated_work_loss','50'),row('reverse','provision_reversal','50',{balanceKey:'provision-old-deducted',previous:'50',eventEnded:'yes',inResult:'no'},''),row('taxed-reverse','taxed_reversal','50',{balanceKey:'provision-old-taxed',previous:'50',eventEnded:'yes',inResult:'yes'},'revenue'),row('prior-income','prior_income','50',{originalYear:'2025',unknownAtClose:'no',closingEvidence:'QA receita conhecida no encerramento anterior'},'revenue')]),limits:setup([row('ad1','advertising','100',{campaign:'no'}),row('ad2','advertising','100',{campaign:'no'}),row('cr1','credit','100'),row('cr2','credit','100')]),closing:joint()};const w=cases.closing.b.costs.rows.at(-1);Object.assign(w.facts,{method:'closing',partialBilling:'no',priorRevenue:'0',priorCost:'0',publicReception:'no'});const expected={additional:[7050,4750,1520],limits:[7210,4910,1571.2],closing:[6530,4230,1353.6]};for(const[name,{d,b}]of Object.entries(cases)){const m=calculateNormal(d,b);assert.deepEqual([val(m.taxableProfit),val(m.taxableBase),val(m.colecta)],expected[name]);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.writeFileSync('outputs/irpc-l6-costs/'+name+'.json',JSON.stringify(d));}});
test('percentage-generated loss below one cent remains a negative fiscal result, never a silently truncated zero',()=>{const{b}=joint();b.base.lossesState='absent';b.base.losses=[];assert.equal(calculateNormalBase(b.base,fraction(-1n,1000n),'2026').taxableBase,null);assert.equal(val(calculateNormalBase(b.base,fraction(-1n,1000n),'2026','QA perda corrente exacta').taxableBase),0);assert.equal(val(calculateNormalBase(b.base,fraction(1n,1000n),'2026').taxableBase),0.001);});
@@ -0,0 +1,26 @@
import test from'node:test';import assert from'node:assert/strict';import fs from'node:fs';
import{normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalReportRows,normalFinancialSource}from'../../resources/js/lib/irpc-normal.ts';
import{newNormalDepreciation,newDepreciationAsset,reuseNormalAssets,calculateNormalDepreciation,depreciationCatalog}from'../../resources/js/lib/irpc-normal-depreciation.ts';
import{createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-costs-preserved.json',import.meta.url),'utf8')),val=f=>f?Number(f.numerator)/Number(f.denominator):null;
function asset(p={}){return{...newDepreciationAsset('server'),assetKey:'asset-server',description:'Servidor QA',document:'QA-activo-servidor',line:'1',category:'server',valuation:'historical_new',specificRate:'no',method:'constant',rate:'20',timing:'annual',start:'2026-01-01',exitState:'absent',cost:'500',residual:'0',accounting:'120',previousAccounting:'0',previousFiscal:'0',priorAvailable:'0',previousReference:'QA ausência de quotas anteriores demonstrada,ou saldos anteriores conforme documentos',consistentMethod:'yes',conditions:'yes',reference:'QA factura/classificação/base histórica/período documentados,sem reavaliações ou regimes especiais',...p};}
function setup(a=asset(),bookExpense=a.accounting){const d=seed(),b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation=bookExpense;d.business_plan_snapshot=JSON.stringify(p);b.depreciation={...newNormalDepreciation(),completenessReference:'QA todos activos/quotas e plano reconciliados no sujeito/exercício2026',assets:[a]};return{d,b};}
const model=(a,expense)=>{const{d,b}=setup(a,expense);return calculateNormal(d,b);};
test('version7 reuses existing server and replaces20 legacy adjustment exactly once without changing financial result or old maps',()=>{const{d,b}=setup(),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.depreciation.adjustment),20);assert.equal(val(m.taxableProfit),6730);assert.equal(val(m.taxableBase),4430);assert.equal(val(m.colecta),1417.6);assert.equal(m.version,'IRPC-normal-adjustments/7');assert.equal(val(m.schedules.net),520);assert.deepEqual(m.schedules.assets,[]);assert.deepEqual(m.schedules.input.assets,b.schedules.assets);assert.equal(m.tax,null);});
test('documented first-year duodecimos include July and exclude month of exit',()=>{let m=model(asset({start:'2026-07-15',timing:'months'}));assert.equal(m.depreciation.assets[0].months,6);assert.equal(val(m.depreciation.assets[0].deduction),50);assert.equal(val(m.taxableProfit),6780);assert.equal(val(m.taxableBase),4480);assert.equal(val(m.colecta),1433.6);m=model(asset({start:'2026-07-15',timing:'months',exitState:'known',exitDate:'2026-10-15'}));assert.equal(m.depreciation.assets[0].months,3);assert.equal(val(m.depreciation.assets[0].deduction),25);assert.equal(val(m.colecta),1441.6);});
test('annual first-year option remains distinct from duodecimos; contradictory dates and unknown event are not defaulted',()=>{assert.equal(val(model(asset({start:'2026-07-15'})).depreciation.assets[0].deduction),100);for(const p of[{exitState:''},{exitState:'known',exitDate:''},{exitState:'absent',exitDate:'2026-10-15'},{start:'2026-02-31'},{exitState:'known',exitDate:'2025-12-31'}])assert.equal(model(asset(p)).colecta,null);});
test('declining method uses fiscal remaining basis and coefficient capped by minimum life; result differs from accounting depreciation',()=>{const m=model(asset({method:'declining',factor:'2',newTangible:'yes',switchToStraight:'no',accounting:'240'}));assert.equal(val(m.depreciation.assets[0].quota),200);assert.equal(val(m.depreciation.assets[0].deduction),200);assert.equal(val(m.depreciation.adjustment),40);assert.equal(val(m.taxableProfit),6630);assert.equal(val(m.taxableBase),4330);assert.equal(val(m.colecta),1385.6);for(const p of[{factor:'2.1'},{newTangible:'no'},{category:'furniture',factor:'2'},{category:'commercial_building',factor:'2'}])assert.equal(model(asset({method:'declining',factor:'2',newTangible:'yes',switchToStraight:'no',...p})).colecta,null);});
test('coefficient bands1.5/2/2.5 and half-to-maximum constant-rate recorte are applied by catalog life',()=>{const c=model(asset({category:'computer',method:'declining',factor:'1.5',newTangible:'yes',switchToStraight:'no',accounting:'200'}));assert.equal(val(c.depreciation.assets[0].quota),187.5);assert.equal(model(asset({category:'computer',method:'declining',factor:'2',newTangible:'yes',switchToStraight:'no'})).colecta,null);const m=model(asset({category:'laundry_machine',specificRate:'yes',method:'declining',factor:'2.5',newTangible:'yes',switchToStraight:'no',accounting:'200'}));assert.equal(val(m.depreciation.assets[0].quota),156.25);assert.equal(val(model(asset({rate:'10'})).depreciation.assets[0].deduction),50);assert.equal(model(asset({rate:'9.99'})).colecta,null);});
test('previous taxable accounting quotas are only recovered within current quota and documented available balance',()=>{const m=model(asset({start:'2025-01-01',accounting:'80',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'}));assert.equal(val(m.depreciation.assets[0].deduction),100);assert.equal(val(m.depreciation.assets[0].priorUsed),20);assert.equal(val(m.depreciation.assets[0].priorRemaining),20);assert.equal(val(m.depreciation.adjustment),-20);assert.equal(val(m.taxableProfit),6730);assert.equal(m.depreciation.input.assets[0].priorAvailable,'40');for(const p of[{priorAvailable:''},{priorAvailable:'41'},{previousFiscal:'121'},{previousReference:''}])assert.equal(model(asset({start:'2025-01-01',accounting:'80',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40',...p})).colecta,null);});
test('unrecorded quotas are not invented as a deduction; zero previous balance remains explicit',()=>{const m=model(asset({accounting:'0'}));assert.equal(val(m.depreciation.assets[0].quota),100);assert.equal(val(m.depreciation.assets[0].deduction),0);assert.equal(val(m.depreciation.adjustment),0);assert.equal(model(asset({previousAccounting:''})).colecta,null);});
test('article8-2 option requires documented past condition and remaining minimum life,not an automatic switch',()=>{const a=asset({start:'2022-01-01',method:'declining',factor:'2',newTangible:'yes',switchToStraight:'yes',priorFullQuotas:'yes',previousAccounting:'500',previousFiscal:'420',priorAvailable:'80',accounting:'0'}),m=model(a);assert.equal(val(m.depreciation.assets[0].quota),80);assert.equal(val(m.depreciation.assets[0].deduction),80);assert.equal(val(m.depreciation.assets[0].priorUsed),80);assert.equal(model({...a,priorFullQuotas:''}).colecta,null);assert.equal(model({...a,priorFullQuotas:'no'}).colecta,null);});
test('specific tableI prevails over generic tableII and known codes are never replaced with a free rate',()=>{const m=model(asset({category:'hotel_decoration',specificRate:'yes',rate:'20'}));assert.equal(m.depreciation.assets[0].catalog.table,'I');assert.equal(m.depreciation.assets[0].catalog.code,'84.2/B');assert.equal(val(m.depreciation.assets[0].quota),100);assert.equal(model(asset({specificRate:'yes'})).colecta,null);assert.equal(model(asset({category:'other'})).colecta,null);assert.equal(model(asset({category:'hotel_decoration',specificRate:'no'})).colecta,null);assert.equal(model(asset({valuation:'used'})).colecta,null);assert.equal(model(asset({valuation:''})).colecta,null);});
test('commercial building excludes separately known land; zero land is not presumed when missing',()=>{const m=model(asset({category:'commercial_building',rate:'2',cost:'1000',land:'200',accounting:'30'}));assert.equal(val(m.depreciation.assets[0].basis),800);assert.equal(val(m.depreciation.assets[0].deduction),16);assert.equal(val(m.depreciation.adjustment),14);assert.equal(model(asset({category:'commercial_building',rate:'2',land:''})).colecta,null);});
test('vehicle ceiling800000 and non-eligible accounting share are separate from prior recoverable quotas',()=>{let m=model(asset({category:'passenger_vehicle',rate:'25',vehicleException:'no',cost:'900000',accounting:'225000'}));assert.equal(val(m.depreciation.assets[0].taxBasis),800000);assert.equal(val(m.depreciation.assets[0].deduction),200000);assert.equal(val(m.depreciation.adjustment),25000);m=model(asset({category:'passenger_vehicle',rate:'25',vehicleException:'no',cost:'900000',accounting:'9000'}));assert.equal(val(m.depreciation.assets[0].eligibleCurrent),8000);assert.equal(val(m.depreciation.assets[0].deduction),8000);assert.equal(model(asset({category:'passenger_vehicle',rate:'25',vehicleException:''})).colecta,null);assert.equal(model(asset({category:'passenger_vehicle',rate:'25',vehicleException:'no',cost:'900000',start:'2025-01-01',previousAccounting:'225000',previousFiscal:'200000',priorAvailable:'25000'})).colecta,null);});
test('low value is an explicit individual-asset option,not automatic just because cost is below20000',()=>{const m=model(asset({method:'low_value',individual:'yes',accounting:'500'}));assert.equal(val(m.depreciation.assets[0].deduction),500);assert.equal(val(m.colecta),1289.6);assert.equal(model(asset({method:'low_value',individual:'no'})).colecta,null);assert.equal(model(asset({category:'other',method:'low_value',individual:'yes'})).colecta,null);assert.equal(model(asset({method:'low_value',individual:'yes',cost:'20000.01'})).colecta,null);});
test('software conditions and explicit nondepreciable category have distinct outcomes',()=>{assert.equal(model(asset({category:'software',rate:'25',limitedRights:''})).colecta,null);assert.equal(val(model(asset({category:'software',rate:'25',limitedRights:'yes'})).depreciation.assets[0].deduction),120);const m=model(asset({category:'nondepreciable',start:'',method:'',previousAccounting:''}));assert.equal(val(m.depreciation.assets[0].deduction),0);assert.equal(val(m.depreciation.adjustment),120);});
test('maximum life exhaustion excludes current quota; first-use future period is known nondeduction',()=>{const m=model(asset({start:'2014-01-01',previousAccounting:'400',previousFiscal:'400',priorAvailable:'0'}));assert.equal(val(m.depreciation.assets[0].deduction),0);assert.match(m.depreciation.assets[0].reason,/vida útil máxima esgotada/);assert.equal(val(model(asset({start:'2027-01-01'})).depreciation.assets[0].deduction),0);assert.equal(model(asset({category:'computer',rate:'25',start:'2018-07-01'})).colecta,null);const clipped=model(asset({category:'computer',rate:'25',start:'2018-07-01',timing:'months'}));assert.equal(clipped.depreciation.assets[0].months,6);});
test('duplicate asset/document and unreconciled accounting cannot create a second depreciation adjustment',()=>{const{d,b}=setup();b.depreciation.assets.push({...asset({id:'second'}),accounting:'0'});assert.equal(calculateNormal(d,b).colecta,null);const c=setup();c.b.depreciation.assets[0].document=c.b.rows[0].document;c.b.depreciation.assets[0].line=c.b.rows[0].line;assert.equal(calculateNormal(c.d,c.b).colecta,null);assert.equal(model(asset(),'121').colecta,null);});
test('one unknown asset keeps independent quotas visible but total profit/base/colecta dependent on reconciliation remains unknown',()=>{const{d,b}=setup(asset({accounting:'60'}),'120');b.depreciation.assets.push(asset({id:'second',assetKey:'second',document:'QA-second',category:'other',accounting:'60'}));const m=calculateNormal(d,b);assert.equal(val(m.depreciation.assets[0].deduction),60);assert.equal(m.depreciation.assets[1].deduction,null);assert.equal(m.taxableProfit,null);assert.equal(m.taxableBase,null);assert.equal(m.colecta,null);});
test('reuse copies declared values without inventing previous accounting/carry data; installed6 stays current and histories survive7',()=>{const d=seed(),old=normalBook(d);assert.equal(normalCurrent(d,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/6');const reused=reuseNormalAssets(old.schedules.assets);assert.equal(reused.assets[0].cost,old.schedules.assets[0].cost);assert.equal(reused.assets[0].previousAccounting,'');assert.equal(reused.assets[0].priorAvailable,'');const{d:next,b}=setup(asset({start:'2026-07-15',timing:'months'})),before=JSON.parse(next.modelo_a_snapshot);const oldAssets=structuredClone(b.schedules.assets);next.modelo_a_snapshot=saveNormalBook(next,recordNormal(next,b,'2026-10-26T10:00:00+02:00'));const back=normalBook(JSON.parse(JSON.stringify(next)));assert.equal(normalCurrent(next,back),true);assert.deepEqual(back.history.at(-1),old.result);assert.deepEqual(back.schedules.assets,oldAssets);assert.deepEqual(JSON.parse(next.modelo_a_snapshot).books,before.books);assert.deepEqual(JSON.parse(next.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);assert.match(normalHtml(next),/1433.60/);assert.match(JSON.stringify(createProjectDossierReport(next,{selectedSections:new Set(['finance'])})),/quota anterior/);const changed=structuredClone(back);changed.depreciation.assets[0].priorAvailable='1';next.modelo_a_snapshot=saveNormalBook(next,changed);assert.equal(normalCurrent(next,normalBook(next)),false);assert.throws(()=>normalHtml(next));});
test('recoverable scenarios generate five API/report proofs without recalculating accepted predecessor suites',()=>{const cases={months:setup(asset({start:'2026-07-15',timing:'months'})),declining:setup(asset({method:'declining',factor:'2',newTangible:'yes',switchToStraight:'no',accounting:'240'})),prior:setup(asset({start:'2025-01-01',accounting:'80',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'})),specific:setup(asset({category:'laundry_machine',specificRate:'yes',rate:'12.5'})),missing:setup(asset({previousAccounting:''}))};fs.mkdirSync('outputs/irpc-l6-depreciation',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T10:00:00+02:00'));fs.writeFileSync('outputs/irpc-l6-depreciation/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-depreciation/'+name+'.html',normalHtml(d));}assert.equal(val(normalBook(cases.specific.d).result.model.colecta),1429.6);});
@@ -0,0 +1,25 @@
import test from'node:test';import assert from'node:assert/strict';import fs from'node:fs';
import{normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalFinancialSource}from'../../resources/js/lib/irpc-normal.ts';
import{newNormalFormPreparation,reuseDirectFormAllocations,normalFormRepresentation,prepareNormalForms}from'../../resources/js/lib/irpc-normal-form-preparation.ts';
import{fraction}from'../../resources/js/lib/fiscal-rational.ts';
import{createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-periods-fx-preserved.json',import.meta.url),'utf8')),val=f=>f?Number(f.numerator)/Number(f.denominator):null;
function setup(){const d=seed(),b=normalBook(d);b.forms={...newNormalFormPreparation(),edition:'accepted_pdf_and_m20a1',reference:'QA PDFM22/Q10–12 e M20A.1/Q4;edições inventariadas distintas',completenessReference:'QA contas completas da oportunidade/sujeito no período;sem outros custos ou proveitos omitidos',allocations:[...reuseDirectFormAllocations(normalFinancialSource(d)),{id:'revenue',sourceCode:'revenue',account:'7.2',amount:'6000',reference:'QA prestação de serviços documentada,sem vendas de bens'},{id:'opex',sourceCode:'operating_expenses',account:'6.3',amount:'600',reference:'QA fornecimentos e serviços de terceiros completos no período'}],incomeTax:{state:'absent',amount:'',placement:'',reference:'QA ausência documentada de imposto contabilizado;não inferir gasto da colecta'},reducedRate:{state:'absent',reference:'QA sujeito/período sem parcela de imposto a taxa reduzida'}};return{d,b};}
const field=(m,code)=>m.formPreparation.fields.find(f=>f.code===code);
test('exact representation keeps signedfraction,approximation,zero,missing and parentheses distinct without fiscal rounding',()=>{for(const f of[fraction(1n,3n),fraction(-1n,300n),fraction(123456789012345678901n,100n)]){const r=normalFormRepresentation(f,'whole');assert.equal(r.exact,f.numerator+'/'+f.denominator+' MZN');assert.equal(r.represented,null);assert.equal(r.ready,false);}assert.match(normalFormRepresentation(fraction(1n,3n)).display,/≈/);assert.equal(normalFormRepresentation(fraction(0n)).represented,'0');assert.equal(normalFormRepresentation(null).display,'Indeterminado');assert.equal(normalFormRepresentation(fraction(-20n),'whole',true).represented,'(20)');assert.match(normalFormRepresentation(fraction(1n,2n)).reason,/exacto ao cêntimo/);});
test('optional9 prepares M20 classifications/netresult and M22 mappedadjustments while preserving all existing totals',()=>{const{d,b}=setup(),before=normalBook(d).result.model,m=calculateNormal(d,b);assert.equal(m.version,'IRPC-normal-adjustments/9');for(const k of['accountingProfit','taxableProfit','taxableBase','colecta'])assert.deepEqual(m[k],before[k]);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(field(m,'200').value),3120);assert.equal(field(m,'200').transcriptionReady,false);assert.equal(val(field(m,'213').value),0);assert.equal(val(field(m,'206').value),10);assert.equal(val(field(m,'238').value),3130);assert.equal(val(field(m,'258').value),0);assert.equal(m.formPreparation.reconciliation.matches,true);assert.equal(field(m,'259').application,'not_applicable');assert.equal(field(m,'260').transcriptionReady,true);assert.equal(val(field(m,'302').value),1001.6);assert.equal(field(m,'302').representation.ready,true);assert.equal(field(m,'309–316').value,null);assert.equal(m.assessment.finalBalance.payable,null);});
test('missingorcontradictory booktax blocks only netresult and its fieldreconciliation; knownnormalprofit/base/colecta preserved',()=>{for(const tax of[{state:'',amount:'',placement:'',reference:''},{state:'absent',amount:'320',placement:'outside',reference:'QA'},{state:'known',amount:'320',placement:'included',reference:'QA'}]){const{d,b}=setup();b.forms.incomeTax=tax;const m=calculateNormal(d,b);assert.equal(m.formPreparation.netResult,null);assert.equal(field(m,'200').value,null);assert.equal(val(field(m,'260').value),3130);assert.equal(val(field(m,'300').value),1001.6);assert.equal(val(m.colecta),1001.6);}});
test('incomeTaxalreadyincluded reconciles account8.5 and mappedtax without subtracting it again',()=>{const{d,b}=setup();b.forms.incomeTax={state:'known',amount:'320',placement:'included',reference:'QA imposto reconhecido no plano'};b.forms.allocations.find(a=>a.id==='opex').amount='280';b.forms.allocations.push({id:'tax',sourceCode:'operating_expenses',account:'8.5',amount:'320',reference:'QA imposto já no custo de origem'});b.schedules.rows.push({id:'tax',rule:'tax_own',amount:'320',sourceCode:'operating_expenses',document:'QA-tax',line:'1',reference:'QA imposto sobre lucro já incluído',fiscalYear:'',conditions:'yes'});Object.assign(b.profit.topics.expenseCorrections,{state:'known',amount:'320',reference:'QA corresponde ao imposto contabilizado já no resultado'});const m=calculateNormal(d,b);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(field(m,'213').value),0);assert.equal(val(m.taxableProfit),3450);assert.equal(field(m,'238').value,null);assert.equal(m.formPreparation.reconciliation.matches,false);assert.ok(m.formPreparation.unmapped.some(e=>e.reason.includes('213')));});
test('incomplete revenueclassification does not erase directM20 fields or independentM22 calculations',()=>{const{d,b}=setup();b.forms.allocations.find(a=>a.id==='revenue').amount='5999';const m=calculateNormal(d,b);assert.equal(m.formPreparation.m20.find(f=>f.account==='7.2').value,null);assert.equal(val(m.formPreparation.m20.find(f=>f.account==='6.5').value),60);assert.equal(m.formPreparation.netResult,null);assert.equal(val(m.taxableProfit),3130);assert.equal(val(field(m,'300').value),1001.6);});
test('fractional M20 allocations remain exact; no truncation or rounding changes source or net/taxable totals',()=>{const{d,b}=setup();b.forms.allocations.find(a=>a.id==='opex').amount='300.50';b.forms.allocations.push({id:'other',sourceCode:'operating_expenses',account:'6.8',amount:'299.50',reference:'QA gastos operacionais restantes documentados'});const m=calculateNormal(d,b),f=m.formPreparation.m20.find(f=>f.account==='6.3');assert.deepEqual(f.value,fraction(601n,2n));assert.equal(f.representation.ready,false);assert.equal(f.representation.represented,null);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(m.colecta),1001.6);});
test('lossuses positiveM22 lossfield and M20parentheses; no loss in profitfield and no negative taxablebase',()=>{const{d,b}=setup(),p=JSON.parse(d.business_plan_snapshot);p.plan.products[0].unitPrice='20';b.schedules.currentLossReference='QA perda fiscal corrente reconciliada no sujeito/período,sem reporte automático';d.business_plan_snapshot=JSON.stringify(p);b.forms.allocations.find(a=>a.id==='revenue').amount='1200';const m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),-1670);assert.equal(val(field(m,'259').value),1670);assert.equal(field(m,'260').application,'not_applicable');assert.equal(m.formPreparation.netRepresentation.represented,'(1680)');assert.equal(val(m.taxableBase),0);assert.equal(val(m.colecta),0);});
test('unmapped priorquota deduction keeps demonstrated effect and existing260/278/300; no guessedblankfield',()=>{const{d,b}=setup(),p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation='48';d.business_plan_snapshot=JSON.stringify(p);Object.assign(b.depreciation.assets[0],{accounting:'24',start:'2025-01-01',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'});b.forms.allocations.find(a=>a.sourceCode==='depreciation').amount='24';const m=calculateNormal(d,b);assert.equal(val(m.depreciation.adjustment),-26);assert.equal(val(m.colecta),1001.6);assert.ok(m.formPreparation.unmapped.some(e=>e.origin.startsWith('Mapa de amortizações')));assert.equal(field(m,'258').value,null);assert.equal(field(m,'238').value,null);assert.equal(val(field(m,'260').value),3130);assert.equal(m.formPreparation.entries.find(e=>e.origin.startsWith('Mapa de amortizações')).code,null);});
test('reducedrate unknown or knowninL7 blocks only302,notnormal300 and not finalL8 assumptions',()=>{for(const state of['','known']){const{d,b}=setup();b.forms.reducedRate.state=state;const m=calculateNormal(d,b);assert.equal(field(m,'302').value,null);assert.equal(val(field(m,'300').value),1001.6);assert.equal(val(m.colecta),1001.6);assert.equal(m.assessment.finalBalance.recoverable,null);}});
test('save/recover/report preserves8history and originalplan and never claims officialrounding or crosseditiontransfer',()=>{const{d,b}=setup(),original=d.business_plan_snapshot,old=structuredClone(b.result);assert.equal(normalCurrent(d,normalBook(d)),true);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T10:00:00+02:00'));const back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),old);assert.equal(d.business_plan_snapshot,original);assert.match(normalHtml(d),/campo213/);assert.match(normalHtml(d),/transporte automático M20/);assert.match(normalHtml(d),/exacto ao cêntimo/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/campo302/);back.forms.allocations[0].reference+='changed';assert.equal(normalCurrent(d,back),false);});
test('fivenewcases write privateAPI proof inputs and reports without rerunning accepted functionalsuites',()=>{const cases={mapped:setup(),fractional:setup(),missing:setup(),unmapped:setup(),booktax:setup()};cases.booktax.b.forms.incomeTax={state:'known',amount:'320',placement:'outside',reference:'QA gasto contabilístico exterior ao plano;campo próprio pendente'};cases.fractional.b.forms.allocations.find(a=>a.id==='opex').amount='300.50';cases.fractional.b.forms.allocations.push({id:'other',sourceCode:'operating_expenses',account:'6.8',amount:'299.50',reference:'QA parcela contabilística com centavos'});cases.missing.b.forms.incomeTax={state:'',amount:'',placement:'',reference:''};const u=cases.unmapped,p=JSON.parse(u.d.business_plan_snapshot);p.plan.assetDepreciation='48';u.d.business_plan_snapshot=JSON.stringify(p);Object.assign(u.b.depreciation.assets[0],{accounting:'24',start:'2025-01-01',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'});u.b.forms.allocations.find(a=>a.sourceCode==='depreciation').amount='24';fs.mkdirSync('outputs/irpc-l6-values-forms',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T10:00:00+02:00'));fs.writeFileSync('outputs/irpc-l6-values-forms/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-values-forms/'+name+'.html',normalHtml(d));}});
test('newlyidentified includedtax mismatch blocks only dependent213/reconciliation without revising demonstratedtax',()=>{const{d,b}=setup();b.forms.incomeTax={state:'known',amount:'320',placement:'included',reference:'QA imposto reconhecido no plano'};b.forms.allocations.find(a=>a.id==='opex').amount='280';b.forms.allocations.push({id:'tax',sourceCode:'operating_expenses',account:'8.5',amount:'320',reference:'QA imposto reconhecido no custo da origem'});const m=calculateNormal(d,b);assert.equal(val(m.colecta),1001.6);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(field(m,'213').value),0);assert.equal(field(m,'238').value,null);assert.equal(val(field(m,'260').value),3130);assert.ok(m.formPreparation.pending.some(s=>s.includes('tax_own')));});
test('positive and negative asseteffects remain separate; netdepreciation is never all assignedto206',()=>{const{d,b}=setup(),a=structuredClone(b.depreciation.assets[0]);Object.assign(b.depreciation.assets[0],{cost:'60',accounting:'36'});Object.assign(a,{id:'other',assetKey:'other',document:'QA-other-asset',accounting:'24',start:'2025-01-01',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'});b.depreciation.assets.push(a);const m=calculateNormal(d,b);assert.equal(val(m.depreciation.adjustment),4);assert.equal(val(field(m,'206').value),30);assert.equal(val(m.taxableProfit),3124);assert.equal(m.formPreparation.unmapped.length,1);assert.equal(val(m.formPreparation.unmapped[0].value),-26);assert.equal(field(m,'238').value,null);});
test('acceptedcostproof feeds independentM22 labels without applying printedpercentages again',()=>{const d=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-costs-preserved.json',import.meta.url),'utf8')),b=normalBook(d),m=b.result.model,{b:input}=setup();const mapped=prepareNormalForms(input.forms,m,normalFinancialSource(d));for(const[rule,code]of [['allowance','219'],['representation','220'],['advertising','221'],['credit','209']]){const row=m.costs.rows.find(r=>r.rule===rule);assert.ok(row);const sum=m.costs.rows.filter(r=>r.rule===rule).reduce((s,r)=>s+val(r.adjustment),0);const value=mapped.fields.find(f=>f.code===code).value;if(code!=='209')assert.equal(val(value),sum);else assert.equal(val(value),m.costs.rows.filter(r=>['credit','stock','litigation'].includes(r.rule)).reduce((s,r)=>s+val(r.adjustment),0));}assert.deepEqual(b.result.model,m);});
test('absence of booktax cannot erase previouslyidentified ownincomeTax cost; independentknowncolecta remains',()=>{const{d,b}=setup();b.schedules.rows.push({id:'tax',rule:'tax_own',amount:'320',sourceCode:'operating_expenses',document:'QA-tax',line:'1',reference:'QA imposto já incluído',fiscalYear:'',conditions:'yes'});Object.assign(b.profit.topics.expenseCorrections,{state:'known',amount:'320',reference:'QA mapa do imposto próprio'});b.forms.incomeTax={state:'absent',amount:'',placement:'',reference:'QA ausência a conferir'};const m=calculateNormal(d,b);assert.equal(m.formPreparation.netResult,null);assert.ok(m.formPreparation.pending.some(s=>s.includes('contradiz imposto próprio')));assert.equal(val(field(m,'300').value),1104);});
@@ -0,0 +1,33 @@
import test from'node:test';import assert from'node:assert/strict';import fs from'node:fs';
import{normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalFinancialSource}from'../../resources/js/lib/irpc-normal.ts';
import{newNormalPeriodFx,mapNormalPeriodFx}from'../../resources/js/lib/irpc-normal-period-fx.ts';
import{emptyFxSelection,selectFx}from'../../resources/js/lib/iva-fx-selection.ts';
import{createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
import{newCurrencyEvent}from'../../resources/js/lib/iva-currency-events.ts';
import{irpcFxSources}from'../../resources/js/lib/irpc-fx.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-depreciation-preserved.json',import.meta.url),'utf8')),val=f=>f?Number(f.numerator)/Number(f.denominator):null;
function fx(direction='mzn_per_unit',rate='64'){const s={...emptyFxSelection(),nature:'commercial',submissionDate:'2026-10-26',bulletinDate:'2026-10-26',bulletinRef:'QA pressuposto sintético64; não boletim real',source:'https://www.bancomoc.mz/',sourceCurrency:'USD',direction,mean:rate};return{...s,applied:selectFx(s)};}
function setup(currency='MZN',kind='start'){
const d=seed(),b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.currency=currency;d.business_plan_snapshot=JSON.stringify(p);
b.rows=[];b.costs=undefined;b.schedules.rows=[];b.schedules.interest.amount=currency==='USD'?'7680':'120';
for(const t of Object.values(b.profit.topics)){t.state='absent';t.amount='';t.reference='QA ausência declarada no período seleccionado';}
b.base.losses=[];b.base.lossesState='absent';b.base.lossesReference='QA ausência de perdas reportadas';
b.depreciation.assets[0].accounting=currency==='USD'?'3840':'60';b.depreciation.assets[0].cost=currency==='USD'?'32000':'500';b.depreciation.assets[0].start='2026-07-01';b.depreciation.assets[0].timing='months';
b.periodsFx={...newNormalPeriodFx(),kind,from:'2026-07-01',to:'2026-12-31',planStart:'2026-01-01',reference:'QA início documentado1Julho;correspondência completa dos meses7–12 ao período do sujeito',mznMapsReference:'QA activo/quotas/juros e demais mapas reconciliados explicitamente emMZN no período',conversionReference:currency==='USD'?'QA pressuposto de simulação64MZN/USD aplicado aos meses seleccionados;sem regra fiscal anual confirmada':''};
if(currency==='USD')b.periodsFx.months=Array.from({length:6},(_,n)=>({period:n+7,currency:'USD',fxSelection:fx()}));
return{d,b};
}
test('installed7 stays current without opt-in; optional8 preserves original result and plan while selecting6 months',()=>{const d=seed(),old=normalBook(d);assert.equal(normalCurrent(d,old),true);const {d:next,b}=setup(),m=calculateNormal(next,b);assert.equal(m.version,'IRPC-normal-adjustments/8');assert.equal(val(m.accountingProfit),3120);assert.equal(val(m.depreciation.assets[0].quota),50);assert.equal(val(m.taxableProfit),3130);assert.equal(val(m.taxableBase),3130);assert.equal(val(m.colecta),1001.6);assert.equal(m.assessment.period.from,'2026-07-01');assert.equal(m.assessment.finalBalance.payable,null);assert.equal(JSON.parse(next.business_plan_snapshot).plan.periods,12);});
test('monthly conversion reuses delivered selection and direction; originalUSD and assumptions remain reportable',()=>{const{d,b}=setup('USD'),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),199680);assert.equal(val(m.depreciation.adjustment),640);assert.equal(val(m.taxableProfit),200320);assert.equal(val(m.colecta),64102.4);assert.equal(m.periodsFx.originalCurrency,'USD');assert.equal(m.periodsFx.rows[0].evidence.column,'mean');assert.match(m.periodsFx.rule,/simulação/);const inv=setup('USD');for(const e of inv.b.periodsFx.months)e.fxSelection=fx('units_per_mzn','0.015625');assert.equal(val(calculateNormal(inv.d,inv.b).colecta),64102.4);});
test('annual plan in another currency and shorterplan aligned to July both reconcile without annualising',()=>{const a=setup('USD');a.b.periodsFx.kind='annual';a.b.periodsFx.from='2026-01-01';a.b.periodsFx.months=Array.from({length:12},(_,n)=>({period:n+1,currency:'USD',fxSelection:fx()}));a.b.schedules.interest.amount='15360';a.b.depreciation.assets[0].accounting='7680';a.b.depreciation.assets[0].start='2026-01-01';assert.equal(val(calculateNormal(a.d,a.b).colecta),128204.8);const{d,b}=setup();const p=JSON.parse(d.business_plan_snapshot);p.plan.periods=6;p.plan.assetDepreciation='60';p.plan.depreciationLifeMonths=6;d.business_plan_snapshot=JSON.stringify(p);b.periodsFx.planStart='2026-07-01';assert.equal(val(calculateNormal(d,b).colecta),1001.6);});
test('cessation and same-year start/end use documented dates; unsupportedpartial months and contradictions remain pending',()=>{const{d,b}=setup();b.periodsFx.kind='cessation';b.periodsFx.from='2026-01-01';b.periodsFx.to='2026-06-30';b.depreciation.assets[0].start='2026-01-01';assert.equal(val(calculateNormal(d,b).colecta),1001.6);b.periodsFx.kind='same_year';b.periodsFx.from='2026-04-01';b.periodsFx.to='2026-09-30';assert.equal(val(calculateNormal(d,b).colecta),1001.6);for(const patch of[{from:'2026-07-15'},{to:'2026-12-30'},{planStart:''},{from:'2026-01-01',kind:'start'},{to:'2026-02-31'},{reference:''},{mznMapsReference:''}]){const x=setup();Object.assign(x.b.periodsFx,patch);assert.equal(calculateNormal(x.d,x.b).colecta,null);}});
test('missingmonth selection,sourcecurrency mismatch and editedapplied rate never become defaultFX',()=>{for(const mutate of[i=>i.months.pop(),i=>i.months[0].currency='EUR',i=>i.months[0].fxSelection.mean='65',i=>i.conversionReference='',i=>i.months.push({...i.months[0],period:1})]){const{d,b}=setup('USD');mutate(b.periodsFx);const m=calculateNormal(d,b);assert.equal(m.accountingProfit,null);assert.equal(m.colecta,null);assert.ok(m.periodsFx.rows.some(r=>r.value));}});
test('fractionalcent conversion blocks dependenttotals while retaining exact source and otherknownmonths',()=>{const{d,b}=setup('USD');b.periodsFx.months[0].fxSelection=fx('mzn_per_unit','0.33333333');const m=calculateNormal(d,b);assert.equal(m.colecta,null);assert.ok(m.periodsFx.pending.some(s=>s.includes('fracção de cêntimo')));assert.equal(m.periodsFx.rows[0].originalCurrency,'USD');});
test('shortperiod priorloss expiry and annualcaps stay explicitlypending; no change to independent arithmetic',()=>{const x=setup();x.b.base=structuredClone(normalBook(seed()).base);const m=calculateNormal(x.d,x.b);assert.equal(val(m.taxableProfit),3130);assert.equal(m.taxableBase,null);assert.ok(m.base.pending.some(s=>s.includes('41(5)')));const y=setup();y.b.costs=structuredClone(normalBook(seed()).costs);assert.equal(calculateNormal(y.d,y.b).colecta,null);assert.ok(calculateNormal(y.d,y.b).costs.pending.some(s=>s.includes('Limite anual neste período curto')));y.b.costs=undefined;y.b.depreciation.assets[0].timing='annual';assert.equal(calculateNormal(y.d,y.b).colecta,null);});
test('save/recovery/report preserves7 history and sourceplan; changed period makes8 historical',()=>{const{d,b}=setup('USD'),original=d.business_plan_snapshot,old=structuredClone(b.result);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T10:00:00+02:00'));const back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),old);assert.equal(d.business_plan_snapshot,original);assert.match(normalHtml(d),/64102.40/);assert.match(normalHtml(d),/regra fiscal anual não confirmada/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/USD/);back.periodsFx.reference+='changed';assert.equal(normalCurrent(d,back),false);});
test('newcases prepare privateAPI persistence and dossiers including insufficientinformation',()=>{const cases={short:setup(),fx:setup('USD'),missing:setup('USD'),loss:setup()};cases.missing.b.periodsFx.months.pop();cases.loss.b.base=structuredClone(normalBook(seed()).base);fs.mkdirSync('outputs/irpc-l6-periods-fx',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T10:00:00+02:00'));fs.writeFileSync('outputs/irpc-l6-periods-fx/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-periods-fx/'+name+'.html',normalHtml(d));}});
test('different monthrates require MZN map reconciliation; UI source uses converted subset without relabellingUSD credit balances',()=>{const{d,b}=setup('USD');b.periodsFx.months[0].fxSelection=fx('mzn_per_unit','65');assert.equal(calculateNormal(d,b).colecta,null);b.depreciation.assets[0].accounting='3850';b.schedules.interest.amount='7700';assert.equal(val(calculateNormal(d,b).colecta),64272);d.modelo_a_snapshot=saveNormalBook(d,b);const source=normalFinancialSource(d);assert.equal(source.currency,'MZN');assert.equal(source.periods,6);assert.equal(source.accountingLines.find(l=>l.code==='depreciation').total,'3850.00');assert.deepEqual(source.cashSchedule,[]);});
test('noncalendar annual mapping cannot bypass existing authorised period and unsupportedshortdates stay pending',()=>{const{d,b}=setup();b.periodsFx.kind='annual';b.periodsFx.from='2025-07-01';b.periodsFx.to='2026-06-30';b.periodsFx.planStart='2025-07-01';assert.equal(calculateNormal(d,b).colecta,null);assert.ok(calculateNormal(d,b).pending.some(s=>s.includes('autorizado/comunicado')));});
test('reusedFX link invalidates current result when existing source changes and never changes historical result',()=>{const{d,b}=setup('USD'),snapshot=JSON.parse(d.modelo_a_snapshot);snapshot.currencyEvents??={};snapshot.currencyEvents[d.context_opportunity]={version:1,rows:[{...newCurrencyEvent('fx-shared','MZN'),sourceCurrency:'USD',fxSelection:fx()}]};d.modelo_a_snapshot=JSON.stringify(snapshot);const linked=irpcFxSources(d)[0];assert.ok(linked);b.periodsFx.months[0].fxLink={id:linked.id,fingerprint:linked.fingerprint};b.periodsFx.months[0].fxSelection=linked.selection;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));const book=normalBook(d),old=structuredClone(book.result);assert.equal(normalCurrent(d,book),true);const changed=JSON.parse(d.modelo_a_snapshot);changed.currencyEvents[d.context_opportunity].rows[0].description='New source context';d.modelo_a_snapshot=JSON.stringify(changed);assert.equal(normalCurrent(d,normalBook(d)),false);assert.deepEqual(normalBook(d).result,old);assert.equal(calculateNormal(d,normalBook(d)).colecta,null);});
test('known nondepreciable asset in shortperiod keeps full addback without asking irrelevant timing',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{category:'nondepreciable',timing:'',method:''});const m=calculateNormal(d,b);assert.equal(val(m.depreciation.adjustment),60);assert.equal(val(m.colecta),1017.6);});
@@ -0,0 +1,18 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {normalBook,normalFinancialSource,recordNormal,calculateNormal,normalCurrent,saveNormalBook,normalHtml,normalReportRows}from'../../resources/js/lib/irpc-normal.ts';
import {newNormalProfitInput,profitTopics}from'../../resources/js/lib/irpc-normal-profit.ts';
import {restoreBusinessPlanSnapshot,serializeBusinessPlanSnapshot}from'../../resources/js/lib/business-financial-plan.ts';
import {createProjectDossierReport}from'../../resources/js/lib/project-dossier-report.ts';
import {irpcBook,saveIrpcBook}from'../../resources/js/lib/irpc-withholdings.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-normal-preserved.json',import.meta.url),'utf8'));
function example(){const d=seed(),b=normalBook(d),profit=newNormalProfitInput();b.rows[1].amount='120';b.rows[1].reference='QA todos120MZN de juros reconhecidos são compensatórios documentados;nenhuma taxa L5 calculada';profit.completenessReference='QA plano cobre sujeito residente/exercício2026 completo,sem outros estabelecimentos,receitas,custos ou operações omitidos';profit.lines=normalFinancialSource(d).accountingLines.map(l=>({code:l.code,state:l.code==='depreciation'?'absent':'known',reference:'QA qualificação ordinária/exercício/reconciliação de '+l.origin+';restante custo indispensável sem outras exclusões'}));for(const[k]of profitTopics)profit.topics[k]={state:'absent',amount:'',reference:'QA ausência verificada de '+k};profit.topics.incomeCorrections={state:'known',amount:'0',reference:'QA revisão de correcções ao rendimento:valor conhecido zero'};b.profit=profit;return{d,b};}
test('documented financial result plus supported addbacks reaches scoped simulated taxable profit without tax/base',()=>{const{d,b}=example(),m=calculateNormal(d,b);assert.equal(m.accountingProfit.numerator,'6480');assert.equal(m.additions.numerator,'220');assert.equal(m.taxableProfit.numerator,'6700');assert.equal(m.taxableBase,null);assert.equal(m.tax,null);assert.equal(m.profit.pending.length,0);assert.equal(m.profit.lines.find(l=>l.code==='interest_expense').remainder.numerator,'0');assert.equal(m.profit.lines.find(l=>l.code==='operating_expenses').remainder.numerator,'1100');});
test('known zero,declared absence,and missing information keep distinct saved evidence and effects',()=>{const{d,b}=example();b.profit.topics.incomeCorrections={state:'known',amount:'0',reference:'QA total revisto de correcções ao rendimento é zero'};let m=calculateNormal(d,b);assert.equal(m.taxableProfit.numerator,'6700');assert.equal(m.profit.topics.find(t=>t.key==='incomeCorrections').state,'known');assert.equal(m.profit.topics.find(t=>t.key==='positivePatrimonial').state,'absent');b.profit.topics.negativePatrimonial={state:'',amount:'',reference:''};m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.additions.numerator,'220');assert.equal(m.profit.topics.find(t=>t.key==='negativePatrimonial').value,null);assert.match(m.profit.pending.join(' '),/sem zero presumido/);});
test('absence cannot override known nonzero financial values or silently discard an entered amount',()=>{const{d,b}=example();b.profit.lines.find(l=>l.code==='operating_expenses').state='absent';assert.equal(calculateNormal(d,b).taxableProfit,null);b.profit.lines.find(l=>l.code==='operating_expenses').state='known';b.profit.topics.positivePatrimonial.amount='100';assert.equal(calculateNormal(d,b).taxableProfit,null);});
test('known unimplemented variation,depreciation and remaining financial interest stay visible without presumed treatment',()=>{for(const patch of ['variation','depreciation','interest']){const{d,b}=example();if(patch==='variation')b.profit.topics.positivePatrimonial={state:'known',amount:'500',reference:'QA variação500,regra específica por corresponder'};if(patch==='depreciation'){const p=restoreBusinessPlanSnapshot(d.business_plan_snapshot);p.assetDepreciation='120';d.business_plan_snapshot=serializeBusinessPlanSnapshot(p);b.profit.lines.find(l=>l.code==='depreciation').state='known';}if(patch==='interest')b.rows[1].amount='20';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.ok(m.profit.pending.length>0);if(patch==='variation')assert.equal(m.profit.topics[0].value.numerator,'500');}});
test('complete line classifications and subject-year completeness are required,not a bare project subtotal',()=>{for(const patch of ['line','reference','completeness','scope','year']){const{d,b}=example();if(patch==='line')b.profit.lines.pop();if(patch==='reference')b.profit.lines[0].reference='';if(patch==='completeness')b.profit.completenessReference='';if(patch==='scope')b.scope='other';if(patch==='year')b.year='2025';assert.equal(calculateNormal(d,b).taxableProfit,null);}});
test('declared no addbacks can close only after full documented review,and fiscal losses are not clamped to zero',()=>{const{d,b}=example();let p=restoreBusinessPlanSnapshot(d.business_plan_snapshot);p.monthlyInterestExpense='0';d.business_plan_snapshot=serializeBusinessPlanSnapshot(p);b.rows=[];b.profit.lines.find(l=>l.code==='interest_expense').state='absent';assert.equal(calculateNormal(d,b).taxableProfit.numerator,'6600');p.products[0].unitPrice='1';d.business_plan_snapshot=serializeBusinessPlanSnapshot(p);assert.equal(calculateNormal(d,b).taxableProfit.numerator,'-5280');});
test('L5 uncertainty does not block independent known expense classification or annual profit',()=>{const{d,b}=example(),s=JSON.parse(d.modelo_a_snapshot);for(const v of Object.values(s.irpcWithholdings??{})){if(v.mrfta){v.mrfta.codes=[];v.mrfta.interest.mode='';v.mrfta.interest.calculation=undefined;}}d.modelo_a_snapshot=JSON.stringify(s);assert.equal(calculateNormal(d,b).taxableProfit.numerator,'6700');});
test('old result remains current and unchanged;new closure history and stale-input guards survive roundtrip and dossier',()=>{const legacy=seed(),old=structuredClone(normalBook(legacy));assert.equal(normalCurrent(legacy,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/1');const{d,b}=example();const saved=recordNormal(d,b,'2026-10-25T22:00:00Z');assert.deepEqual(saved.history.at(-1),old.result);const before=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,saved);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);const back=JSON.parse(JSON.stringify(d));assert.equal(normalCurrent(back,normalBook(back)),true);assert.match(normalHtml(back),/lucro tributável simulado:6700/);assert.match(JSON.stringify(createProjectDossierReport(back,{selectedSections:new Set(['finance'])})),/6700/);assert.match(JSON.stringify(normalReportRows(back)),/Ausência declarada/);const l5=irpcBook(back);back.modelo_a_snapshot=saveIrpcBook(back,l5);assert.deepEqual(normalBook(back),saved);normalBook(back).profit.lines[0].reference='';const changed=normalBook(back);changed.profit.lines[0].reference='';back.modelo_a_snapshot=saveNormalBook(back,changed);assert.equal(normalCurrent(back,normalBook(back)),false);assert.throws(()=>normalHtml(back));
fs.mkdirSync('outputs/irpc-l6-profit',{recursive:true});fs.writeFileSync('outputs/irpc-l6-profit/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-profit/preparation.html',normalHtml(d));const missing=normalBook(d);missing.profit.topics.negativePatrimonial={state:'',amount:'',reference:''};const md=structuredClone(d);md.modelo_a_snapshot=saveNormalBook(md,recordNormal(md,missing));fs.writeFileSync('outputs/irpc-l6-profit/missing.json',JSON.stringify(md));});
test('malformed added evidence and unsupported stored versions fail without resetting the book',()=>{const{d,b}=example();b.profit.topics.positivePatrimonial.amount=100;assert.throws(()=>saveNormalBook(d,b));b.profit.topics.positivePatrimonial.amount='';b.profit.lines.push({...b.profit.lines[0]});assert.throws(()=>saveNormalBook(d,b));});
@@ -0,0 +1,28 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalFinancialSource,normalReportRows} from '../../resources/js/lib/irpc-normal.ts';
import {newNormalSchedules,newScheduleRow,newNormalAsset,calculateSchedules} from '../../resources/js/lib/irpc-normal-schedules.ts';
import {normalFormFields} from '../../resources/js/lib/irpc-normal-forms.ts';import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-assessment-preserved.json',import.meta.url),'utf8'));
function scenario(){const d=seed(),b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyInterestExpense='20';p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);b.schedules=newNormalSchedules();const s=b.schedules;s.period={kind:'calendar',from:'2026-01-01',to:'2026-12-31',reference:'QA doze meses do plano correspondem ao ano civil completo'};
s.interest={amount:'120',reference:'QA contrato bancário, exploração, sem relação especial, limites especiais ou capitalização; pagamentos separados',conditions:'yes'};
for(const [id,rule,amount,sourceCode,fiscalYear]of[['thirdparty','tax_other','250','operating_expenses',''],['gain','patrimonial_gain','200','',''],['cost','patrimonial_cost','50','',''],['timing','future_cost','100','operating_expenses','2027']])s.rows.push({...newScheduleRow(id),rule,amount,sourceCode,fiscalYear,document:'QA-'+id,line:'1',reference:'QA factos documentados no recorte ordinário, não é operação especial',conditions:'yes'});
for(const[k,amount]of Object.entries({expenseCorrections:'250',positivePatrimonial:'200',negativePatrimonial:'50',timing:'100'}))b.profit.topics[k]={state:'known',amount,reference:'QA reconciliação integral do tema com os documentos do mapa'};
b.profit.lines.find(l=>l.code==='depreciation').state='known';s.assetsReference='QA único activo; total contabilístico120 reconciliado com o plano';s.assets=[{...newNormalAsset('server'),category:'server',description:'Servidor QA',cost:'500',residual:'0',rate:'20',accounting:'120',previousFiscal:'0',inUseFrom:'2026-01-01',tableReference:'Decreto72/2013 tabelaII grupo3 item9.1, sem taxa específica da tabelaI',reference:'QA factura500, activo próprio, vida útil fiscal, sem regras especiais',conditions:'yes'}];return{d,b};}
const val=f=>f?Number(f.numerator)/Number(f.denominator):null;
test('new rules reconcile financial interest, patrimonial variations, timing and server quota through profit/base/colecta',()=>{const{d,b}=scenario(),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.additions),220);assert.equal(val(m.schedules.interest),120);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.schedules.net),520);assert.equal(val(m.taxableProfit),6980);assert.equal(val(m.taxableBase),4680);assert.equal(val(m.colecta),1497.6);assert.equal(m.tax,null);assert.equal(m.assessment.finalBalance.payable,null);assert.equal(m.schedules.assets[0].fiscal.numerator,'100');assert.equal(m.version,'IRPC-normal-adjustments/5');});
test('ordinary interest is qualified independently from compensatory interest and cash payments',()=>{const{d,b}=scenario();b.schedules.interest.amount='119';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(val(m.schedules.net),520);assert.equal(m.schedules.interest,null);assert.match(m.profit.pending.join(' '),/Juros financeiros/);const c=scenario();c.b.schedules.rows[0].sourceCode='interest_expense';c.b.schedules.rows[0].amount='20';c.b.profit.topics.expenseCorrections.amount='20';assert.equal(calculateNormal(c.d,c.b).taxableProfit,null);c.b.schedules.interest.amount='100';assert.equal(val(calculateNormal(c.d,c.b).schedules.interest),100);assert.notEqual(calculateNormal(c.d,c.b).taxableProfit,null);});
test('missing rule evidence blocks dependent totals while independent amounts remain calculated',()=>{const{d,b}=scenario();b.schedules.rows[0].reference='';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.colecta,null);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),200);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.accountingProfit),6240);});
test('duplicate documents, over-allocation, wrong source and contradictory topic absence are rejected',()=>{for(const mutate of[b=>{b.schedules.rows[0].document=b.rows[0].document+' ';b.schedules.rows[0].line=b.rows[0].line;},b=>{b.schedules.rows[0].amount='1201';b.profit.topics.expenseCorrections.amount='1201';},b=>b.schedules.rows[0].sourceCode='revenue',b=>b.profit.topics.positivePatrimonial={state:'absent',amount:'',reference:'QA declared absent'}]){const{d,b}=scenario();mutate(b);assert.equal(calculateNormal(d,b).taxableProfit,null);}});
test('capital contributions and distributions outside result are known amounts with zero tax effect, not ordinary gains/costs',()=>{const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='gain').rule='capital_in';b.schedules.rows.find(r=>r.id==='cost').rule='capital_out';const m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),6830);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),0);assert.equal(val(m.schedules.effects.find(e=>e.id==='cost').amount),50);});
test('timing belongs to documented year; previous years and inconsistent mapping stay pending',()=>{for(const y of['','2025','2026']){const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='timing').fiscalYear=y;assert.equal(calculateNormal(d,b).taxableProfit,null);}});
test('asset quota uses remaining fiscal value; unsupported rates, short-year facts and incomplete reconciliation do not default to accounting',()=>{const{d,b}=scenario();b.schedules.assets[0].previousFiscal='450';let m=calculateNormal(d,b);assert.equal(val(m.schedules.assets[0].fiscal),50);assert.equal(val(m.schedules.depreciation),70);for(const patch of[{rate:'25'},{category:'other'},{inUseFrom:'2026-02-01'},{inUseFrom:'2025-02-31'},{inUseFrom:'2014-01-01'},{accounting:'119'},{conditions:''}]){const{d,b}=scenario();Object.assign(b.schedules.assets[0],patch);m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.schedules.depreciation,null);}});
test('known current loss gives zero base/colecta only with documented current-loss treatment and no self-deduction; no official carryforward created',()=>{const{d,b}=scenario(),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyOperatingExpenses='1000';d.business_plan_snapshot=JSON.stringify(p);b.base.losses=[];b.base.lossesState='absent';b.base.lossesReference='QA ausência expressa de perdas anteriores';let m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),-3820);assert.equal(m.taxableBase,null);b.schedules.currentLossReference='QA perda fiscal corrente apurada, sem dedução de perdas anteriores ou reporte automático';m=calculateNormal(d,b);assert.equal(val(m.currentLoss),3820);assert.equal(val(m.taxableBase),0);assert.equal(val(m.colecta),0);assert.deepEqual(b.base.losses,[]);assert.equal(normalFormFields(m).fields.find(f=>f.code==='269').value.numerator,'3820');assert.equal(normalFormFields(m).fields.find(f=>f.code==='270').value,null);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.mkdirSync('outputs/irpc-l6-sets',{recursive:true});fs.writeFileSync('outputs/irpc-l6-sets/loss.json',JSON.stringify(d));});
test('EP ordinary annual MZN uses its attributable financial result; unsupported currency/year remains case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.scopeReference='QA único EP em Moçambique, imputação completa e exclusiva de custos/proveitos do EP, CIRPC15(1)c';assert.equal(val(calculateNormal(d,b).colecta),1497.6);b.year='2025';assert.equal(calculateNormal(d,b).colecta,null);});
test('form preparation keeps exact fractions and unmapped fields without provisional codes or fabricated final settlement',()=>{const{d,b}=scenario(),m=calculateNormal(d,b),f=normalFormFields(m);assert.equal(val(f.fields.find(f=>f.code==='278').value),4680);assert.equal(f.fields.find(f=>f.code==='300').transcriptionReady,true);for(const code of['200','302','309–316'])assert.equal(f.fields.find(f=>f.code===code).value,null);assert.equal(f.m20.status,'partial');assert.equal(val(f.m20.fields.find(f=>f.account==='6.5').value),120);assert.match(f.m20.reason,/campo201.*campo200/);const partial=normalFormFields({...m,colecta:fraction(2n,625n)}).fields.find(f=>f.code==='300');assert.deepEqual(partial.value,{numerator:'2',denominator:'625'});assert.equal(partial.transcriptionReady,false);assert.match(partial.reason,/sem arredondamento/);});
test('installed4 is current without recalculation; new inputs survive save/recovery/history/export/dossier and become stale on edit',()=>{const original=seed(),old=normalBook(original);assert.equal(normalCurrent(original,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/4');const{d,b}=scenario(),before=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-25T23:50:00Z'));const recovered=JSON.parse(JSON.stringify(d)),back=normalBook(recovered);assert.equal(normalCurrent(recovered,back),true);assert.deepEqual(back.history.at(-1),old.result);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).books,before.books);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);assert.match(normalHtml(d),/1497.6/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/M22 campo 300/);assert.match(JSON.stringify(normalReportRows(d)),/M20/);fs.writeFileSync('outputs/irpc-l6-sets/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-sets/preparation.html',normalHtml(d));back.schedules.rows[0].amount='251';recovered.modelo_a_snapshot=saveNormalBook(recovered,back);assert.equal(normalCurrent(recovered,normalBook(recovered)),false);assert.throws(()=>normalHtml(recovered));const missing=normalBook(d);missing.schedules.interest.conditions='';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,missing));fs.writeFileSync('outputs/irpc-l6-sets/missing.json',JSON.stringify(d));});
test('unknown schema/rules fail without silently replacing saved originals',()=>{const{d,b}=scenario();b.schedules.version=2;assert.throws(()=>saveNormalBook(d,b));b.schedules.version=1;b.schedules.rows[0].rule='fiction';assert.throws(()=>saveNormalBook(d,b));});
test('documented noncalendar EP year preserves12 monthly periods; short periods and absent legal basis stay case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.schedules.period={kind:'ep_annual',from:'2025-07-01',to:'2026-06-30',reference:'QA comunicação expressa AT anterior, início e manutenção5exercícios, plano completo do EP'};b.schedules.assets[0].inUseFrom='2025-07-01';let m=calculateNormal(d,b);assert.equal(val(m.colecta),1497.6);assert.equal(m.assessment.period.from,'2025-07-01');assert.equal(m.assessment.period.to,'2026-06-30');d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.writeFileSync('outputs/irpc-l6-sets/ep.json',JSON.stringify(d));b.schedules.period.from='2026-01-01';assert.equal(calculateNormal(d,b).colecta,null);b.schedules.period.reference='';assert.equal(calculateNormal(d,b).taxableProfit,null);});
test('ordinary tax/undocumented addbacks and current income/cost timing apply their distinct signs without overwriting original accounting',()=>{for(const rule of['tax_own','undocumented']){const{d,b}=scenario();b.schedules.rows[0].rule=rule;assert.equal(val(calculateNormal(d,b).taxableProfit),6980);}for(const [rule,delta]of[['current_cost',-200],['current_income',0]]){const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule=rule;r.sourceCode='';r.fiscalYear='2026';assert.equal(val(calculateNormal(d,b).taxableProfit),6980+delta);}const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule='future_income';r.sourceCode='revenue';assert.equal(val(calculateNormal(d,b).taxableProfit),6780);assert.equal(normalFinancialSource(d).accountingProfitBeforeTax,'6240.00');});
+37
View File
@@ -0,0 +1,37 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {serializeBusinessPlanSnapshot} from '../../resources/js/lib/business-financial-plan.ts';
import {normalBook,newNormalBook,newNormalAdjustment,normalFinancialSource,calculateNormal,recordNormal,normalCurrent,saveNormalBook,normalReportRows,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
import {irpcBook} from '../../resources/js/lib/irpc-withholdings.ts';
const plan=()=>({currency:'MZN',openingCash:'0',products:[{label:'Serviço',monthlyUnits:'10',unitPrice:'100',unitVariableCost:'30'}],monthlyCollections:'800',monthlyVariableCostPayments:'250',monthlyOperatingExpenses:'100',monthlyOperatingPayments:'80',monthlyPayrollExpense:'50',monthlyPayrollPayments:'40',assetInvestment:'0',inventory:'0',receivables:'0',payables:'0',equity:'0',shareholderLoans:'0',bankLoanProceeds:'0',grants:'0',assetDepreciation:'0',depreciationLifeMonths:12,monthlyInterestExpense:'10',monthlyInterestPayment:'5',monthlyPrincipalRepayment:'60',periods:12});
const draft=()=>({...JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-mrfta-preserved.json',import.meta.url),'utf8')),fiscal_component:'irpc',business_plan_snapshot:serializeBusinessPlanSnapshot(plan())});
const row=(id='a')=>({...newNormalAdjustment(id),sourceCode:'operating_expenses',document:'QA aviso multa sintético',line:id,amount:'100',rule:'noncontractual_fine',included:'yes',reference:'QA pressuposto:multa não contratual incluída no custo operacional'});
const book=()=>({...newNormalBook(),year:'2026',mappingReference:'QA plano meses1–12=Jan–Dez2026',scope:'resident_general',scopeReference:'QA residente geral sem regime especial',rows:[row(),{...row('b'),sourceCode:'interest_expense',amount:'20',rule:'compensatory_interest',document:'QA liquidação juros sintética',reference:'QA pressuposto:juros compensatórios incluídos no custo financeiro'}]});
test('L6 reuses reconciled profit and expense sources,without cash repayment or received withholding becoming expense',()=>{
const d=draft(),s=normalFinancialSource(d),m=calculateNormal(d,book());assert.equal(s.accountingProfitBeforeTax,'6480.00');assert.equal(m.accountingProfit.numerator,'6480');assert.equal(m.additions.numerator,'120');assert.equal(m.partialAdjustedResult.numerator,'6600');assert.equal(m.taxableProfit,null);assert.equal(m.taxableBase,null);assert.equal(m.tax,null);assert.equal(m.origin,'projected');assert.match(m.pending.join(' '),/L6 parcial/);
const p=plan();p.monthlyPrincipalRepayment='0';p.monthlyInterestPayment='0';d.business_plan_snapshot=serializeBusinessPlanSnapshot(p);assert.deepEqual(calculateNormal(d,book()).partialAdjustedResult,m.partialAdjustedResult);
});
test('missing qualification affects its own adjustment;confirmed additions remain visible without a full adjusted total',()=>{
const b=book();b.rows[1].reference='';const m=calculateNormal(draft(),b);assert.equal(m.rows[0].status,'calculated');assert.equal(m.rows[1].adjustment,null);assert.equal(m.additions.numerator,'100');assert.equal(m.partialAdjustedResult,null);b.rows[1].rule='other';assert.equal(calculateNormal(draft(),b).rows[1].status,'outside_scope');
});
test('duplicate document lines and oversubscribed source costs cannot double count',()=>{
const b=book();b.rows.push({...b.rows[0],id:'duplicate'});let m=calculateNormal(draft(),b);assert.equal(m.rows[0].adjustment,null);assert.equal(m.rows[2].adjustment,null);b.rows=[{...row('one'),amount:'700'},{...row('two'),amount:'600'}];m=calculateNormal(draft(),b);assert.ok(m.rows.every(r=>r.adjustment===null));assert.match(m.pending.join(' '),/excedem/);
});
test('already excluded expenses,revenue sources,negative and overprecision amounts do not become additions',()=>{
for(const patch of [{included:'no'},{sourceCode:'revenue'},{amount:'-10'},{amount:'1.001'},{rule:''}]){const b=book();b.rows=[{...row(),...patch}];assert.equal(calculateNormal(draft(),b).rows[0].adjustment,null);}
});
test('year,mapping,currency,horizon and regime stay explicit;no global annual assessment inferred',()=>{
for(const patch of [{year:''},{year:'2025'},{mappingReference:''},{scope:'other'},{scopeReference:''}])assert.equal(calculateNormal(draft(),{...book(),...patch}).partialAdjustedResult,null);
for(const p of [{...plan(),currency:'USD'},{...plan(),periods:6}]){const d=draft();d.business_plan_snapshot=serializeBusinessPlanSnapshot(p);assert.equal(calculateNormal(d,book()).partialAdjustedResult,null);}
const d=draft();d.business_plan_snapshot='';assert.equal(calculateNormal(d,book()).accountingProfit,null);assert.equal(calculateNormal(d,{...book(),rows:[]}).partialAdjustedResult,null);
});
test('calculation save recovery report preserves installed books and invalidates history on financial or adjustment changes',()=>{
const d=draft(),before=JSON.parse(d.modelo_a_snapshot),b=recordNormal(d,book(),'2026-10-25T20:00:00Z');d.modelo_a_snapshot=saveNormalBook(d,b);const recovered=JSON.parse(JSON.stringify(d)),saved=normalBook(recovered);assert.equal(normalCurrent(recovered,saved),true);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);assert.ok(irpcBook(d));assert.match(JSON.stringify(normalReportRows(recovered)),/6600/);assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/6600/);assert.match(normalHtml(recovered),/Não é demonstração contabilística/);
const p=plan();p.monthlyOperatingExpenses='110';recovered.business_plan_snapshot=serializeBusinessPlanSnapshot(p);assert.equal(normalCurrent(recovered,saved),false);assert.throws(()=>normalHtml(recovered));const next=recordNormal(recovered,saved);assert.deepEqual(next.history[0],b.result);assert.equal(next.result.model.partialAdjustedResult.numerator,'6480');
fs.mkdirSync('outputs/irpc-l6',{recursive:true});fs.writeFileSync('outputs/irpc-l6/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6/preparation.html',normalHtml(d));
});
test('opportunities remain separate and malformed persisted data is preserved with explicit error',()=>{
const d=draft();d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,book()));d.context_opportunity='another';assert.equal(normalBook(d).rows.length,0);const b=book();b.rows[0].amount=100;assert.throws(()=>saveNormalBook(d,b));const old=d.modelo_a_snapshot;assert.throws(()=>saveNormalBook(d,{...book(),rows:[row(),row()]}));assert.equal(d.modelo_a_snapshot,old);
});
+67
View File
@@ -0,0 +1,67 @@
import {pecCase} from './helpers/irpc-pec-case.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import {annualBook,newAnnualBook,newAnnualPpc,reuseAnnualAssessment,calculateAnnual,recordAnnual,saveAnnualBook,annualCurrent,annualReportRows,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const value=x=>x?Number(x.numerator)/Number(x.denominator):null;
const now='2026-10-27T15:00:00+02:00';
const model=({d,b})=>calculateAnnual(d,b);
const indeterminate=c=>{const m=model(c);assert.equal(m.status,'indeterminate',JSON.stringify(m.pending));assert.equal(m.arithmeticBalance,null);assert.equal(m.pecDeduction.deducted,null);assert.equal(m.pecDeduction.remaining,null);return m;};
test('PEC precedes PPC; 1408 - 200 - 500 = 708, no official consumption',()=>{
const m=model(pecCase());assert.deepEqual(m.pending,[]);assert.equal(value(m.pecDeduction.deducted),200);assert.equal(value(m.pecDeduction.afterPec),1208);assert.equal(value(m.payable),708);assert.equal(m.pecDeduction.carryCreated,false);assert.equal(m.officialBalancesConsumed,false);
});
test('existing received L5 proof is reused after PEC and cannot become another PEC receipt',()=>{
const c=pecCase('200','500','300');assert.equal(value(model(c).payable),408);
Object.assign(c.b.pecPayments.rows[0],{document:'QA-RF-PEC-2026',proofAmount:'300'});
assert.match(indeterminate(c).pending.join(' '),/natureza/);
});
test('PEC cap never generates refund; PPC can still generate recoverable balance',()=>{
const a=model(pecCase('2000','0'));assert.equal(value(a.pecDeduction.deducted),1408);assert.equal(value(a.pecDeduction.remaining),592);assert.equal(value(a.arithmeticBalance),0);assert.equal(value(a.recoverable),0);
const b=model(pecCase('2000','500'));assert.equal(value(b.recoverable),500);assert.equal(value(b.pecDeduction.remaining),592);assert.equal(value(b.pecDeduction.afterPec),0);
});
test('previous use explicit, precise decimals and available portion are respected',()=>{
const c=pecCase('200.25','0');Object.assign(c.b.pecPayments.rows[0],{proofAmount:'300.75',previouslyUsed:'100.50'});assert.equal(value(model(c).payable),1207.75);
c.b.pecPayments.rows[0].amount='200.26';indeterminate(c);
});
test('absence, known zero and missing PEC are distinct; contradictory rows block',()=>{
const c=pecCase();c.b.pecPayments.rows=[];c.b.pecPayments.state='absent';assert.equal(value(model(c).payable),908);
c.b.pecPayments.state='';indeterminate(c);c.b.pecPayments.state='known';indeterminate(c);
const z=pecCase('0');assert.equal(value(model(z).payable),908);z.b.pecPayments.rows[0].previouslyUsed='';indeterminate(z);
z.b.pecPayments.state='absent';indeterminate(z);
});
test('payment proof, unused balance, entity, year and currency cannot be presumed',()=>{
for(const change of[{effective:''},{effective:'no'},{document:''},{reference:''},{priorUseReference:''},{previouslyUsed:''},{previouslyUsed:'201'},{amount:'201'},{amount:'1.001'},{subjectNuit:'999999999'},{year:'2025'},{date:'2025-08-31'},{date:'2026-02-30'},{currency:'USD'}]){const c=pecCase();Object.assign(c.b.pecPayments.rows[0],change);indeterminate(c);}
});
test('same physical proof cannot be duplicated, re-dated or relabelled PPC',()=>{
const a=pecCase();a.b.pecPayments.rows.push({...a.b.pecPayments.rows[0],id:'copy',date:'2026-09-01'});assert.match(indeterminate(a).pending.join(' '),/repetido|saldo disponível/);
const b=pecCase();Object.assign(b.b.pecPayments.rows[0],{document:b.b.ppc.rows[0].document,proofAmount:'500'});assert.match(indeterminate(b).pending.join(' '),/naturezas|natureza incompatível/);
});
test('cross-context allocations respect prior use and changing external claims invalidates current result',()=>{
const c=pecCase('100','0'),r=c.b.pecPayments.rows[0];r.proofAmount='500';r.previouslyUsed='100';
const other=structuredClone(c.b);other.pecPayments.rows[0].amount='300';
const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);
c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const saved=annualBook(c.d);assert.equal(annualCurrent(c.d,saved),true);assert.equal(value(saved.result.model.payable),1308);
const edit=JSON.parse(c.d.modelo_a_snapshot);edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].amount='301';c.d.modelo_a_snapshot=JSON.stringify(edit);assert.equal(annualCurrent(c.d,saved),false);assert.match(indeterminate({...c,b:saved}).pending.join(' '),/saldo disponível/);
edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].amount='100';edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].previouslyUsed='0';c.d.modelo_a_snapshot=JSON.stringify(edit);indeterminate({...c,b:saved});
});
test('other contexts cannot change PEC titular, origin or payment nature',()=>{
for(const change of[{subjectNuit:'999999999'},{year:'2025'}]){const c=pecCase('100','0'),other=structuredClone(c.b);other.pecPayments.rows[0].proofAmount='500';Object.assign(other.pecPayments.rows[0],change);const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);indeterminate(c);}
const c=pecCase('100','0'),other=structuredClone(c.b);other.ppc.rows=[{...c.b.pecPayments.rows[0],amount:'50',proofAmount:'100'}];other.ppc.state='known';other.pecPayments.rows=[];other.pecPayments.state='absent';const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);indeterminate(c);
});
test('positive special credits remain scoped and cannot be silently skipped before PEC',()=>{
const c=pecCase();c.b.credits.amount='10';assert.match(indeterminate(c).pending.join(' '),/Créditos/);assert.equal(value(model(c).pecDeduction.available),200);
});
test('save/recover/recalculate preserves old annual results and all other fiscal books, plan and reports',()=>{
const c=pecCase(),before=JSON.parse(c.d.modelo_a_snapshot),plan=c.d.business_plan_snapshot;
const legacy=structuredClone(c.b);delete legacy.pecPayments;c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,legacy,now));const old=structuredClone(annualBook(c.d).result);
c.b={...annualBook(c.d),pecPayments:c.b.pecPayments};c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const back=annualBook(JSON.parse(JSON.stringify(c.d)));
assert.deepEqual(back.history,[old]);assert.equal(value(back.result.model.payable),708);assert.equal(annualCurrent(c.d,back),true);const after=JSON.parse(c.d.modelo_a_snapshot);delete after.irpcAnnual;assert.deepEqual(after,before);assert.equal(c.d.business_plan_snapshot,plan);
const html=annualHtml(c.d);assert.match(html,/PEC — dedução limitada/);assert.match(html,/708.00/);assert.match(html,/sem reembolso PEC/);
const dossier=createProjectDossierReport(c.d,{selectedSections:new Set(['finance'])});assert.match(JSON.stringify(dossier),/PEC — dedução limitada/);
const frozen=structuredClone(back.result);back.pecPayments.rows[0].amount='100';c.d.modelo_a_snapshot=saveAnnualBook(c.d,back);assert.equal(annualCurrent(c.d,back),false);assert.deepEqual(back.result,frozen);assert.match(annualReportRows(c.d).flat().join(' '),/Histórico/);assert.throws(()=>annualHtml(c.d),/fontes actuais/);
});
test('malformed PEC extension never replaces legacy snapshots',()=>{
const c=pecCase(),original=c.d.modelo_a_snapshot;c.b.pecPayments.rows[0].previouslyUsed=null;assert.throws(()=>saveAnnualBook(c.d,c.b),/não recuperáveis/);assert.equal(c.d.modelo_a_snapshot,original);
});
@@ -0,0 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {irpcBook,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
test('each saved calculation identifies income category rate and Portuguese nature without recalculation',()=>{
const d=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-grouped-preserved.json',import.meta.url),'utf8')),before=JSON.stringify(irpcBook(d)),rows=irpcReportRows(d);
assert.equal(rows.length,6);for(const r of rows){assert.match(r[2],/enquadramento:/);assert.match(r[2],/taxa:(10|20)%/);assert.match(r[3],/Retenção definitiva|Imposto por conta/);}
assert.match(rows[2][2],/Propriedade intelectual\/industrial/);assert.match(rows[3][2],/Serviços de telecomunicações/);assert.match(rows[4][3],/Imposto por conta/);assert.match(rows[0][3],/Retenção definitiva/);assert.equal(JSON.stringify(irpcBook(d)),before);const html=irpcPreparationHtml(d);assert.match(html,/Retenção definitiva/);assert.match(html,/Imposto por conta/);assert.ok(!html.includes(';definitive;'));
});
+53
View File
@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newIrpcRow,emptyIrpcOrdinaryFacts,recordIrpc,saveIrpcBook,irpcBook,irpcCurrent,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
import {newIrpcReliefPreparation} from '../../resources/js/lib/irpc-relief-preparation.ts';
import {createProjectDossierReport,createProjectDossierPrintHtml} from '../../resources/js/lib/project-dossier-report.ts';
const draft=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-grouped-preserved.json',import.meta.url),'utf8'));
const row=(id='dispensa')=>({...newIrpcRow(id),counterparty:'QA sociedade',documentRef:'QA-'+id,base:'100000',kind:'services',recipientIrpc:'yes',payerIrpc:'yes',residence:'resident',mozambique:'yes',qualificationRef:'QA contrato e cadastro do titular',specialRelief:'claimed',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'QA custo'}],ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'QA contrato de serviços'},reliefPreparation:{...newIrpcReliefPreparation(),rule:'cirpc68d',instrument:'CIRPC Lei34/2007',article:'68(d)',validFrom:'2026-01-01',validTo:'2026-12-31',conditions:'QA titular e rendimento sujeitos a IRPC',evidence:'QA cadastro e contrato existentes',facts:{subjectToIrpc:'yes'}}});
const treaty=(id='convention')=>({...row(id),kind:'royalty',residence:'nonresident',permanentEstablishment:'no',reliefPreparation:{...row().reliefPreparation,kind:'treaty',rule:'ptMz12',instrument:'CDT PT-MZ RAR36/1992 e Protocolo2009',article:'12',country:'PT',residenceDocument:'QA certificado fiscal Portugal 2026',facts:{treatyResident:'yes',beneficialOwner:'yes',payerResidentMz:'yes',armLength:'yes',royaltyDefinition:'yes'}}});
const calc=(r)=>recordIrpc(draft(),{version:1,rows:[r],payments:[]}).result.rows[0];
test('68d dispenses resident and attributable EP services/equipment without exempting the income',()=>{
for(const kind of ['services','equipment'])for(const patch of [{},{residence:'nonresident',permanentEstablishment:'yes',attributable:'yes'}]){
const v=calc({...row(),kind,...patch});assert.equal(v.status,'calculated');assert.equal(v.treatment,'dispensed');assert.equal(v.nature,'on_account');assert.equal(v.tax.numerator,'0');assert.equal(v.outstanding.numerator,'0');assert.match(v.reason,/não é isenção/);
}
});
test('68f requires own-property management and absence of fiscal transparency',()=>{
const r=row();r.kind='rent';r.ordinary.rentOwnPropertyManagement='yes';r.reliefPreparation.rule='cirpc68f';r.reliefPreparation.article='68(f)';r.reliefPreparation.facts.notTransparent='yes';assert.equal(calc(r).treatment,'dispensed');
for(const value of ['', 'no']){r.reliefPreparation.facts.notTransparent=value;assert.equal(calc(r).tax,null);}r.reliefPreparation.facts.notTransparent='yes';r.ordinary.rentOwnPropertyManagement='no';assert.equal(calc(r).tax,null);
});
test('definitive nonresident equipment is never dispensed under68d',()=>{
for(const patch of [{permanentEstablishment:'no'},{permanentEstablishment:'yes',attributable:'no'},{permanentEstablishment:''}])assert.equal(calc({...row(),kind:'equipment',residence:'nonresident',...patch}).tax,null);
});
test('Portugal royalties and qualified equipment use10% gross definitive limit',()=>{
for(const kind of ['royalty','equipment']){const v=calc({...treaty(),kind});assert.equal(v.status,'calculated');assert.equal(v.treatment,'treaty');assert.equal(v.rate,10);assert.equal(v.tax.numerator,'10000');assert.equal(v.nature,'definitive');}
});
test('missing,negative,expired or contradictory evidence remains indeterminate,never zero',()=>{
const samples=[r=>r.reliefPreparation.evidence='',r=>r.reliefPreparation.residenceDocument='',r=>r.reliefPreparation.validTo='2026-09-30',r=>r.reliefPreparation.validFrom='2026-02-30',r=>r.reliefPreparation.article='7',r=>r.reliefPreparation.country='ZA',r=>r.permanentEstablishment='yes',r=>r.kind='digital_services'];
for(const fact of ['treatyResident','beneficialOwner','payerResidentMz','armLength','royaltyDefinition'])for(const value of ['', 'no'])samples.push(r=>r.reliefPreparation.facts[fact]=value);
for(const mutate of samples){const r=treaty();mutate(r);const v=calc(r);assert.equal(v.status,'indeterminate');assert.equal(v.tax,null);assert.equal(v.rate,null);}
});
test('independent rows calculate beside insufficient information; save recover report export and history',()=>{
const d=draft(),prior=irpcBook(d),insufficient=treaty('missing');insufficient.reliefPreparation.residenceDocument='';
const rent=row('property-dispensation');rent.kind='rent';rent.ordinary.rentOwnPropertyManagement='yes';rent.reliefPreparation.rule='cirpc68f';rent.reliefPreparation.article='68(f)';rent.reliefPreparation.facts.notTransparent='yes';
const b=recordIrpc(d,{...prior,rows:[...prior.rows,row(),rent,treaty(),insufficient]});d.modelo_a_snapshot=saveIrpcBook(d,b);
const recovered=JSON.parse(JSON.stringify(d)),book=irpcBook(recovered);assert.deepEqual(book,b);assert.equal(irpcCurrent(recovered,book),true);assert.deepEqual(book.history.at(-1),prior.result);
assert.deepEqual(book.result.rows.slice(-3).map(r=>r.status),['calculated','calculated','indeterminate']);
const report=irpcReportRows(recovered).slice(-3);assert.match(report[0][2],/Dispensa de retenção/);assert.match(report[1][2],/Limite convencional/);assert.match(report[2][2],/Indeterminado/);assert.match(report[1][3],/QA certificado fiscal Portugal 2026/);
assert.match(irpcPreparationHtml(recovered),/10000/);book.rows.at(-2).reliefPreparation.facts.beneficialOwner='';assert.equal(irpcCurrent(recovered,book),false);assert.equal(recordIrpc(recovered,book).result.rows.at(-2).tax,null);
fs.mkdirSync('outputs/irpc-reliefs',{recursive:true});fs.writeFileSync('outputs/irpc-reliefs/calculated.json',JSON.stringify(recovered));fs.writeFileSync('outputs/irpc-reliefs/preparation.html',irpcPreparationHtml(recovered));
});
test('dispensa with payment attached requires reconciliation and retains the original proof',()=>{
const r=row();r.allocations=[{paymentId:'p',amount:'100'}];const p={id:'p',kind:'remittance',reference:'QA prova',date:'2026-10-20',amount:'100',currency:'MZN'};const b=recordIrpc(draft(),{version:1,rows:[r],payments:[p]});assert.equal(b.result.rows[0].status,'indeterminate');assert.deepEqual(b.payments,[p]);
});
test('unsupported relief and malformed persisted conditions are not accepted as a benefit',()=>{
const r=row();r.reliefPreparation.rule='';assert.equal(calc(r).tax,null);r.reliefPreparation.facts.subjectToIrpc='no';assert.equal(calc(r).tax,null);r.reliefPreparation.facts.subjectToIrpc=42;assert.throws(()=>saveIrpcBook(draft(),{version:1,rows:[r],payments:[]}));
});
test('financial dossier includes each IRPC result, evidence and insufficient information',async()=>{
const d=draft(),missing=treaty('missing');missing.reliefPreparation.residenceDocument='';d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,{version:1,rows:[row(),treaty(),missing],payments:[]}));
const report=createProjectDossierReport(d,{selectedSections:new Set(['finance'])}),text=JSON.stringify(report);
for(const label of ['Dispensa de retenção','Limite convencional','Indeterminado','QA certificado fiscal Portugal 2026'])assert.ok(text.includes(label),label);
const html=await createProjectDossierPrintHtml(report);assert.match(html,/Dispensa de retenção/);assert.match(html,/prova de residência/);
});
@@ -0,0 +1,41 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {validateSpecial} from '../../resources/js/lib/irpc-special.ts';
import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const read=name=>JSON.parse(fs.readFileSync(new URL('./fixtures/'+name,import.meta.url),'utf8'));
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
function accepted(){const d=read('irpc-l7-preserved.json').acceleration,f=read('irpc-l7-start-preserved.json');d.business_plan_snapshot=f.plan;d.modelo_a_snapshot=saveNormalBook(d,f.book);return{d,b:normalBook(d)};}
function setup(period='cessation',prior=true){
const{d,b}=accepted(),months=period==='annual'?12:6,p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);
if(period==='annual')delete b.periodsFx;else Object.assign(b.periodsFx,{kind:'cessation',from:'2026-01-01',to:'2026-06-30',reference:'QA cessação documentada30Junho2026;meses completos sem transmissão presumida'});
b.schedules.interest.amount=String(months*20);
Object.assign(b.depreciation.assets[0],{start:prior?'2025-01-01':'2026-01-15',timing:'months',accounting:String(months*10),previousAccounting:prior?'150':'0',previousFiscal:prior?'100':'0',priorAvailable:prior?'50':'0',previousReference:prior?'QA quotas contabilísticas150,fiscais100,parcela50 temporariamente tributada;sem encargos permanentemente excluídos':'QA ausência declarada de quotas anteriores;activo entrou em2026',exitState:'absent',exitDate:''});
for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])Object.assign(part.qualification,{from:'2026-01-01',to:period==='annual'?'2026-12-31':'2026-06-30',periodReference:period==='annual'?'QA ano civil2026':b.periodsFx.reference});
delete b.special.acceleration.monthly.startPeriod;
b.special.acceleration.monthly.continuation={version:1,period,reference:'QA CBF16 vigente no sujeito/projecto e período2026 declarado;sem cumulação',historyReference:'QA activo novo2025/2026,método constante e duodécimos desde entrada;quotas anteriores reconciliadas no mapaL6,não recalculadas',monthlyFromStart:'yes'};return{d,b};
}
test('accepted12 result,key and fullhistory remain exact without continuation',()=>{const{d,b}=accepted();assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),b.result.model);assert.deepEqual(normalBook(d).history,b.history);});
test('cessation reuses6 months without assuming assetexit;50 normal,75 accelerated capped60,additional10',()=>{const{d,b}=setup('cessation',false),asset=JSON.stringify(b.depreciation),period=JSON.stringify(b.periodsFx),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(m.version,'IRPC-normal-adjustments/13');assert.equal(a.months,6);assert.equal(a.exit.state,'absent');assert.equal(val(a.normalQuota),50);assert.equal(val(a.acceleratedQuota),75);assert.equal(val(a.acceleratedDeduction),60);assert.equal(val(a.additionalDeduction),10);assert.equal(val(m.taxableProfit),3130);assert.equal(val(m.colecta),1001.6);assert.equal(val(s.taxableProfit),3120);assert.equal(val(s.taxableBase),3120);assert.equal(val(s.colecta),998.4);assert.equal(JSON.stringify(b.depreciation),asset);assert.equal(JSON.stringify(b.periodsFx),period);assert.equal(s.finalBalance.payable,null);});
test('cessation with priorbook150,fiscal100,available50 recovers15 once and adds only25 to normaldeduction',()=>{const{d,b}=setup(),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(a.months,6);assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),75);assert.equal(val(a.prior.normalUsed),0);assert.equal(val(a.prior.acceleratedUsed),15);assert.equal(val(a.prior.normalRemaining),50);assert.equal(val(a.prior.acceleratedRemaining),35);assert.equal(val(a.prior.normalFiscalRemaining),850);assert.equal(val(a.prior.acceleratedFiscalRemaining),825);assert.equal(val(s.acceleration.additionalDeduction),25);assert.equal(val(s.taxableProfit),3105);assert.equal(val(s.taxableBase),3105);assert.equal(val(s.colecta),993.6);assert.equal(b.depreciation.assets[0].priorAvailable,'50');assert.equal(b.depreciation.assets[0].previousFiscal,'100');});
test('annual2026 continuation preserves2025 actualbalances and records30priorused within150 deduction,additional50',()=>{const{d,b}=setup('annual'),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(a.months,12);assert.equal(val(a.normalQuota),100);assert.equal(val(a.acceleratedQuota),150);assert.equal(val(a.prior.acceleratedUsed),30);assert.equal(val(a.prior.acceleratedRemaining),20);assert.equal(val(s.acceleration.additionalDeduction),50);assert.equal(val(m.taxableProfit),6260);assert.equal(val(s.taxableProfit),6210);assert.equal(val(s.colecta),1987.2);assert.deepEqual(s.acceleration.normal.input,s.acceleration.accelerated.input);});
test('documented assetexit excludes eventmonth and keeps exact fractional quotas;cessation date does not edit exit',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{exitState:'known',exitDate:'2026-06-20'});const a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(a.months,5);assert.deepEqual(a.normalQuota,fraction(125n,3n));assert.deepEqual(a.acceleratedQuota,fraction(125n,2n));assert.deepEqual(a.additionalDeduction,fraction(125n,6n));assert.deepEqual(a.prior.acceleratedUsed,fraction(5n,2n));assert.deepEqual(a.prior.acceleratedRemaining,fraction(95n,2n));});
test('remaining fiscalbalance and accounted/priorcaps bind both calculations;available amount is never a second deduction',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{previousAccounting:'1000',previousFiscal:'980',priorAvailable:'20'});let a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),20);assert.equal(val(a.acceleratedDeduction),20);assert.equal(val(a.additionalDeduction),0);assert.equal(val(a.prior.acceleratedFiscalRemaining),0);const second=setup(),p=JSON.parse(second.d.business_plan_snapshot);p.plan.assetDepreciation='0';second.d.business_plan_snapshot=JSON.stringify(p);second.b.depreciation.assets[0].accounting='0';a=calculateNormal(second.d,second.b).special.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),50);assert.equal(val(a.additionalDeduction),0);assert.equal(val(a.prior.acceleratedRemaining),0);});
test('missing support or missing priorbalance blocks only dependent CBF16;absence and negative remain explicit',()=>{for(const key of['reference','historyReference','monthlyFromStart']){const{d,b}=setup();b.special.acceleration.monthly.continuation[key]='';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);}const missing=setup();missing.b.depreciation.assets[0].previousFiscal='';const pending=calculateNormal(missing.d,missing.b);assert.equal(pending.special.acceleration.additionalDeduction,null);assert.match(pending.depreciation.pending.join(' '),/em falta/);assert.equal(missing.b.depreciation.assets[0].previousFiscal,'');for(const state of['negative','absent']){const{d,b}=setup();if(state==='negative')b.special.acceleration.rows[0].eligible='no';else{b.special.acceleration.rows=[];Object.assign(b.special.acceleration.qualification,{state:'absent',eligible:''});}const s=calculateNormal(d,b).special;assert.equal(val(s.acceleration.additionalDeduction),0);assert.equal(s.acceleration.status,state==='absent'?'absent':'calculated');if(state==='negative')assert.equal(s.acceleration.rows[0].status,'not_applicable');}});
test('otherstartyears,nonmonthlyhistory,transition,cumulation and prioramounts for currentyearentry stay out of scope',()=>{for(const mutate of[b=>b.depreciation.assets[0].start='2024-01-01',b=>b.special.acceleration.monthly.continuation.monthlyFromStart='no',b=>b.periodsFx.kind='transition',b=>b.special.acceleration.noOtherBenefits='no',b=>b.depreciation.assets[0].start='2026-01-01']){const{d,b}=setup();mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);}});
test('save-recover13 preserves12result and allhistorical balances;recalc idempotent and reports reconciliation without officialconsumption',()=>{const{d,b}=setup(),history=structuredClone(b.history),old=structuredClone(b.result),asset=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot,others=JSON.parse(d.modelo_a_snapshot).irpcWithholdings;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history,[...history,old]);assert.equal(old.model.version,'IRPC-normal-adjustments/12');assert.equal(JSON.stringify(back.depreciation),asset);assert.equal(d.business_plan_snapshot,plan);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,others);const html=normalHtml(d);assert.match(html,/2026-01-01–2026-06-30/);assert.match(html,/recuperada CBF16:15.00/);assert.match(html,/saldo anterior aritmético CBF16:35.00/);assert.match(html,/sem consumo de saldos oficiais/);assert.match(html,/colecta sobre a base:993.60/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/quotas contabilísticas anteriores/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(val(back.result.model.special.acceleration.additionalDeduction),25);assert.equal(back.depreciation.assets[0].priorAvailable,'50');back.depreciation.assets[0].previousFiscal='110';assert.equal(normalCurrent(d,back),false);});
test('invalid or overlapping optionalpreparation cannot replace preservedbooks',()=>{for(const c of[null,{version:2,period:'annual',reference:'QA',historyReference:'QA',monthlyFromStart:'yes'},{version:1,period:'other',reference:'QA',historyReference:'QA',monthlyFromStart:'yes'}]){const{b}=setup();b.special.acceleration.monthly.continuation=c;assert.throws(()=>validateSpecial(b.special),/não recuperável/);}const{b}=setup();b.special.acceleration.monthly.startPeriod={version:1,reference:'QA'};assert.throws(()=>validateSpecial(b.special),/incompatível/);});
test('prepare only compatible new guardedAPI cases with missing,negative and absence variants',()=>{const cases={cessation:setup('cessation',false),prior:setup(),annual:setup('annual'),exit:setup(),missing:setup(),negative:setup(),absent:setup()};Object.assign(cases.exit.b.depreciation.assets[0],{exitState:'known',exitDate:'2026-06-20'});cases.missing.b.special.acceleration.monthly.continuation.historyReference='';cases.negative.b.special.acceleration.rows[0].eligible='no';cases.absent.b.special.acceleration.rows=[];Object.assign(cases.absent.b.special.acceleration.qualification,{state:'absent',eligible:''});fs.mkdirSync('outputs/irpc-l7-cessation-prior',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T18:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-cessation-prior/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-cessation-prior/'+name+'.html',normalHtml(d));}});
@@ -0,0 +1,34 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {validateSpecial} from '../../resources/js/lib/irpc-special.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const read=n=>JSON.parse(fs.readFileSync(new URL('./fixtures/'+n,import.meta.url),'utf8'));
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
function accepted(){const d=read('irpc-l7-preserved.json').acceleration,f=read('irpc-l7-continuation-preserved.json');d.business_plan_snapshot=f.plan;d.modelo_a_snapshot=saveNormalBook(d,f.book);return{d,b:normalBook(d)};}
function setup(expense=false){const{d,b}=accepted();const i=b.special;Object.assign(i.cooperative,{kind:'agrarian',allIncomeEligible:'yes',withheldIncome:'absent'});Object.assign(i.cooperative.qualification,{state:'known',eligible:'yes',reference:'QA cooperativa agrária12,base ordinária integralmente elegível;sem rendimentos sujeitos a retenção'});i.acceleration.rows[0].kind='agroindustrial_equipment';i.combinations={version:1,reference:'QA mesmo sujeito/projecto/período;reduçãoCIRPC12 eCBF16 genérico,efeitos em etapas distintas',specificState:'absent',specificReference:'QA sem estatuto/benefício específicoCBF20–48 neste projecto/período;CBF13',specificException:'',otherState:'absent',otherReference:'QA sem outros benefícios além dos tratamentosL7 registados'};if(expense){const old=normalBook(read('irpc-l7-preserved.json').expense);b.schedules.rows.push(structuredClone(old.schedules.rows[0]));b.profit.topics.expenseCorrections=structuredClone(old.profit.topics.expenseCorrections);i.expenses=structuredClone(old.special.expenses);Object.assign(i.expenses.qualification,{from:b.periodsFx.from,to:b.periodsFx.to});}return{d,b};}
test('accepted13 model,key and allhistorical balances reproduce exactly without optionalcombination',()=>{const{d,b}=accepted();assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),b.result.model);assert.deepEqual(normalBook(d).history,b.history);});
test('qualified cooperative12 and genericCBF16 combine in order,additional25 once then16percent,prior15 already included',()=>{const{d,b}=setup(),assets=JSON.stringify(b.depreciation),m=calculateNormal(d,b),s=m.special,c=s.combinations;assert.equal(m.version,'IRPC-normal-adjustments/14');assert.equal(c.status,'compatible');assert.equal(val(m.taxableProfit),3130);assert.equal(val(m.colecta),1001.6);assert.equal(val(s.acceleration.additionalDeduction),25);assert.equal(val(s.taxableProfit),3105);assert.equal(val(s.taxableBase),3105);assert.equal(val(s.cooperative.rate),16);assert.equal(val(s.colecta),496.8);assert.equal(val(s.combinedColecta),496.8);assert.equal(val(c.assetClaims[0].acceleratedDeduction),75);assert.equal(val(c.assetClaims[0].priorRecoveredIncluded),15);assert.equal(val(c.assetClaims[0].additionalDeduction),25);assert.equal(JSON.stringify(b.depreciation),assets);assert.equal(s.finalBalance.payable,null);assert.doesNotMatch(s.acceleration.source,/Sem cumulação[.]/);});
test('autonomousexpense35percent is not halved by cooperative rate or added again to the taxableprofit',()=>{const{d,b}=setup(true),m=calculateNormal(d,b),s=m.special;assert.equal(val(m.taxableProfit),3230);assert.equal(val(s.taxableProfit),3205);assert.equal(val(s.taxableBase),3205);assert.equal(val(s.expenses.rows[0].l6Adjustment),100);assert.equal(val(s.expenses.rows[0].additionalProfitAdjustment),0);assert.equal(val(s.expenses.autonomousTax),35);assert.equal(val(s.colecta),512.8);assert.equal(val(s.combinedColecta),547.8);assert.equal(val(s.combinations.limits.autonomousRate),35);});
test('specificCBF22 with generic16 and declaredabsence of exception is incompatible,neither benefit automatically selected',()=>{const{d,b}=setup();Object.assign(b.special.combinations,{specificState:'known',specificReference:'QA CBF22 benefício específico infra-estruturas declarado no mesmo projecto/período',specificException:'absent'});const input=structuredClone(b.special),m=calculateNormal(d,b),s=m.special;assert.equal(s.combinations.status,'incompatible');assert.equal(s.acceleration.additionalDeduction,null);assert.equal(s.taxableProfit,null);assert.equal(s.colecta,null);assert.equal(s.combinedColecta,null);assert.equal(val(m.colecta),1001.6);assert.deepEqual(b.special,input);assert.match(s.pending.join(' '),/IncompatibilidadeCBF4\/13/);});
test('claimedexception,unknownspecificpresence,otherbenefit or missingsupport remain indeterminate without provisionalrate',()=>{for(const patch of[{specificState:'known',specificReference:'QA excepção invocada,artigo/limite não correspondidos',specificException:'claimed'},{specificState:''},{otherState:'known',otherReference:'QA outro benefício fora dos ramos calculados'},{reference:''},{specificReference:''},{otherReference:''}]){const{d,b}=setup();Object.assign(b.special.combinations,patch);const m=calculateNormal(d,b);assert.equal(m.special.combinations.status,'indeterminate');assert.equal(m.special.colecta,null);assert.equal(val(m.colecta),1001.6);}});
test('missingcooperativeperiod or mixedincome blocks its dependenttotal without choosing eligibility from location',()=>{for(const patch of[{allIncomeEligible:'no'},{withheldIncome:'known'}]){const{d,b}=setup();Object.assign(b.special.cooperative,patch);d.province_id='QA-ZEE-address';const s=calculateNormal(d,b).special;assert.equal(s.colecta,null);assert.match(s.cooperative.reason,/mistas\/retidas/);}const{d,b}=setup();b.special.cooperative.qualification.from='2026-02-01';assert.equal(calculateNormal(d,b).special.colecta,null);});
test('duplicateassetqualification cannot duplicate CBF16 deduction;remainingfiscal20 cap keepsadditionalzero',()=>{const{d,b}=setup();b.special.acceleration.rows.push({...b.special.acceleration.rows[0],id:'same-asset-again'});let s=calculateNormal(d,b).special;assert.equal(s.acceleration.additionalDeduction,null);assert.match(s.acceleration.reason,/Activo repetido/);const cap=setup();Object.assign(cap.b.depreciation.assets[0],{previousAccounting:'1000',previousFiscal:'980',priorAvailable:'20'});s=calculateNormal(cap.d,cap.b).special;assert.equal(val(s.acceleration.additionalDeduction),0);assert.equal(val(s.combinations.assetClaims[0].acceleratedDeduction),20);assert.equal(val(s.combinations.assetClaims[0].remainingFiscal),0);});
test('negativecooperative or absentCBF16 retains the other independentlyqualified treatment',()=>{const negative=setup();negative.b.special.cooperative.qualification.eligible='no';let s=calculateNormal(negative.d,negative.b).special;assert.equal(s.cooperative.status,'not_applicable');assert.equal(val(s.colecta),993.6);const absent=setup();absent.b.special.acceleration.rows=[];Object.assign(absent.b.special.acceleration.qualification,{state:'absent',eligible:''});s=calculateNormal(absent.d,absent.b).special;assert.equal(s.acceleration.status,'absent');assert.equal(val(s.acceleration.additionalDeduction),0);assert.equal(val(s.colecta),500.8);});
test('save-recover14 appends13 verbatim,leaves inputbalances/plan/otherbooks unchanged,and report explains order and no double deduction',()=>{const{d,b}=setup(true),old=structuredClone(b.result),history=structuredClone(b.history),assets=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot,other=JSON.parse(d.modelo_a_snapshot).irpcWithholdings;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history,[...history,old]);assert.equal(old.model.version,'IRPC-normal-adjustments/13');assert.equal(JSON.stringify(back.depreciation),assets);assert.equal(d.business_plan_snapshot,plan);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,other);const html=normalHtml(d);assert.match(html,/Combinação permitida no recorte/);assert.match(html,/parcela anterior já incluída na dedução CBF16:15.00/);assert.match(html,/única diferença adicional:25.00/);assert.match(html,/imposto autónomo independente:35.00/);assert.match(html,/colecta combinada:547.80/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/Cumulações — qualificação/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(val(back.result.model.special.combinedColecta),547.8);assert.equal(back.depreciation.assets[0].priorAvailable,'50');back.special.combinations.otherState='known';assert.equal(normalCurrent(d,back),false);});
test('positive unclassified L6benefit is preserved and blocks another deduction until reconciled;zero base never offsets autonomous35',()=>{const extra=setup();extra.b.base.benefits={state:'known',amount:'100',reference:'QA dedução já declarada sem correspondência à combinação'};let m=calculateNormal(extra.d,extra.b);assert.equal(m.special.combinations.status,'indeterminate');assert.equal(m.special.colecta,null);assert.notEqual(m.taxableProfit,null);assert.equal(extra.b.base.benefits.amount,'100');assert.match(m.special.pending.join(' '),/já registado emL6/);const loss=setup(true),p=JSON.parse(loss.d.business_plan_snapshot);p.plan.products[0].unitPrice='10';loss.d.business_plan_snapshot=JSON.stringify(p);loss.b.schedules.currentLossReference='QA perda própria do período reconciliada;sem transporte oficial automático';m=calculateNormal(loss.d,loss.b);assert.ok(val(m.special.taxableProfit)<0);assert.equal(val(m.special.taxableBase),0);assert.equal(val(m.special.colecta),0);assert.equal(val(m.special.expenses.autonomousTax),35);assert.equal(val(m.special.combinedColecta),35);});
test('malformedoptionalcombination cannot overwrite acceptedbooks',()=>{for(const value of[null,{version:2},{version:1,reference:'QA',specificState:'unknown',specificReference:'QA',specificException:'',otherState:'absent',otherReference:'QA'}]){const{b}=setup();b.special.combinations=value;assert.throws(()=>validateSpecial(b.special),/não recuperável/);}});
test('prepare only new guardedAPI cases;accepted13 proofs remain separate',()=>{const cases={combined:setup(),autonomous:setup(true),incompatible:setup(),missing:setup(),exception:setup(),negative:setup(),absent:setup()};Object.assign(cases.incompatible.b.special.combinations,{specificState:'known',specificReference:'QA CBF22 no mesmo projecto/período',specificException:'absent'});cases.missing.b.special.combinations.specificReference='';Object.assign(cases.exception.b.special.combinations,{specificState:'known',specificReference:'QA excepção expressa invocada sem correspondência de artigo/limites',specificException:'claimed'});cases.negative.b.special.cooperative.qualification.eligible='no';cases.absent.b.special.acceleration.rows=[];Object.assign(cases.absent.b.special.acceleration.qualification,{state:'absent',eligible:''});fs.mkdirSync('outputs/irpc-l7-combinations',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T16:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-combinations/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-combinations/'+name+'.html',normalHtml(d));}});
@@ -0,0 +1,30 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {newSpecialInput,validateSpecial} from '../../resources/js/lib/irpc-special.ts';
import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const frozen=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l7-preserved.json',import.meta.url),'utf8'));
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
const q=state=>({state,eligible:state==='known'?'yes':'',from:'2026-01-01',to:'2026-12-31',reference:'QA condições/ausência documentadas no sujeito/projecto',periodReference:'QA ano civil2026,CBF16 e Decreto72/2013art9'});
function setup(account='60',start='2026-07-15'){
const d=frozen().acceleration,b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation=account;d.business_plan_snapshot=JSON.stringify(p);
Object.assign(b.depreciation.assets[0],{accounting:account,start,timing:'months'});
b.special.acceleration.monthly={version:1,reference:'QA opção de duodécimos desde a entrada emuso,datas/meses do mesmo activoL6'};return{d,b};
}
test('accepted10 books remain current and reproduce exact original expense,cooperative and annualasset models;history is read unchanged',()=>{for(const[name,d]of Object.entries(frozen())){const b=normalBook(d),original=structuredClone(b.result);assert.equal(normalCurrent(d,b),true,name);assert.deepEqual(calculateNormal(d,b),original.model,name);assert.deepEqual(normalBook(d).result,original,name);}});
test('explicit monthly11 reuses6 L6months and limits75quota to60accounted,adding only10deduction',()=>{const{d,b}=setup(),original=JSON.stringify(b.depreciation),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(m.version,'IRPC-normal-adjustments/11');assert.equal(a.months,6);assert.equal(val(a.normalRate),10);assert.equal(val(a.acceleratedRate),15);assert.equal(val(a.normalQuota),50);assert.equal(val(a.acceleratedQuota),75);assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),60);assert.equal(val(a.additionalDeduction),10);assert.equal(val(m.taxableProfit),6310);assert.equal(val(m.colecta),2019.2);assert.equal(val(s.taxableProfit),6300);assert.equal(val(s.taxableBase),6300);assert.equal(val(s.colecta),2016);assert.equal(s.finalBalance.payable,null);assert.equal(JSON.stringify(b.depreciation),original);});
test('month timing comes from L6 including midmonth entry,January,October and December;never dailyproration',()=>{for(const[start,account,months,normal,accelerated]of[['2026-01-01','120',12,100,150],['2026-07-31','60',6,50,75],['2026-10-20','60',3,25,37.5],['2026-12-31','20',1,100/12,12.5]]){const{d,b}=setup(account,start),m=calculateNormal(d,b),a=m.special.acceleration.monthly.assets[0];assert.equal(a.months,months);assert.equal(val(a.normalQuota),normal);assert.equal(val(a.acceleratedQuota),accelerated);assert.equal(a.months,m.depreciation.assets[0].months);}});
test('December retains exact fractional difference and report never rounds fiscal amount',()=>{const{d,b}=setup('20','2026-12-15'),m=calculateNormal(d,b),a=m.special.acceleration.monthly.assets[0];assert.deepEqual(a.normalQuota,fraction(25n,3n));assert.deepEqual(a.acceleratedQuota,fraction(25n,2n));assert.deepEqual(a.additionalDeduction,fraction(25n,6n));d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));assert.match(normalHtml(d),/25\/6/);assert.match(normalHtml(d),/≈/);});
test('accounted caps of0,40,60,100 remain binding and no additional quota is invented',()=>{for(const[account,normal,accelerated,diff]of[['0',0,0,0],['40',40,40,0],['60',50,60,10],['100',50,75,25]]){const{d,b}=setup(account),s=calculateNormal(d,b).special,a=s.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),normal);assert.equal(val(a.acceleratedDeduction),accelerated);assert.equal(val(s.acceleration.additionalDeduction),diff);}});
test('new project building reuses land/residual basis and existing2percent table rate with monthlycap',()=>{const{d,b}=setup('50');Object.assign(b.depreciation.assets[0],{category:'commercial_building',cost:'5000',land:'1000',rate:'2'});b.special.acceleration.rows[0].kind='new_project_building';const a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(val(a.taxBasis),4000);assert.equal(val(a.normalQuota),40);assert.equal(val(a.acceleratedQuota),60);assert.equal(val(a.acceleratedDeduction),50);assert.equal(val(a.additionalDeduction),10);});
test('other existing assets stay on their normal quota;selected asset is neither copied nor deducted twice',()=>{const{d,b}=setup('80'),other=structuredClone(b.depreciation.assets[0]);b.depreciation.assets[0].accounting='60';Object.assign(other,{id:'other',assetKey:'other',document:'QA-other',description:'Mobiliário foraCBF16',category:'furniture',cost:'200',rate:'10',start:'2026-01-01',timing:'annual',accounting:'20'});b.depreciation.assets.push(other);const s=calculateNormal(d,b).special;assert.equal(s.acceleration.normal.assets.length,2);assert.equal(s.acceleration.accelerated.assets.length,2);assert.deepEqual(s.acceleration.normal.assets[1],s.acceleration.accelerated.assets[1]);assert.equal(val(s.acceleration.additionalDeduction),10);});
test('missingmonthlysupport blocks only CBF16 totals;normalreference and storedinput are retained',()=>{const{d,b}=setup();b.special.acceleration.monthly.reference='';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.status,'indeterminate');assert.equal(m.special.acceleration.additionalDeduction,null);assert.equal(val(m.colecta),2019.2);assert.equal(m.special.acceleration.monthly.assets[0].acceleratedQuota,null);assert.match(m.special.acceleration.reason,/Falta suporte/);});
test('negative condition,declaredabsence and missingqualification have distinct monthly outcomes',()=>{const negative=setup();negative.b.special.acceleration.rows[0].eligible='no';let s=calculateNormal(negative.d,negative.b).special;assert.equal(s.acceleration.rows[0].status,'not_applicable');assert.equal(val(s.acceleration.additionalDeduction),0);assert.equal(s.acceleration.monthly.assets[0].applied,false);assert.equal(s.acceleration.monthly.assets[0].acceleratedRate,null);for(const state of['absent','']){const{d,b}=setup();b.special.acceleration.rows=[];b.special.acceleration.qualification=q(state);b.special.acceleration.monthly.reference='';s=calculateNormal(d,b).special;assert.equal(s.acceleration.status,state==='absent'?'absent':'indeterminate');assert.equal(val(s.acceleration.additionalDeduction),state==='absent'?0:null);}});
test('newmonthlyscope excludes priorquotas,annualoption,exits and earlier/laterentry while retaining L6 reference',()=>{for(const mutate of[a=>a.previousAccounting='1',a=>a.timing='annual',a=>{a.exitState='known';a.exitDate='2026-12-01';},a=>a.start='2025-07-01',a=>a.start='2027-01-01']){const{d,b}=setup();mutate(b.depreciation.assets[0]);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.taxableProfit,null);assert.equal(m.special.acceleration.status,'indeterminate');}});
test('short fiscalperiod already supported in L6 is not silently included in this annual CBF16 increment',()=>{const d=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l6-values-preserved.json',import.meta.url),'utf8')),b=normalBook(d),model=setup().b.special;b.special=structuredClone(model);for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])part.qualification={...part.qualification,from:'2026-07-01'};Object.assign(b.depreciation.assets[0],{category:'machine',cost:'1000',rate:'10',accounting:'60',timing:'months',start:'2026-07-01'});const m=calculateNormal(d,b);assert.notEqual(m.colecta,null);assert.equal(m.special.acceleration.additionalDeduction,null);assert.match(m.special.acceleration.reason,/exercício anual civil2026/);});
test('monthlyexplicitoption never selects CBF from location;missingeligibility evidence remains pending',()=>{const{d,b}=setup();d.province_id='QA-ZEE-candidate';b.special.acceleration.recognitionReference='';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);assert.match(m.special.acceleration.reason,/reconhecimento10/);});
test('version11 save-recover preserves10history,originalasset,plan and otherbooks;recalculate stays idempotent',()=>{const{d,b}=setup(),old=structuredClone(b.result),asset=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),old);assert.equal(back.history.at(-1).model.version,'IRPC-normal-adjustments/10');assert.equal(JSON.stringify(back.depreciation),asset);assert.equal(d.business_plan_snapshot,plan);assert.ok(JSON.parse(d.modelo_a_snapshot).irpcWithholdings);const html=normalHtml(d);assert.match(html,/duodécimosL6:6/);assert.match(html,/diferença adicional:10.00/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/CBF16 por duodécimos/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(val(back.result.model.special.acceleration.additionalDeduction),10);assert.equal(val(back.result.model.special.colecta),2016);back.depreciation.assets[0].start='2026-10-01';assert.equal(normalCurrent(d,back),false);});
test('malformed optionalmonthly schema cannot overwrite oldbooks',()=>{for(const value of[null,{version:2,reference:'QA'},{version:1,reference:12}]){const{b}=setup();b.special.acceleration.monthly=value;assert.throws(()=>validateSpecial(b.special),/não recuperável/);}});
test('newmonthlycases only prepare guardedprivateAPI inputs and reports',()=>{const cases={monthly:setup(),fraction:setup('20','2026-12-15'),missing:setup(),negative:setup(),absent:setup()};cases.missing.b.special.acceleration.monthly.reference='';cases.negative.b.special.acceleration.rows[0].eligible='no';cases.absent.b.special.acceleration.qualification=q('absent');cases.absent.b.special.acceleration.rows=[];fs.mkdirSync('outputs/irpc-l7-months',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T16:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-months/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-months/'+name+'.html',normalHtml(d));}});
@@ -0,0 +1,37 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {validateSpecial} from '../../resources/js/lib/irpc-special.ts';
import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const read=name=>JSON.parse(fs.readFileSync(new URL('./fixtures/'+name,import.meta.url),'utf8'));
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
function accepted(){const d=read('irpc-l7-preserved.json').acceleration,f=read('irpc-l7-monthly-preserved.json');d.business_plan_snapshot=f.plan;d.modelo_a_snapshot=saveNormalBook(d,f.book);return{d,b:normalBook(d)};}
function setup(month=7){
const{d,b}=accepted(),p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);
const old=normalBook(read('irpc-l6-values-preserved.json'));b.periodsFx=structuredClone(old.periodsFx);b.periodsFx.from='2026-'+String(month).padStart(2,'0')+'-01';
b.periodsFx.reference='QA início documentado em'+b.periodsFx.from+';meses completos até31/12';
delete b.forms;const months=13-month;b.schedules.interest.amount=String(months*20);
Object.assign(b.depreciation.assets[0],{accounting:String(months*10),start:b.periodsFx.from.slice(0,8)+'15',timing:'months'});
for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])Object.assign(part.qualification,{from:b.periodsFx.from,periodReference:b.periodsFx.reference});
b.special.acceleration.monthly.startPeriod={version:1,reference:'QA início de actividade residente2026,CBF16 e períodoL6 reconciliados'};return{d,b};
}
test('accepted11 result and currentkey remain exact without startPeriod;10 histories still readable',()=>{const{d,b}=accepted();assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),b.result.model);const old=read('irpc-l7-preserved.json').acceleration;assert.equal(normalCurrent(old,normalBook(old)),true);});
test('resident July start reuses6 actualmonths,50 normalquota,75 accelerated capped60;only10 additional',()=>{const{d,b}=setup(),asset=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot,m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(m.version,'IRPC-normal-adjustments/12');assert.equal(m.periodsFx.short,true);assert.equal(a.months,6);assert.equal(val(a.normalQuota),50);assert.equal(val(a.acceleratedQuota),75);assert.equal(val(a.acceleratedDeduction),60);assert.equal(val(s.acceleration.additionalDeduction),10);assert.equal(val(m.taxableProfit),3130);assert.equal(val(m.taxableBase),3130);assert.equal(val(m.colecta),1001.6);assert.equal(val(s.taxableProfit),3120);assert.equal(val(s.taxableBase),3120);assert.equal(val(s.colecta),998.4);assert.equal(s.finalBalance.payable,null);assert.equal(s.acceleration.accelerated.assets.length,b.depreciation.assets.length);assert.equal(JSON.stringify(b.depreciation),asset);assert.equal(d.business_plan_snapshot,plan);assert.match(s.acceleration.source,/início de actividade/);assert.doesNotMatch(s.acceleration.source,/primeiro exercício anual/);});
test('October and December use3 and1 months without annualisation and retain exact fiscalfraction',()=>{for(const[month,months,diff]of[[10,3,fraction(5n)],[12,1,fraction(5n,3n)]]){const{d,b}=setup(month),m=calculateNormal(d,b),a=m.special.acceleration.monthly.assets[0];assert.equal(m.periodsFx.months,months);assert.equal(a.months,months);assert.deepEqual(a.additionalDeduction,diff);assert.deepEqual(a.normalQuota,fraction(BigInt(months)*25n,3n));d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));assert.match(normalHtml(d),new RegExp('duodécimosL6:'+months));if(month===12)assert.match(normalHtml(d),/5\/3/);}});
test('missing start evidence blocks only L7;absent and negative remain distinct withnormal totals',()=>{const missing=setup();missing.b.special.acceleration.monthly.startPeriod.reference='';let m=calculateNormal(missing.d,missing.b);assert.equal(m.special.acceleration.status,'indeterminate');assert.equal(m.special.acceleration.additionalDeduction,null);assert.equal(val(m.colecta),1001.6);assert.match(m.special.acceleration.reason,/Falta suporte do período curto/);for(const state of['negative','absent']){const{d,b}=setup();if(state==='negative')b.special.acceleration.rows[0].eligible='no';else{b.special.acceleration.rows=[];Object.assign(b.special.acceleration.qualification,{state:'absent',eligible:''});}m=calculateNormal(d,b);assert.equal(val(m.special.acceleration.additionalDeduction),0);assert.equal(m.special.acceleration.status,state==='absent'?'absent':'calculated');assert.equal(val(m.special.colecta),1001.6);if(state==='negative')assert.equal(m.special.acceleration.rows[0].status,'not_applicable');}});
test('cessation,otherperiodkind,nonresident,previousyearentry,priorquotas and exits remain outside newrecorte',()=>{for(const mutate of[b=>b.periodsFx.kind='same_year',b=>{b.periodsFx.kind='cessation';b.periodsFx.from='2026-01-01';b.periodsFx.to='2026-06-30';},b=>b.scope='ep_general',b=>b.depreciation.assets[0].start='2026-06-01',b=>b.depreciation.assets[0].previousAccounting='1',b=>{b.depreciation.assets[0].exitState='known';b.depreciation.assets[0].exitDate='2026-12-01';}]){const{d,b}=setup();mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.equal(m.special.acceleration.status,'indeterminate');assert.notEqual(m.taxableProfit,null);}});
test('short fiscalperiod remains unsupported in11 unless explicitly prepared;cancel returns same11 decision',()=>{const{d,b}=setup(),period=JSON.stringify(b.periodsFx),asset=JSON.stringify(b.depreciation);delete b.special.acceleration.monthly.startPeriod;const m=calculateNormal(d,b);assert.equal(m.version,'IRPC-normal-adjustments/11');assert.equal(m.special.acceleration.additionalDeduction,null);assert.match(m.special.acceleration.reason,/exercício anual civil2026/);assert.equal(JSON.stringify(b.periodsFx),period);assert.equal(JSON.stringify(b.depreciation),asset);});
test('save-recover12 keeps11result verbatim,originalassets,otherbooks and fractional report;recalc is idempotent',()=>{const{d,b}=setup(),old=structuredClone(b.result),history=structuredClone(b.history),asset=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot,others=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history,[...history,old]);assert.deepEqual(back.history.at(-1),old);assert.equal(back.history.at(-1).model.version,'IRPC-normal-adjustments/11');assert.equal(JSON.stringify(back.depreciation),asset);assert.equal(d.business_plan_snapshot,plan);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,others.irpcWithholdings);const html=normalHtml(d);assert.match(html,/2026-07-01–2026-12-31/);assert.match(html,/dedução adicional conciliada:10.00/);assert.match(html,/colecta sobre a base:998.40/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/início de actividade/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(val(back.result.model.special.acceleration.additionalDeduction),10);assert.equal(val(back.result.model.special.colecta),998.4);back.periodsFx.from='2026-10-01';assert.equal(normalCurrent(d,back),false);});
test('invalid startPeriod child cannot replace originalbooks',()=>{for(const child of[null,{version:2,reference:'QA'},{version:1,reference:3}]){const{b}=setup();b.special.acceleration.monthly.startPeriod=child;assert.throws(()=>validateSpecial(b.special),/não recuperável/);}});
test('prepare only new guardedAPI cases and reports',()=>{const cases={start:setup(),fraction:setup(12),missing:setup(),negative:setup(),absent:setup()};cases.missing.b.special.acceleration.monthly.startPeriod.reference='';cases.negative.b.special.acceleration.rows[0].eligible='no';cases.absent.b.special.acceleration.rows=[];Object.assign(cases.absent.b.special.acceleration.qualification,{state:'absent',eligible:''});fs.mkdirSync('outputs/irpc-l7-start-period',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T17:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-start-period/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-start-period/'+name+'.html',normalHtml(d));}});
+32
View File
@@ -0,0 +1,32 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {newSpecialInput,newQualification,newSpecialExpense,newAcceleratedAsset} from '../../resources/js/lib/irpc-special.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l6-values-preserved.json',import.meta.url),'utf8'));
const value=v=>v?Number(v.numerator)/Number(v.denominator):null;
const qualification=(state='absent',from='2026-07-01',to='2026-12-31')=>({...newQualification(),state,eligible:state==='known'?'yes':'',from,to,reference:'QA declaração/prova das condições legais no sujeito e parcela,sem inferência territorial',periodReference:'QA norma/exercício2026 e período documentado'});
function setup(){const d=seed(),b=normalBook(d);b.special=newSpecialInput();for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])part.qualification=qualification();return{d,b};}
function expenses(){const{d,b}=setup();b.schedules.rows.push({id:'expense36g',rule:'undocumented',amount:'100',sourceCode:'operating_expenses',document:'QA-despesa',line:'1',reference:'QA despesa100 efectivamente incluída e não documentada36g',fiscalYear:'',conditions:'yes'});b.profit.topics.expenseCorrections={state:'known',amount:'100',reference:'QA acréscimo36g já conferido,sem outra correcção'};b.special.expenses.qualification=qualification('known');b.special.expenses.rows=[{...newSpecialExpense('autonomous1'),scheduleId:'expense36g',nature:'undocumented',eligible:'yes',reference:'QA qualificação legal61(4),não inferida de anexo em falta'}];return{d,b};}
function cooperative(){const{d,b}=setup();b.special.cooperative={qualification:qualification('known'),kind:'agrarian',allIncomeEligible:'yes',withheldIncome:'absent'};return{d,b};}
function acceleration(){const{d,b}=setup();delete b.periodsFx;delete b.forms;b.schedules.interest.amount='240';Object.assign(b.depreciation.assets[0],{category:'machine',description:'Máquina industrial existente QA',cost:'1000',rate:'10',accounting:'120',start:'2026-01-01',timing:'annual'});for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])part.qualification=qualification('absent','2026-01-01','2026-12-31');b.special.acceleration={qualification:qualification('known','2026-01-01','2026-12-31'),cbfRequirements:'yes',recognition:'documented',recognitionReference:'QA instrumento do projecto/reconhecimento10,pressupostos3/9 e afectação industrial documentados',noOtherBenefits:'yes',rows:[{...newAcceleratedAsset('accelerated1'),assetId:b.depreciation.assets[0].id,kind:'industrial_equipment',eligible:'yes',reference:'QA factura/afectação da máquina ao projecto industrialCBF16'}]};return{d,b};}
test('no optionalL7 keeps exact accepted9 result and key; installed history is not recalculated on read',()=>{const d=seed(),b=normalBook(d),old=structuredClone(b.result);assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),old.model);assert.deepEqual(normalBook(d).result,old);});
test('61(4) reuses100 addback36g and computes35 autonomously without another profit adjustment',()=>{const{d,b}=expenses(),m=calculateNormal(d,b),s=m.special;assert.equal(m.version,'IRPC-normal-adjustments/10');assert.equal(value(m.taxableProfit),3230);assert.equal(value(m.colecta),1033.6);assert.equal(value(s.taxableProfit),3230);assert.equal(value(s.expenses.rows[0].l6Adjustment),100);assert.equal(value(s.expenses.rows[0].additionalProfitAdjustment),0);assert.equal(value(s.expenses.autonomousTax),35);assert.equal(value(s.combinedColecta),1068.6);assert.equal(s.finalBalance.payable,null);});
test('missing, absent and notapplicable differ; attachment presence alone never classifies an expense',()=>{const{d,b}=setup();b.special.expenses.qualification=qualification('');let s=calculateNormal(d,b).special;assert.equal(s.expenses.status,'indeterminate');assert.equal(s.expenses.autonomousTax,null);b.special.expenses.qualification=qualification('absent');s=calculateNormal(d,b).special;assert.equal(s.expenses.status,'absent');assert.equal(value(s.expenses.autonomousTax),0);b.special.expenses.qualification=qualification('not_applicable');assert.equal(calculateNormal(d,b).special.expenses.status,'not_applicable');const c=expenses();c.b.special.expenses.rows[0].nature='';assert.equal(calculateNormal(c.d,c.b).special.expenses.autonomousTax,null);});
test('false condition keeps existing addback and no autonomous tax; contradictory absence cannot erase expense',()=>{const{d,b}=expenses();b.special.expenses.rows[0].eligible='no';let m=calculateNormal(d,b);assert.equal(m.special.expenses.rows[0].status,'not_applicable');assert.equal(value(m.special.expenses.autonomousTax),0);assert.equal(value(m.taxableProfit),3230);b.special.expenses.rows=[];b.special.expenses.qualification=qualification('absent');m=calculateNormal(d,b);assert.equal(m.special.expenses.status,'indeterminate');assert.equal(value(m.taxableProfit),3230);});
test('duplicate, unlinked or uncalculated36g cannot doublecharge or manufacture tax',()=>{for(const mode of['duplicate','unlinked','uncalculated','missingcoverage']){const{d,b}=expenses();if(mode==='duplicate')b.special.expenses.rows.push({...b.special.expenses.rows[0],id:'duplicate'});if(mode==='unlinked')b.special.expenses.rows[0].scheduleId='not-found';if(mode==='uncalculated')b.schedules.rows[0].conditions='';if(mode==='missingcoverage')b.special.expenses.rows=[];assert.equal(calculateNormal(d,b).special.expenses.autonomousTax,null,mode);}});
test('common qualification rejects wrong period/year or missing identity without blocking independent L6 totals',()=>{for(const mutate of[b=>b.special.expenses.qualification.to='2025-12-31',b=>b.special.expenses.qualification.periodReference='',b=>b.base.subjectNuit='']){const{d,b}=expenses();mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.expenses.autonomousTax,null);if(b.base.subjectNuit)assert.equal(value(m.colecta),1033.6);}});
test('unknown benefit does not suppress separately calculable autonomous expense or normal reference',()=>{const{d,b}=expenses();b.special.acceleration.qualification=qualification('');const m=calculateNormal(d,b);assert.equal(value(m.special.expenses.autonomousTax),35);assert.equal(value(m.special.reference.colecta),1033.6);assert.equal(m.special.combinedColecta,null);});
test('eligible cooperative uses16percent without changing reference32percent or autonomous expense rate',()=>{const{d,b}=cooperative(),m=calculateNormal(d,b);assert.equal(value(m.colecta),1001.6);assert.equal(m.special.cooperative.status,'calculated');assert.equal(value(m.special.cooperative.rate),16);assert.equal(value(m.special.colecta),500.8);assert.equal(value(m.special.taxableBase),3130);assert.equal(value(m.special.combinedColecta),500.8);const x=expenses();x.b.special.cooperative=structuredClone(b.special.cooperative);assert.equal(value(calculateNormal(x.d,x.b).special.combinedColecta),551.8);});
test('mixed, withheld, unclassified or false cooperative conditions never grant blanket reduction',()=>{for(const mutate of[c=>c.allIncomeEligible='no',c=>c.withheldIncome='known',c=>c.kind='other']){const{d,b}=cooperative();mutate(b.special.cooperative);const m=calculateNormal(d,b);assert.equal(m.special.cooperative.status,'indeterminate');assert.equal(m.special.colecta,null);assert.equal(value(m.colecta),1001.6);}const{d,b}=cooperative();b.special.cooperative.qualification.eligible='no';assert.equal(value(calculateNormal(d,b).special.colecta),1001.6);});
test('cooperative treatment does not require mislabelling special rate as L6 general; contradictory absence remains pending',()=>{const{d,b}=cooperative();b.assessment.treatment='other';let m=calculateNormal(d,b);assert.equal(m.colecta,null);assert.equal(value(m.special.colecta),500.8);b.special.cooperative.qualification=qualification('absent');m=calculateNormal(d,b);assert.equal(m.special.cooperative.status,'indeterminate');assert.equal(m.special.colecta,null);});
test('common period follows declared shortperiod even without schedules; calendar dates cannot replace it',()=>{const{d,b}=cooperative();delete b.schedules;b.special.expenses.qualification=qualification('absent','2026-01-01','2026-12-31');const m=calculateNormal(d,b);assert.equal(m.special.period.from,'2026-07-01');assert.equal(m.special.expenses.status,'indeterminate');});
test('CBF16 reuses asset and rate10to15 but respects120 accounted cap; additional deduction20 not50',()=>{const{d,b}=acceleration(),input=JSON.stringify(b.depreciation),m=calculateNormal(d,b),s=m.special;assert.equal(s.acceleration.status,'calculated',s.acceleration.reason);assert.equal(value(m.depreciation.assets[0].quota),100);assert.equal(value(s.acceleration.accelerated.assets[0].quota),150);assert.equal(value(s.acceleration.accelerated.assets[0].deduction),120);assert.equal(value(s.acceleration.additionalDeduction),20);assert.equal(value(m.taxableProfit),6260);assert.equal(value(s.taxableProfit),6240);assert.equal(value(s.taxableBase),6240);assert.equal(value(s.colecta),1996.8);assert.equal(JSON.stringify(b.depreciation),input);});
test('new project building reuses land separation and legal table rate before bounded acceleration',()=>{const{d,b}=acceleration();Object.assign(b.depreciation.assets[0],{category:'commercial_building',cost:'5000',land:'0',rate:'2'});b.special.acceleration.rows[0].kind='new_project_building';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.status,'calculated');assert.equal(value(m.special.acceleration.accelerated.assets[0].quota),150);assert.equal(value(m.special.acceleration.additionalDeduction),20);});
test('lower optional normal quota remains valid L6 but is not treated as legally fixed rate in CBF16',()=>{const{d,b}=acceleration();b.depreciation.assets[0].rate='5';const m=calculateNormal(d,b);assert.equal(m.depreciation.assets[0].status,'calculated');assert.equal(m.special.acceleration.additionalDeduction,null);assert.match(m.special.acceleration.reason,/quota inferior/);});
test('location alone, wrong activity, prior quotas, unsupported periods and benefit combinations cannot accelerate',()=>{for(const mutate of[b=>b.special.acceleration.cbfRequirements='',b=>b.special.acceleration.recognitionReference='',b=>b.special.acceleration.rows[0].kind='other',b=>b.depreciation.assets[0].timing='months',b=>b.depreciation.assets[0].previousAccounting='1',b=>b.special.acceleration.noOtherBenefits='no',b=>{b.special.cooperative={qualification:qualification('known','2026-01-01','2026-12-31'),kind:'agrarian',allIncomeEligible:'yes',withheldIncome:'absent'};}]){const{d,b}=acceleration();d.province_id='ZEE-candidate';mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.taxableProfit,null);assert.equal(m.special.acceleration.status,'indeterminate');}});
test('save/recover/report preserves9history and L5/books; repeated calculation never accumulates tax or asset deduction',()=>{const{d,b}=expenses(),old=structuredClone(b.result),plan=d.business_plan_snapshot;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T14:00:00+02:00'));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),old);assert.equal(d.business_plan_snapshot,plan);assert.ok(JSON.parse(d.modelo_a_snapshot).irpcWithholdings);const html=normalHtml(d);assert.match(html,/imposto autónomo:35/);assert.match(html,/novo ajustamento ao lucro:0/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/Despesas — imposto autónomo/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(value(back.result.model.special.expenses.autonomousTax),35);assert.equal(value(back.result.model.taxableProfit),3230);d.province_id='changed';assert.equal(normalCurrent(d,normalBook(d)),false);assert.throws(()=>normalHtml(d),/Calcule/);});
test('new evidence changes key; malformedqualification or duplicate row ID cannot overwrite book',()=>{const{d,b}=expenses();d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));const back=normalBook(d);back.special.expenses.rows[0].reference+=' alterado';assert.equal(normalCurrent(d,back),false);back.special.expenses.rows.push(structuredClone(back.special.expenses.rows[0]));assert.throws(()=>saveNormalBook(d,back),/identificador repetido/);});
test('report presents context labels and retains provenance internally without dumping raw context snapshots',()=>{const{d,b}=setup();d.province_id_label='Província QA';d.context_opportunity_snapshot='{"internal":"QA implementation blob"}';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));assert.match(normalHtml(d),/Província: Província QA/);assert.doesNotMatch(normalHtml(d),/QA implementation blob/);assert.equal(normalBook(d).result.model.special.context.context_opportunity_snapshot,d.context_opportunity_snapshot);});
test('write only new QA cases for transactional API and recovered reports',()=>{const missing=expenses(),negative=expenses();missing.b.special.expenses.rows[0].nature='';negative.b.special.expenses.rows[0].eligible='no';const cases={expense:expenses(),missing,negative,absent:setup(),cooperative:cooperative(),acceleration:acceleration()};fs.mkdirSync('outputs/irpc-l7-common',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T14:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-common/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-common/'+name+'.html',normalHtml(d));}});
+28
View File
@@ -0,0 +1,28 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {irpcBook,newIrpcRow,recordIrpc,saveIrpcBook,reuseIrpcSources,irpcCurrent,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
import {acquisitionBook,newAcquisition,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/ispc-residual-preserved.json',import.meta.url),'utf8'));
const draft=()=>({...structuredClone(seed),fiscal_component:'irpc'});
const row=(id='digital')=>({...newIrpcRow(id),counterparty:'SYNTHETIC QA fornecedor não residente',documentRef:'SYNTHETIC-'+id,base:'100000',kind:'digital_services',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'SYNTHETIC QA: SaaS utilizado em Moçambique, devedor nacional,sem EP,Lei12/2025 arts5(5/7)/62(5)b',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'SYNTHETIC custo'},{kind:'payment',date:'2026-11-03',reference:'SYNTHETIC pagamento do rendimento'}],settlement:'none',settlementRef:'SYNTHETIC sem entrega'});
const book=(r=row())=>({version:1,rows:[r],payments:[]});
test('positive digital goods/services and e-money have exact 10 percent and first applicable cost event',()=>{for(const kind of ['digital_goods','digital_services','emoney_commission']){const r=recordIrpc(draft(),book({...row(),kind})).result.rows[0];assert.equal(r.status,'calculated');assert.deepEqual(r.tax,{numerator:'10000',denominator:'1'});assert.equal(r.eventDate,'2026-10-02');assert.equal(r.nature,'definitive');assert.equal(r.period,'2026-10');}});
test('unknown territory, recipient, payer, EP, event completeness, relief never become zero or nonapplicable',()=>{for(const patch of [{mozambique:''},{recipientIrpc:''},{payerIrpc:''},{permanentEstablishment:''},{eventsComplete:''},{specialRelief:''}]){const r=recordIrpc(draft(),book({...row(),...patch})).result.rows[0];assert.equal(r.status,'indeterminate');assert.equal(r.tax,null);}});
test('resident and income attributable to EP preserve concrete 68d mapping gap; no 10 or20 automatic',()=>{for(const patch of [{residence:'resident'},{permanentEstablishment:'yes',attributable:'yes'}]){const r=recordIrpc(draft(),book({...row(),...patch})).result.rows[0];assert.equal(r.tax,null);assert.match(r.reason,/68/);}});
test('nonresident with EP and explicitly nonattributable income has definitive positive branch',()=>assert.equal(recordIrpc(draft(),book({...row(),permanentEstablishment:'yes',attributable:'no'})).result.rows[0].status,'calculated'));
test('ordinary and exceptional income need specific amended rule; no legacy2002 rates',()=>{for(const kind of ['royalty','rent','capital','equipment','services','other']){const r=recordIrpc(draft(),book({...row(),kind})).result.rows[0];assert.equal(r.tax,null);assert.match(r.reason,/20\/2022/);}});
test('event before2026 is not moved to later payment; impossible dates rejected',()=>{for(const date of ['2025-12-31','2026-02-30']){const r=row();r.events[0].date=date;assert.equal(recordIrpc(draft(),book(r)).result.rows[0].status,'indeterminate');}});
test('foreign amount and missing base remain indeterminate, explicit zero is distinct',()=>{for(const patch of [{currency:'USD'},{base:''},{base:'-1'}])assert.equal(recordIrpc(draft(),book({...row(),...patch})).result.rows[0].tax,null);assert.deepEqual(recordIrpc(draft(),book({...row(),base:'0'})).result.rows[0].tax,{numerator:'0',denominator:'1'});});
test('partial remittance is separate from tax and recipient payment cannot settle tax',()=>{const r={...row(),settlement:'complete',allocations:[{paymentId:'p1',amount:'6000'}]};const b={...book(r),payments:[{id:'p1',kind:'remittance',reference:'SYNTHETIC-AT1',date:'2026-10-20',amount:'6000',currency:'MZN'}]};const out=recordIrpc(draft(),b).result.rows[0];assert.deepEqual(out.settled,{numerator:'6000',denominator:'1'});assert.deepEqual(out.outstanding,{numerator:'4000',denominator:'1'});b.payments[0].kind='beneficiary';assert.equal(recordIrpc(draft(),b).result.rows[0].status,'indeterminate');});
test('received withholding needs matching proof; no yearly credit or buyer debt generated',()=>{const r={...row(),direction:'received',settlement:'complete',allocations:[{paymentId:'p1',amount:'1500'}],base:'20000'};const b={...book(r),payments:[{id:'p1',kind:'withholding_proof',reference:'SYNTHETIC-PROOF1',date:'2026-10-20',amount:'1500',currency:'MZN'}]};const out=recordIrpc(draft(),b).result.rows[0];assert.equal(out.direction,'received');assert.deepEqual(out.outstanding,{numerator:'500',denominator:'1'});assert.equal('annualCredit'in out,false);});
test('unknown settlement stays null and no-payment with allocations is contradictory',()=>{let r=recordIrpc(draft(),book({...row(),settlement:''})).result.rows[0];assert.equal(r.settled,null);assert.equal(r.tax.numerator,'10000');r=recordIrpc(draft(),book({...row(),allocations:[{paymentId:'p',amount:'1'}]})).result.rows[0];assert.equal(r.status,'indeterminate');});
test('same remittance can be split but never consumed twice, including other opportunities',()=>{const p={id:'p',kind:'remittance',reference:'SYNTHETIC-SHARED',date:'2026-10-20',amount:'10000',currency:'MZN'},a={...row('a'),settlement:'complete',allocations:[{paymentId:'p',amount:'5000'}]},b={...row('b'),settlement:'complete',allocations:[{paymentId:'p',amount:'5000'}]};let bk={version:1,rows:[a,b],payments:[p]};assert.ok(recordIrpc(draft(),bk).result.rows.every(r=>r.status==='calculated'));bk.rows[1].allocations[0].amount='6000';assert.ok(recordIrpc(draft(),bk).result.rows.every(r=>r.status==='indeterminate'));const d=draft();d.modelo_a_snapshot=saveIrpcBook(d,{version:1,rows:[a],payments:[p]});d.context_opportunity='another';assert.equal(recordIrpc(d,{version:1,rows:[{...b,allocations:[{paymentId:'p',amount:'6000'}]}],payments:[p]}).result.rows[0].status,'indeterminate');});
test('duplicate document, duplicate proof ID or duplicate reference do not double totals',()=>{const r=row(),d=draft();assert.ok(recordIrpc(d,{version:1,rows:[r,{...r,id:'x'}],payments:[]}).result.rows.every(r=>r.status==='indeterminate'));const p={id:'p',kind:'remittance',reference:'SYNTHETIC-SAME',date:'2026-10-20',amount:'1',currency:'MZN'};assert.throws(()=>saveIrpcBook(d,{...book(),payments:[p,{...p,id:'p2'}]}),/já registado/);});
test('reuse is idempotent, inherits supplier/document/amount, preserves deliberate edits and detects source delta',()=>{const d=draft(),ab=acquisitionBook(d),a={...newAcquisition('L5-supplier','2026-10','MZN'),supplier:'SYNTHETIC saved supplier',documentRef:'SYNTHETIC invoice',documentLine:'1',net:'100000',description:'SaaS'};d.modelo_a_snapshot=saveAcquisitionBook(d,{...ab,rows:[...ab.rows,a]});let b=reuseIrpcSources(d,irpcBook(d));assert.equal(reuseIrpcSources(d,b).rows.length,b.rows.length);let r=b.rows.find(r=>r.source?.id==='acquisition:L5-supplier');assert.equal(r.counterparty,a.supplier);assert.equal(r.documentRef,'SYNTHETIC invoice / linha 1');r.base='90000';assert.equal(reuseIrpcSources(d,b).rows.find(v=>v.id===r.id).base,'90000');b={version:1,rows:[{...row(),id:r.id,source:r.source}],payments:[]};const current=acquisitionBook(d);d.modelo_a_snapshot=saveAcquisitionBook(d,{...current,rows:current.rows.map(v=>v.id===a.id?{...v,net:'120000'}:v)});assert.match(recordIrpc(d,b).result.rows[0].reason,/origem alterado/);});
test('save/recover preserves all preexisting maps, history, context separation and frozen report',()=>{const d=draft(),prior=JSON.parse(d.modelo_a_snapshot),b=recordIrpc(d,book());d.modelo_a_snapshot=saveIrpcBook(d,b);const now=JSON.parse(d.modelo_a_snapshot);for(const[k,v]of Object.entries(prior))assert.deepEqual(now[k],v);assert.deepEqual(irpcBook(d).result,b.result);assert.equal(irpcCurrent(d,b),true);b.rows[0].base='200000';assert.equal(irpcCurrent(d,b),false);assert.match(irpcReportRows({...d,modelo_a_snapshot:saveIrpcBook(d,b)})[0][2],/10000.00/);assert.equal(recordIrpc(d,b).history.length,1);d.context_opportunity='other';assert.equal(irpcBook(d).rows.length,0);});
test('malformed store remains recoverable as explicit error, never overwritten blank',()=>{assert.throws(()=>irpcBook({modelo_a_snapshot:'{"version":1,"books":{},"irpcWithholdings":[]}'}));const d=draft(),b=book();b.rows[0].events=[{kind:'madeup',date:'2026-10-01',reference:'x'}];assert.throws(()=>saveIrpcBook(d,b));});
test('export escapes private inputs, retains period/nature/gaps and excludes submission claims',()=>{const d=draft(),r={...row(),counterparty:'<script>alert(1)</script>'};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const html=irpcPreparationHtml(d);assert.ok(!html.includes('<script>'));assert.match(html,/&lt;script&gt;/);assert.match(html,/sem submissão/);assert.match(html,/CER/);assert.match(html,/2026-10/);assert.match(html,/Retenção definitiva/);});
if(process.env.IRPC_FIXTURE_OUTPUT){const d=draft(),b=recordIrpc(d,{version:1,rows:[{...row(),settlement:'complete',allocations:[{paymentId:'p1',amount:'6000'}]},{...row('commission'),base:'20000',kind:'emoney_commission',direction:'received',settlement:'complete',allocations:[{paymentId:'p2',amount:'1500'}]}],payments:[{id:'p1',kind:'remittance',reference:'SYNTHETIC-AT1',date:'2026-10-20',amount:'6000',currency:'MZN'},{id:'p2',kind:'withholding_proof',reference:'SYNTHETIC-PROOF1',date:'2026-10-20',amount:'1500',currency:'MZN'}]});d.modelo_a_snapshot=saveIrpcBook(d,b);fs.writeFileSync(process.env.IRPC_FIXTURE_OUTPUT,JSON.stringify(d,null,2)+'\n');}
test("allocation changes in another opportunity mark previous result stale only for shared payments",()=>{const d=draft(),p={id:'p',kind:'remittance',reference:'SYNTHETIC-SHARED',date:'2026-10-20',amount:'10000',currency:'MZN'},r={...row(),settlement:'complete',allocations:[{paymentId:'p',amount:'6000'}]},b=recordIrpc(d,{...book(r),payments:[p]});d.modelo_a_snapshot=saveIrpcBook(d,b);assert.equal(irpcCurrent(d,b),true);const ctx=d.context_opportunity;d.context_opportunity='other';d.modelo_a_snapshot=saveIrpcBook(d,book(row('unrelated')));d.context_opportunity=ctx;assert.equal(irpcCurrent(d,b),true);d.context_opportunity='other';d.modelo_a_snapshot=saveIrpcBook(d,{...book({...row('shared'),settlement:'complete',allocations:[{paymentId:'p',amount:'1000'}]}),payments:[p]});d.context_opportunity=ctx;assert.equal(irpcCurrent(d,b),false);assert.match(irpcReportRows(d)[0][3],/Histórico/);});
+33
View File
@@ -0,0 +1,33 @@
import {ispcEligibilityBook,saveIspcEligibilityBook}from '../../resources/js/lib/ispc-eligibility.ts';
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {ispcResidualBook,saveIspcResidualBook,recordIspcResidual,ispcResidualCurrent,ispcResidualReportRows,ispcFormalPreparationHtml}from '../../resources/js/lib/ispc-residuals.ts';
import {ispcWithholdingBook,saveIspcWithholdingBook}from '../../resources/js/lib/ispc-withholding.ts';
import {createProjectDossierReport}from '../../resources/js/lib/project-dossier-report.ts';
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/ispc-quarter-preserved.json',import.meta.url),'utf8'));
function own(p={}){const d=structuredClone(seed),b=ispcResidualBook(d,'occasional');b.input={...b.input,personType:'singular',currency:'MZN',origin:'projected',operationDate:'2026-06-15',quarter:'2',noHabitual:'yes',territory:'yes',sporadic:'yes',activity:'agriculture',reference:'SYNTHETIC QA colheita não reiterada',amount:'100000',allQuarterOperations:'yes',priorYearTurnover:'0',priorYearRef:'SYNTHETIC QA2025zeroexpresso',organizedRequired:'no',option:'preparation',optionRef:'',optionDate:'',settlement:'self',annual2026:'100000',annualOrigin:'forecast',annualRef:'SYNTHETIC QA2026',priorInYear:'0',...p};return{d,b};}
function acq(p={},wpatch={}){const d=structuredClone(seed),w=ispcWithholdingBook(d);w.input={...w.input,applicable:'yes',quarter:'2',base:'200000',priorAnnualPurchases:'2400000',naturalOccasional:'yes',unregistered:'yes',related:'yes',activity:'agriculture',reference:'SYNTHETIC QA aquisição/ordem e acumulado',...wpatch};d.modelo_a_snapshot=saveIspcWithholdingBook(d,w);const b=ispcResidualBook(d,'acquisitions');b.input={...b.input,...p};return{d,b};}
const calc=({d,b})=>recordIspcResidual(d,b,'2026-10-25T15:40:00Z');const number=x=>x?Number(x.numerator)/Number(x.denominator):null;
test('positive ownoccasional agricultural isolated8h route;self3/4/5 andnoautomaticIRPS20',()=>{for(const[annual,n]of [['100000',3],['2000000',4],['3000000',5]]){const r=calc(own({annual2026:annual})).result;assert.equal(r.status,'calculated',r.missing.join(';'));assert.equal(number(r.amounts.tax),1000*n);assert.match(r.source,/8\(3h,4\)/);assert.equal(r.details.IRPS.includes('Não se infere'),true);}});
test('unknown notnonapplicable;known exclusions andactivityfamilyownscope',()=>{assert.equal(calc(own({personType:''})).result.status,'indeterminate');for(const p of [{personType:'collective'},{noHabitual:'no'},{sporadic:'no'},{territory:'no'},{activity:'other'}])assert.equal(calc(own(p)).result.status,'not_applicable');for(const p of [{territory:''},{currency:'USD'},{quarter:'3'},{reference:''},{priorYearTurnover:''},{organizedRequired:''}])assert.equal(calc(own(p)).result.status,'indeterminate');});
test('ownminimum500withoutlosingdeclaration;zeroexplicit and4mexcessnotreused',()=>{let r=calc(own({amount:'1000'})).result;assert.equal(number(r.amounts.tax),30);assert.equal(number(r.amounts.payment),0);assert.match(r.details.declaration,/Mantida/);r=calc(own({amount:'0'})).result;assert.equal(number(r.amounts.tax),0);assert.equal(calc(own({amount:''})).result.status,'indeterminate');r=calc(own({amount:'200000',priorInYear:'3900000',annual2026:'4100000'})).result;assert.equal(number(r.amounts.ordinary),100000);assert.equal(number(r.amounts.excess),100000);assert.equal(number(r.amounts.tax),25000);});
test('documentedliberatory5meansno secondselftax;smallwithholdingnotminimum500;divergenceunknown',()=>{const p={settlement:'withheld',origin:'effective',buyerIspc:'yes',unregistered:'yes',buyerAnnualPurchases:'200000',withheldActual:'5000',withheldRef:'SYNTHETIC QA recibo5'};let r=calc(own(p)).result;assert.equal(r.status,'settled_by_withholding',r.missing.join(';'));assert.equal(number(r.amounts.withheld),5000);assert.equal(r.amounts.payment,undefined);assert.match(r.reason,/não somar nova liquidação/);r=calc(own({...p,amount:'100',withheldActual:'5'})).result;assert.equal(r.status,'settled_by_withholding');assert.equal(number(r.amounts.tax),5);assert.equal(calc(own({...p,withheldActual:'4000'})).result.status,'indeterminate');assert.equal(calc(own({...p,buyerAnnualPurchases:'2500000.000001'})).result.status,'indeterminate');});
test('selfeffective requiresapplicableoption;forecastcannot masqueradeasactual;nonoptionnoISPC',()=>{assert.equal(calc(own({origin:'effective'})).result.status,'indeterminate');assert.equal(calc(own({option:'not_opted'})).result.status,'not_applicable');const r=calc(own({origin:'effective',operationDate:'2026-12-31',quarter:'4',option:'opted',optionRef:'QA opção',optionDate:'2026-01-01',annualOrigin:'closed_year'})).result;assert.equal(r.status,'calculated',r.missing.join(';'));assert.equal(calc(own({origin:'effective',option:'opted',optionRef:'QA',optionDate:'2026-07-01'})).result.status,'indeterminate');});
test('above2.5m splits100kcovered/100kexcess;5kknownbuttotalnullwithunknownroute',()=>{const r=calc(acq()).result;assert.equal(r.status,'partial');assert.equal(number(r.amounts.covered),100000);assert.equal(number(r.amounts.coveredTax),5000);assert.equal(number(r.amounts.excess),100000);assert.equal(r.amounts.excessTax,null);assert.equal(r.amounts.total,null);});
test('positiveIRPS57k20onidentifiedgrossact—separatefromISPCexcess20andownbuyerpayment',()=>{const p={route:'irps_isolated',noIspcOnExcess:'yes',isolatedAct:'yes',incomeInMoz:'yes',grossExcess:'100000',grossRef:'SYNTHETIC QA base ilíquida',reference:'SYNTHETIC QA8h/57k sem opção5'};const r=calc(acq(p)).result;assert.equal(r.status,'calculated',r.missing.join(';'));assert.equal(number(r.amounts.excessTax),20000);assert.equal(number(r.amounts.total),25000);assert.match(r.reason,/não são crédito/);for(const patch of [{grossExcess:''},{grossExcess:'80000'},{noIspcOnExcess:''},{isolatedAct:''},{incomeInMoz:'no'},{reference:''}]){const r=calc(acq({...p,...patch})).result;assert.equal(r.status,'partial');assert.equal(r.amounts.total,null);}});
test('supplierISPCnotautomaticallyIRPS;crossingexactboundaryandprioroverlimit;noimplicit5onexcess',()=>{let r=calc(acq({route:'ispc_supplier',supplierOptionRef:'QA opção aplicável'})).result;assert.equal(r.status,'partial');assert.equal(r.amounts.excessTax,null);assert.match(r.reason,/art5impede/);r=calc(acq({}, {base:'100000'})).result;assert.equal(r.status,'not_applicable');assert.equal(number(r.amounts.excess),0);r=calc(acq({}, {priorAnnualPurchases:'2500001'})).result;assert.equal(number(r.amounts.covered),0);assert.equal(number(r.amounts.coveredTax),0);assert.equal(number(r.amounts.excess),200000);});
test('newbooks preservehabitualandL3exactly;dependenciesstale withoutrewritingfrozenresults',()=>{let{d,b}=acq();const old=JSON.parse(d.modelo_a_snapshot);b=calc({d,b});d.modelo_a_snapshot=saveIspcResidualBook(d,b);for(const[k,v]of Object.entries(old))assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v);assert.equal(ispcResidualCurrent(d,b),true);const w=ispcWithholdingBook(d);w.input.base='300000';d.modelo_a_snapshot=saveIspcWithholdingBook(d,w);assert.equal(ispcResidualCurrent(d,b),false);assert.equal(number(b.result.amounts.excess),100000);const next=recordIspcResidual(d,b);assert.equal(next.history.length,1);assert.equal(number(next.result.amounts.excess),200000);assert.equal(number(next.history[0].amounts.excess),100000);});
test('formalBGeneral mappingfromexistingcurrentquarterwithoutrecalculation;monthnotinferred;old30notused',()=>{const d=structuredClone(seed),b=ispcResidualBook(d,'formalities'),before=d.modelo_a_snapshot;const result=recordIspcResidual(d,b);assert.equal(result.result.status,'partial');assert.equal(number(result.result.amounts.tax),24000);assert.equal(number(result.result.amounts.payment),24000);assert.match(result.result.details.formPeriod,/Indeterminado/);assert.equal(result.result.details.paymentMonth,'Julho2026');assert.equal(result.input.formMonth,'');d.modelo_a_snapshot=saveIspcResidualBook(d,result);const html=ispcFormalPreparationHtml(d);assert.match(html,/24000.00/);assert.match(html,/Quadro8/);assert.match(html,/Quadro10/);assert.match(html,/não é formulário oficial/);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).ispcQuarters,JSON.parse(before).ispcQuarters);});
test('formalinputsguardedandexternalactsdeclaredonly;signed/officialareasnotautofilled;HTMLescaped',()=>{let d=structuredClone(seed),b=ispcResidualBook(d,'formalities');b.input={...b.input,name:'QA <script>bad()</script>',nuit:'123456789',fiscalArea:'QAUGC',address:'QAendereço',formMonth:'06',formYear:'2026',formPeriodRef:'SYNTHETIC QA correspondência período',interestSituation:'none',interestRef:'SYNTHETIC QA sem juros'};b=recordIspcResidual(d,b);assert.equal(b.result.status,'calculated',b.result.missing.join(';'));d.modelo_a_snapshot=saveIspcResidualBook(d,b);const html=ispcFormalPreparationHtml(d);assert.ok(html.includes('&lt;script&gt;'));assert.ok(!html.includes('<script>bad'));assert.match(html,/não preencher.*assinatura/i);b.input.externalStatus='paid';assert.equal(recordIspcResidual(d,b).result.status,'partial');});
test('documentedwithholdingownroutepreventsduplicatedformalselfpayment',()=>{let{d,b}=own({settlement:'withheld',buyerIspc:'yes',unregistered:'yes',buyerAnnualPurchases:'200000',withheldActual:'5000',withheldRef:'QA recibo5'});b=recordIspcResidual(d,b);d.modelo_a_snapshot=saveIspcResidualBook(d,b);const f=ispcResidualBook(d,'formalities');f.input.sourceKind='occasional';const r=recordIspcResidual(d,f).result;assert.equal(r.status,'not_applicable');assert.match(r.reason,/duplicada/);});
test('invalidstoredmaps/enumspreserveerrors;noemptyfallback',()=>{const d=structuredClone(seed),s=JSON.parse(d.modelo_a_snapshot);s.ispcResiduals=[];assert.throws(()=>ispcResidualBook({...d,modelo_a_snapshot:JSON.stringify(s)},'occasional'));const b=ispcResidualBook(d,'occasional');b.input.activity='services12';assert.throws(()=>saveIspcResidualBook(d,b));});
test('exports separate seller376/buyer375 and report;habitualbooksunchanged',()=>{let{d,b}=own();d.context_opportunity='opportunity:376';d.person_type='singular';d.project_title='SYNTHETIC QA — ISPC residuais';const e=ispcEligibilityBook(d);e.input.mode='occasional';e.input.personType='singular';d.modelo_a_snapshot=saveIspcEligibilityBook(d,e);b=recordIspcResidual(d,b,'2026-10-25T15:40:00Z');d.modelo_a_snapshot=saveIspcResidualBook(d,b);d.context_opportunity='opportunity:375';d.person_type='collective';const a=acq({route:'irps_isolated',noIspcOnExcess:'yes',isolatedAct:'yes',incomeInMoz:'yes',grossExcess:'100000',grossRef:'QA base ilíquida',reference:'QA acto57k'});const aw=ispcWithholdingBook(a.d);d.modelo_a_snapshot=saveIspcWithholdingBook(d,aw);d.modelo_a_snapshot=saveIspcResidualBook(d,recordIspcResidual(d,a.b,'2026-10-25T15:41:00Z'));const f=ispcResidualBook(d,'formalities');d.modelo_a_snapshot=saveIspcResidualBook(d,recordIspcResidual(d,f,'2026-10-25T15:42:00Z'));const report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA residuais',generatedAt:'2026-10-25T15:43:00Z'});assert.match(JSON.stringify(report),/25000.00/);assert.match(JSON.stringify(report),/24000.00/);assert.match(JSON.stringify(ispcResidualReportRows(d)),/IRPS57/);d.context_opportunity='opportunity:376';assert.equal(ispcResidualBook(d,'occasional').result.status,'calculated');assert.equal(number(ispcResidualBook(d,'occasional').result.amounts.tax),3000);d.context_opportunity='opportunity:375';if(process.env.ISPC_RESIDUAL_OUTPUT)fs.writeFileSync(process.env.ISPC_RESIDUAL_OUTPUT,JSON.stringify(d,null,2));});
test('new uniqueformsource preserves deliberatechoice;documentedoptionwithannualforecast staysscenario',()=>{let{d,b}=own({origin:'effective',option:'opted',optionRef:'QA opção',optionDate:'2026-01-01'});b=recordIspcResidual(d,b);assert.equal(b.result.status,'calculated');assert.equal(b.result.details.projected,'yes');assert.match(b.result.reason,/Cenário declarado/);const e=ispcEligibilityBook(d);e.input.mode='occasional';d.modelo_a_snapshot=saveIspcEligibilityBook(d,e);d.modelo_a_snapshot=saveIspcResidualBook(d,b);let f=ispcResidualBook(d,'formalities');assert.equal(f.input.sourceKind,'occasional');f.input.sourceKind='quarter';d.modelo_a_snapshot=saveIspcResidualBook(d,f);assert.equal(ispcResidualBook(d,'formalities').input.sourceKind,'quarter');});
test('acquisitionformalpreparation distinguishesISPC5andIRPS20andkeepsspecificexternalmodelpending',()=>{let{d,b}=acq({route:'irps_isolated',noIspcOnExcess:'yes',isolatedAct:'yes',incomeInMoz:'yes',grossExcess:'100000',grossRef:'QA base ilíquida',reference:'QA57k'});b=recordIspcResidual(d,b);d.modelo_a_snapshot=saveIspcResidualBook(d,b);let f=ispcResidualBook(d,'formalities');f.input.sourceKind='acquisitions';f=recordIspcResidual(d,f);assert.equal(number(f.result.amounts.tax),25000);assert.ok(f.result.missing.some(s=>s.includes('Modelo/código')));assert.equal(f.result.status,'partial');d.modelo_a_snapshot=saveIspcResidualBook(d,f);const html=ispcFormalPreparationHtml(d);assert.match(html,/ISPC—retenção12/);assert.match(html,/IRPS—acto isolado57/);assert.match(html,/5000.00/);assert.match(html,/20000.00/);assert.match(html,/Por corresponder/);});
test('knownownaccess exclusions are concrete notrequestsforrevalidation;unknownstillindeterminate',()=>{for(const patch of [{organizedRequired:'yes'},{priorYearTurnover:'4000000.000001'}]){const r=calc(own(patch)).result;assert.equal(r.status,'not_applicable');assert.equal(r.amounts.payment,undefined);assert.match(r.reason,/fora/i);}assert.equal(calc(own({organizedRequired:''})).result.status,'indeterminate');});
test('below500paymentdispensa iscalculatedbutformalrepresentationnotinventedaszerocalculatedtax',()=>{let{d,b}=own({amount:'1000'});b=recordIspcResidual(d,b);d.modelo_a_snapshot=saveIspcResidualBook(d,b);let f=ispcResidualBook(d,'formalities');f.input={...f.input,sourceKind:'occasional',name:'QA vendedor',nuit:'123456789',fiscalArea:'QAUGC',address:'QA endereço',formMonth:'06',formYear:'2026',formPeriodRef:'QA correspondência',interestSituation:'none',interestRef:'QA sem juros'};f=recordIspcResidual(d,f);assert.equal(number(f.result.amounts.tax),30);assert.equal(number(f.result.amounts.payment),0);assert.equal(f.result.status,'partial');assert.ok(f.result.missing.some(s=>s.includes('dispensa')));d.modelo_a_snapshot=saveIspcResidualBook(d,f);const html=ispcFormalPreparationHtml(d);assert.match(html,/Imposto calculado:30.00;prestação legal:0.00/);});
test('switchfromdocumentedliberatoryroute cannotgenerate secondduewithoutconcretereconciliation',()=>{let{d,b}=own({settlement:'withheld',buyerIspc:'yes',unregistered:'yes',buyerAnnualPurchases:'200000',withheldActual:'5000',withheldRef:'QA retenção definitiva'});b=recordIspcResidual(d,b);assert.equal(b.result.status,'settled_by_withholding');b.input.settlement='self';let next=recordIspcResidual(d,b);assert.equal(next.result.status,'partial');assert.equal(next.result.amounts.payment,null);assert.equal(next.result.amounts.tax,null);assert.equal(number(next.result.amounts.withheld),5000);assert.equal(next.history[0].status,'settled_by_withholding');const fresh=structuredClone(b);fresh.input.reference='QA outra operação';assert.equal(recordIspcResidual(d,fresh).result.status,'calculated');b.input.withholdingCorrected='yes';b.input.withholdingCorrectionRef='QA documento externo de rectificação';next=recordIspcResidual(d,b);assert.equal(next.result.status,'calculated');assert.equal(number(next.result.amounts.payment),3000);assert.equal(next.result.details.priorWithholdingRef,'QA retenção definitiva');assert.equal(next.history[0].status,'settled_by_withholding');});
+2 -2
View File
@@ -34,7 +34,7 @@ test('missing inputs still block only the affected result; invalid fractions do
const {draft}=l1DeltaFixture();book.rows[0].sourceAmount='1';const stale=calculateSales(draft,book).rows[0];assert.equal(stale.vat,null);assert.equal(stale.exact,undefined);
assert.throws(()=>fractionPresentation({numerator:'1',denominator:'0'}));assert.equal(fractionPresentation(fraction(1n,21n)).approximate,true);assert.equal(fractionPresentation(decimalFraction('105')).approximate,false);
});
test('form06 keeps the agreed residual message and does not infer a foreign operation',()=>{
const {book}=l1DeltaFixture();const r=evaluateSalesOperation({...book.rows[0],treatment:'foreign_operation'},'MZN');assert.equal(r.field,null);assert.equal(r.vat,null);assert.match(modeloA06Correspondence,/P001 residual/);assert.match(modeloA06Correspondence,/Não há atribuição automática06/);assert.match(modeloA06Correspondence,/provas anteriores preservadas/);
test('confirmed form06 correspondence preserves unknown foreign treatment',()=>{
const {book}=l1DeltaFixture();const r=evaluateSalesOperation({...book.rows[0],treatment:'foreign_operation'},'MZN');assert.equal(r.field,null);assert.equal(r.vat,null);assert.match(modeloA06Correspondence,/Correspond\u00eancia confirmada pela Tempo/);assert.match(modeloA06Correspondence,/Opera\u00e7\u00e3o no estrangeiro n\u00e3o significa06/);assert.match(modeloA06Correspondence,/Originais e resultados hist\u00f3ricos preservados/);
});
if(process.env.IVA_L1_DELTA_OUT)fs.writeFileSync(process.env.IVA_L1_DELTA_OUT,JSON.stringify(l1DeltaFixture().draft,null,2));
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import {portalNavigation,currentNavigationItem} from '../../resources/js/lib/portal-navigation.ts';
import {searchTerritoryHref} from '../../resources/js/lib/search-territory.ts';
// Shallow SSR of the actual navigation component. Inertia and visual primitives
// are replaced by semantic wrappers; this is not a browser/viewport proof.
const require=createRequire(import.meta.url);
const source=fs.readFileSync(new URL('../../resources/js/components/portal-sidebar-navigation.tsx',import.meta.url),'utf8');
const output=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const primitive=(tag)=>({children,asChild,...props})=>asChild?React.cloneElement(children,{}):React.createElement(tag,props,children);
const module={exports:{}};
new Function('require','module','exports',output)((name)=>{
if(name==='@inertiajs/react')return {Link:({href,children,...props})=>React.createElement('a',{href,...props},children)};
if(name==='@/lib/portal-navigation')return {currentNavigationItem};
if(name==='@/lib/search-territory')return {searchTerritoryHref};
if(name==='lucide-react')return {LayoutGrid:()=>null};
if(name==='@/components/ui/sidebar')return {SidebarGroup:primitive('section'),SidebarGroupLabel:primitive('h2'),SidebarMenu:primitive('ul'),SidebarMenuButton:primitive('div'),SidebarMenuItem:primitive('li')};
return require(name);
},module,module.exports);
const Component=module.exports.PortalSidebarNavigation;
const flatten=(rows)=>rows.flatMap(r=>[...(r.href?[r.href]:[]),...flatten(r.children??[])]);
const render=(access,url='/')=>{const groups=portalNavigation(access);return {groups,html:renderToStaticMarkup(React.createElement(Component,{groups,url}))};};
void test('sidebar renders every nested authorised destination instead of a first-child shortcut',()=>{
const {groups,html}=render({authenticated:true,manager:true,businesses:true,education:true,ai:true,contentManagementHref:'/gestao-conteudos',contentManagementAccess:{opportunities:{index:'/management/opportunities',create:'/management/opportunities/create'}},scopedAdministration:[{label:'Referências económicas',href:'/management/economic-references/lot-01'}]});
const hrefs=[...html.matchAll(/<a\b[^>]*href="([^"]+)"/g)].map(m=>m[1].replaceAll('&amp;','&'));
assert.deepEqual([...hrefs].sort(),flatten(groups).sort());
assert.equal(hrefs[0],'/gestao-conteudos');
for(const href of ['/carteira','/zonas','/documentos','/legislacao','/management/opportunities','/management/economic-references/lot-01'])assert.ok(hrefs.includes(href),href);
assert.match(html,/Área de trabalho/);
});
void test('nested current destination is selected without changing its URL or replacing it with a group root',()=>{
const {html}=render({authenticated:true,manager:false},'/zonas?category=zee');
const current=[...html.matchAll(/<a\b([^>]*aria-current="page"[^>]*)>/g)];
assert.equal(current.length,1);
assert.match(current[0][1],/href="\/zonas"/);
});
void test('rendered visitor sidebar explains access without private operation links',()=>{
const {html}=render({authenticated:false,manager:false});
assert.match(html,/href="\/acesso-gestao"/);
assert.doesNotMatch(html,/href="\/management\//);
assert.doesNotMatch(html,/analysis=proactive/);
});
void test('sidebar carries the existing search territory when moving between catalogues',()=>{
const {html}=render({authenticated:false,manager:false},'/oportunidades?province_id=QA-province&district_id=QA-district');
assert.match(html,/href="\/carteira\?province_id=QA-province&amp;district_id=QA-district"/);
assert.match(html,/href="\/zonas\?province_id=QA-province&amp;district_id=QA-district"/);
assert.match(html,/href="\/acesso-gestao"/);
});
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {opportunityFillGroups,opportunityErrorGroup,opportunityMissingGroups} from '../../resources/js/lib/opportunity-fill-flow.ts';
import {territorialSearchParams} from '../../resources/js/lib/territorial-search-params.ts';
import {documentFacetLabel} from '../../resources/js/lib/document-facet-label.ts';
test('adaptive groups keep every applicable schema field once, including new specialized fields',()=>{
const keys=['official_title','locations','sector_id','documents','currency','real_estate','future_schema_field','currency'];
const groups=opportunityFillGroups(keys);
assert.deepEqual(groups.flatMap(group=>group.fields).sort(),[...new Set(keys)].sort());
assert.ok(groups.find(group=>group.id==='specific').fields.includes('future_schema_field'));
assert.deepEqual(opportunityFillGroups(['official_title']).map(group=>group.id),['idea']);
assert.deepEqual(opportunityFillGroups([]),[]);
});
test('saved pending fields and validation errors point to the correct existing group without inventing applicability',()=>{
const groups=opportunityFillGroups(['official_title','locations','total_investment']);
assert.equal(opportunityErrorGroup(groups,{'canonical.total_investment':'Valor em falta'}),'finance');
assert.equal(opportunityErrorGroup(groups,{'canonical.locations.0':'Local em falta'}),'territory');
assert.deepEqual(opportunityMissingGroups(groups,['locations.0','canonical.total_investment']).map(group=>group.id),['territory','finance']);
assert.equal(opportunityErrorGroup(groups,{reason:'Motivo em falta'}),undefined);
});
test('territorial search replaces old descendants while retaining all selected non-territorial filters',()=>{
const entries=[['province_id','old'],['district_id','old-child'],['locality_id','old-leaf'],['category','zee'],['q','Nacala'],['page','5']];
const scoped=territorialSearchParams(entries,[{id:'new',location_type:'province'}]);
assert.equal(scoped.get('province_id'),'new');assert.equal(scoped.has('district_id'),false);assert.equal(scoped.has('locality_id'),false);
assert.equal(scoped.get('category'),'zee');assert.equal(scoped.get('q'),'Nacala');assert.equal(scoped.has('page'),false);
const all=territorialSearchParams(entries,[]);assert.equal(all.has('province_id'),false);assert.equal(all.get('category'),'zee');
const unchanged=territorialSearchParams(entries);assert.equal(unchanged.get('district_id'),'old-child');
});
test('document facets have readable labels while original query identifiers remain unchanged',()=>{
const original=['agronegocio','education','media','pt','turismo'];
assert.deepEqual(original.map(documentFacetLabel),['Agronegócio','Formação e competências','Imagens e conteúdos de apoio','Português','Turismo']);
assert.deepEqual(original,['agronegocio','education','media','pt','turismo']);
assert.equal(documentFacetLabel('Tema definido pela Tempo'),'Tema definido pela Tempo');
});
+48 -3
View File
@@ -9,6 +9,7 @@ import {
legacyPortalNavigation,
currentNavigationGroup,
currentNavigationItem,
canShowLegacyManagement,
} from '../../resources/js/lib/portal-navigation.ts';
void test('public and authenticated navigation share groups without revealing management', () => {
@@ -20,7 +21,7 @@ void test('public and authenticated navigation share groups without revealing ma
assert.deepEqual(publicGroups.slice(0, 3), investor.slice(0, 3));
assert.deepEqual(
publicGroups.map((g) => g.id),
['discovery', 'realization', 'support', 'account'],
['discovery', 'realization', 'support', 'work', 'account'],
);
const hrefs = JSON.stringify(investor);
assert.equal(hrefs.includes('/management/'), false);
@@ -37,7 +38,7 @@ void test('management and optional modules follow permissions and feature flags'
education: true,
ai: true,
});
assert.ok(groups.some((group) => group.id === 'management'));
assert.ok(groups.some((group) => group.id === 'work'));
assert.equal(JSON.stringify(groups).includes('/investidor/negocios'), true);
assert.equal(JSON.stringify(groups).includes('/management/ai'), true);
assert.equal(
@@ -100,7 +101,7 @@ void test('reconciled navigation selects discovery, realization and private supp
);
assert.equal(
currentNavigationGroup(groups, '/simulador-fiscal'),
'realization',
'discovery',
);
assert.equal(
currentNavigationGroup(groups, '/balcao-virtual/dossiers/fixture'),
@@ -151,3 +152,47 @@ void test('catalogue detail and return preserve the search inside the local cata
'/zonas?https%3A%2F%2Foutside.invalid=',
);
});
const destinations=(items)=>items.flatMap(item=>[...(item.href?[item.href]:[]),...destinations(item.children??[])]);
void test('both menu consumers respect the existing scoped server decision rather than generic manager status',()=>{
assert.equal(canShowLegacyManagement({user:{id:1},canManagePortal:true,legacyManagementAccess:false}),false);
assert.equal(canShowLegacyManagement({user:{id:1},canManagePortal:true,legacyManagementAccess:true}),true);
assert.equal(canShowLegacyManagement({user:null,canManagePortal:true,legacyManagementAccess:true}),false);
assert.equal(canShowLegacyManagement({user:{id:1},canManagePortal:true}),true);
});
void test('work entry is discoverable without exposing private capabilities to an anonymous visitor',()=>{
const groups=portalNavigation({authenticated:false,manager:true,contentManagementHref:'/gestao-conteudos',contentManagementAccess:{opportunities:{index:'/management/opportunities',create:'/management/opportunities/create'}},scopedAdministration:[{label:'Private QA administration',href:'/management/secret'}]});
assert.deepEqual(destinations(groups.find(g=>g.id==='work').children),['/acesso-gestao']);
assert.equal(JSON.stringify(groups).includes('/management/'),false);
assert.equal(JSON.stringify(groups).includes('/gestao-conteudos'),false);
});
void test('work area exposes existing content capability even when the old dashboard flag has no menu object',()=>{
const groups=portalNavigation({authenticated:true,manager:true,opportunityManagement:null,contentManagementHref:'/gestao-conteudos',contentManagementAccess:{opportunities:{index:'/management/opportunities',create:'/management/opportunities/create'}}});
const work=groups.find(g=>g.id==='work');
assert.equal(work.label,'Área de trabalho');
assert.ok(work.children.some(i=>i.label==='Preparar análise e ideia'&&i.href==='/management/opportunities/create?analysis=proactive'));
assert.ok(work.children.some(i=>i.href==='/gestao-conteudos'));
assert.ok(work.children.some(i=>i.href==='/management/opportunities'));
});
void test('every previously authorised management/profile destination survives the regrouping exactly once',()=>{
const access={authenticated:true,manager:true,businesses:true,education:true,ai:true,contentManagementHref:'/gestao-conteudos',profileAccess:{contexts:[],mine:'/minhas-oportunidades',create:'/minhas-oportunidades/create',request:'/perfil/promotor',review:null,audit:'/perfis/auditoria',administration:'/perfis/gestao'},scopedAdministration:[{label:'Referências económicas',href:'/management/economic-references/lot-01'}]};
const old=destinations(legacyPortalNavigation(access).filter(g=>['management','profile-opportunities'].includes(g.id)));
const groups=portalNavigation(access);const after=destinations(groups);
for(const href of old)assert.equal(after.filter(x=>x===href).length,1,href);
assert.equal(after.length,new Set(after).size);
assert.ok(destinations(groups.find(g=>g.id==='work').children).includes('/management/economic-references/lot-01'));
assert.equal(after.filter(href=>href==='/management/profile-install').length,1,'profile installation already present in the old authorised sidebar must survive');
});
void test('ordinary authenticated account has an explanation entry and does not acquire creation or management links',()=>{
const groups=portalNavigation({authenticated:true,manager:false});
assert.deepEqual(destinations(groups.find(g=>g.id==='work').children),['/acesso-gestao']);
assert.equal(JSON.stringify(groups).includes('analysis=proactive'),false);
assert.equal(JSON.stringify(groups).includes('/management/'),false);
});
void test('direct analysis and ordinary creation preserve distinct selection state in the same work group',()=>{
const access={authenticated:true,manager:false,profileAccess:{contexts:[],mine:'/minhas-oportunidades',create:'/minhas-oportunidades/create',request:'/perfil/promotor',review:null,administration:null}};
const groups=portalNavigation(access);
assert.equal(currentNavigationGroup(groups,'/minhas-oportunidades/create?analysis=proactive'),'work');
assert.equal(currentNavigationItem(groups,'/minhas-oportunidades/create?analysis=proactive'),'prepare-analysis');
assert.equal(currentNavigationItem(groups,'/minhas-oportunidades/create'),'new-my-opportunity');
});
@@ -0,0 +1,13 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {buildProjectDossierRows} from '../../resources/js/lib/project-dossier.ts';
import {proactiveInvestmentRows} from '../../resources/js/lib/proactive-investment-report.ts';
const source=(id,need)=>({key:'opportunity:'+id,title:'SYNTHETIC QA '+id,version:3,analysis:{proactive:{version:1,group:'ideas',state:'hypothesis',need,proposal:'Solução '+id,basis:'Fixture pública sintética',limitations:'Não comprova procura real.',next_action:'Confirmar dados reais.',evidence:[{statement:'QA facto '+id,source:'QA exemplar',locator:'Fixture linha1',period:'2026',scope:'Piloto',coverage:'Sintética',verified_on:'2026-10-27'}]}}});
test('absent proactivity is a no-op for accepted dossiers',()=>{assert.deepEqual(proactiveInvestmentRows({}),[]);assert.deepEqual(proactiveInvestmentRows({context_applicability_snapshot:'{"version":1,"opportunity_choices":[]}'}),[]);assert.deepEqual(proactiveInvestmentRows({context_opportunity_snapshot:'unreadable-original-preserved'}),[]);});
test('each idea retains origin,source,need,limits without summing or inventing economic values',()=>{const choices=[{opportunity:source(1,'A')},{opportunity:source(2,'B')}],d={context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:choices})},r=proactiveInvestmentRows(d);assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,2);assert.match(JSON.stringify(r),/opportunity:1/);assert.match(JSON.stringify(r),/opportunity:2/);assert.match(JSON.stringify(r),/Hipótese analítica/);assert.match(JSON.stringify(r),/não é zero/);assert.equal(d.context_applicability_snapshot,JSON.stringify({version:1,opportunity_choices:choices}));});
test('consumer selection only reports its idea even when other source contexts are present',()=>{const choices=[{opportunity:source(1,'A')},{opportunity:source(2,'B')}],r=proactiveInvestmentRows({context_opportunity:'opportunity:2',context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:choices})});assert.doesNotMatch(JSON.stringify(r),/opportunity:1/);assert.match(JSON.stringify(r),/opportunity:2/);});
test('explicit consumer snapshot is retained and repeat references do not duplicate report rows',()=>{const a=source(1,'A');let r=proactiveInvestmentRows({context_opportunity_snapshot:JSON.stringify({choice:{opportunity:a}})});assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,1);r=proactiveInvestmentRows({context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:[{opportunity:a},{opportunity:a}]})});assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,1);});
test('portable API consumer snapshot produces an independently scoped report',()=>{const d=JSON.parse(fs.readFileSync(new URL('./fixtures/proactive-api-consumers.json',import.meta.url),'utf8'));const before=JSON.stringify(d);const perConsumer=d.consumer_contexts.map(c=>proactiveInvestmentRows(c.prefill.simulator));for(const rows of perConsumer)assert.equal(rows.filter(x=>x[1]==='Origem da análise e ideia').length,1);const r=perConsumer.flat();assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,2);assert.match(JSON.stringify(perConsumer[0]),/opportunity:399/);assert.doesNotMatch(JSON.stringify(perConsumer[0]),/opportunity:400/);assert.match(JSON.stringify(perConsumer[1]),/opportunity:400/);assert.doesNotMatch(JSON.stringify(perConsumer[1]),/opportunity:399/);assert.doesNotMatch(JSON.stringify(r),/PRIVATE-QA-/);assert.equal(JSON.stringify(d),before);});
test('resources,assumptions and selected relationships are retained in the existing dossier report',()=>{const a=source(7,'A');Object.assign(a.analysis.proactive,{resources:'QA recurso declarado',assumptions:'QA pressuposto explícito',conditions:'QA condição',relations:[{kind:'project',target_id:'QA-reference',label:'QA projecto existente',relationship:'hypothesis',basis:'QA fundamento público'}]});const draft={context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:[{opportunity:a}]})},rows=buildProjectDossierRows(draft);assert.match(JSON.stringify(rows),/QA pressuposto explícito/);assert.match(JSON.stringify(rows),/QA projecto existente/);assert.match(JSON.stringify(rows),/sem contrato, acesso ou participação presumidos/);});
test('existing facts hide unrelated need and idea rows without deleting previous preparation',()=>{const a=source(8,'Old need');a.analysis.proactive.group='exists';a.analysis.proactive.proposal='Old idea';const d={context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:[{opportunity:a}]})},before=d.context_applicability_snapshot,r=proactiveInvestmentRows(d);assert.equal(r.some(x=>x[1]==='Estado da necessidade'||x[1]==='Proposta de investimento'),false);assert.equal(d.context_applicability_snapshot,before);assert.match(JSON.stringify(r),/QA facto 8/);});
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import React from 'react';
import ts from 'typescript';
// Exercise the real component's event/state contract; this is not browser visual QA.
const require=createRequire(import.meta.url);
const source=fs.readFileSync(new URL('../../resources/js/components/territory-search-select.tsx',import.meta.url),'utf8');
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
function harness(overrides={}){
const memory=[];let cursor=0;const chosen=[];
const hooks={...React,useState(initial){const i=cursor++;if(!(i in memory))memory[i]=initial;return[memory[i],value=>{memory[i]=typeof value==='function'?value(memory[i]):value;}];},useRef:()=>({current:null}),useEffect:()=>{}};
const module={exports:{}};
new Function('require','module','exports',compiled)(name=>name==='react'?hooks:require(name),module,module.exports);
const options=[{id:'province-a',name:'Nampula'},{id:'province-b',name:'Maputo Província'}];
const props={id:'territory',label:'Província',search:'',options,onSearch(query){props.search=query;},onChange(option){chosen.push(option);props.selected=option;},...overrides};
let tree;
const flat=node=>!node||typeof node!=='object'?[]:[node,...React.Children.toArray(node.props?.children).flatMap(flat)];
const render=()=>{cursor=0;tree=module.exports.default(props);return tree;};
const find=predicate=>flat(tree).find(predicate);
const input=()=>find(node=>node.type==='input');
const key=value=>input().props.onKeyDown({key:value,preventDefault(){}});
render();return{props,chosen,render,find,input,key,options};
}
test('one search input owns the dropdown; the separate select is absent',()=>{
const h=harness();assert.equal(h.find(node=>node.type==='select'),undefined);
assert.equal(h.input().props.role,'combobox');assert.equal(h.input().props['aria-expanded'],false);
h.input().props.onFocus();h.render();
assert.equal(h.input().props['aria-expanded'],true);
assert.ok(h.find(node=>node.props?.role==='listbox'));
h.find(node=>node.props?.role==='option'&&node.props.id==='territory-options-1').props.onClick();h.render();
assert.deepEqual(h.chosen,[h.options[1]]);assert.equal(h.input().props.value,'Maputo Província');
assert.equal(h.input().props['aria-expanded'],false);
});
test('typing and Escape preserve the existing selection until an option is explicitly chosen',()=>{
const selected={id:'saved',name:'Selecção guardada fora desta página'};const h=harness({selected});
h.input().props.onFocus();h.render();h.input().props.onChange({target:{value:'Map'}});h.render();
assert.equal(h.props.search,'Map');assert.deepEqual(h.chosen,[]);
h.key('Escape');h.render();assert.equal(h.input().props.value,selected.name);assert.deepEqual(h.chosen,[]);
});
test('arrow keys and Enter choose from the list; loading cannot commit an old result',()=>{
const h=harness();h.input().props.onFocus();h.render();h.key('ArrowDown');h.render();h.key('ArrowDown');h.render();
assert.equal(h.input().props['aria-activedescendant'],'territory-options-1');
h.key('Enter');h.render();assert.deepEqual(h.chosen,[h.options[1]]);
const busy=harness({loading:true});busy.input().props.onFocus();busy.render();busy.key('Enter');
assert.deepEqual(busy.chosen,[]);
});
test('clear is explicit; disabled dependent levels cannot expose the dropdown',()=>{
const h=harness({selected:{id:'saved',name:'Nampula'}});
h.find(node=>node.props?.['aria-label']==='Limpar Província').props.onClick();h.render();
assert.deepEqual(h.chosen,[undefined]);assert.equal(h.input().props.value,'');
const disabled=harness({disabled:true});assert.equal(disabled.input().props.disabled,true);
assert.equal(disabled.find(node=>node.props?.role==='listbox'),undefined);
});