feat(iva): implement G3 temporary benefit cessation and explicit treatment continuity
This commit is contained in:
@@ -2,8 +2,9 @@ import { useState } from 'react';
|
||||
import { fundraisingActivities } from '@/lib/iva-fundraising';
|
||||
import { pharmaAnnexEntries, pharmaAnnexEntry, pharmaAnnexReference } from '@/lib/iva-pharma-annex';
|
||||
import { annexIEntries, annexIEntry, annexIReference } from '@/lib/iva-annex-i';
|
||||
import { temporaryBenefit, temporaryBenefitNotice } from '@/lib/iva-temporary-benefits';
|
||||
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
|
||||
import { continueAnnexIOperation, modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
|
||||
import { continueTemporaryOperation, continueAnnexIOperation, modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
|
||||
|
||||
export function SimulationOperations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
|
||||
const [error,setError]=useState('');
|
||||
@@ -14,6 +15,7 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
|
||||
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
|
||||
const save=(next:SalesBook)=>onChange('modelo_a_snapshot',saveSalesBook(draft,next));
|
||||
const change=(id:string,patch:Partial<SalesOperation>)=>run(()=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)}));
|
||||
const continueTemporary=(id:string,target:string)=>run(()=>save({...book,rows:book.rows.map(row=>row.id===id?continueTemporaryOperation(row,target):row)}));
|
||||
return <section aria-label="Operações IVA por oportunidade" className="space-y-3 rounded border p-3">
|
||||
<h4 className="font-semibold">Operações de IVA — enquadramento das vendas</h4>
|
||||
<p>Oportunidade {String(draft.context_opportunity||'entrada geral')}; moeda original {book.currency||'por indicar'}. Cobertura2026/MZN. Reutilize as vendas e complete apenas o enquadramento. Este bloco classifica as operações; não substitui nem soma novamente a execução mensal geral acima.</p>
|
||||
@@ -30,8 +32,17 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
|
||||
<label>Período<input type="month" className="block rounded border p-2" min="2026-01" max="2026-12" value={row.period} onChange={e=>change(row.id,{period:e.target.value})}/></label>
|
||||
<label>Valor declarado<input inputMode="decimal" className="block rounded border p-2" value={row.amount} onChange={e=>change(row.id,{amount:e.target.value})}/></label>
|
||||
<label>Representação do preço<select className="block rounded border p-2" value={row.basis} onChange={e=>change(row.id,{basis:e.target.value as SalesOperation['basis']})}><option value="">Ainda desconhecida</option><option value="net">Sem IVA</option><option value="included">IVA incluído</option></select></label>
|
||||
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
|
||||
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>temporaryBenefit(row.treatment)&&!temporaryBenefit(e.target.value)?continueTemporary(row.id,e.target.value):change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
|
||||
</div>
|
||||
{temporaryBenefit(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Benefício temporário — prazo e continuidade</legend>
|
||||
<p>{temporaryBenefitNotice(row.treatment,row.period)}</p>
|
||||
<p>{temporaryBenefit(row.treatment)!.historicalScope}</p>
|
||||
<p>Não precisa de preencher documentos de uma isenção já terminada para prosseguir em2026. Antes de escolher outro tratamento, confirme a natureza da operação: a escolha conserva valor,moeda,período,estabelecimento eorigem; limpa as confirmações para revisão e mantém o resultado anterior no histórico.</p>
|
||||
<button type="button" onClick={()=>continueTemporary(row.id,'general')}>Escolher operação interna geral e rever condições</button>
|
||||
{row.treatment==='temporary_sugar_cane'&&<button type="button" onClick={()=>continueTemporary(row.id,'production_agriculture')}>Analisar produção agrícola própria — artigo9(7)(a)</button>}
|
||||
<p>Para outro enquadramento disponível,use a escolha «Tratamento da operação». Nenhum resultado é recalculado automaticamente.</p>
|
||||
</fieldset>}
|
||||
{temporaryBenefit(row.temporaryBenefitOrigin)&&!temporaryBenefit(row.treatment)&&<p>Origem conservada: {temporaryBenefit(row.temporaryBenefitOrigin)!.label},benefício com termo31/12/2025. Enquadramento actual escolhido expressamente;confirme as respectivas condições antes de calcular.</p>}
|
||||
{row.treatment==='annexI_goods'&&<fieldset className="space-y-2 rounded border p-2"><legend>Bem do Anexo I — edição Lei22/2022</legend>
|
||||
<p>Escolha código e designação e confira apenas os qualificadores dessa linha. A pesquisa conserva a escolha actual; mudar de posição exige rever as condições específicas. O anexo não dispensa o enquadramento de outras isenções aplicáveis ao mesmo bem.</p>
|
||||
<label className="block">Pesquisar código ou designação no Anexo I<input value={annexSearch[`I:${row.id}`]??''} onChange={e=>setAnnexSearch({...annexSearch,[`I:${row.id}`]:e.target.value})}/></label>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fundraisingError, type FundraisingContext } from './iva-fundraising.ts'
|
||||
import { pharmaAnnexEntry, pharmaAnnexReference, pharmaAnnexSource, pharmaAnnexVersion } from './iva-pharma-annex.ts';
|
||||
import { annexIEntry, annexIReference, annexISource, annexIVersion } from './iva-annex-i.ts';
|
||||
import { productionOptionCheck, productionOptionVersion, type ProductionTaxOption } from './iva-production-option.ts';
|
||||
import { temporaryBenefits, temporaryBenefit, temporaryBenefitNotice, temporaryBenefitVersion } from './iva-temporary-benefits.ts';
|
||||
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
|
||||
|
||||
@@ -57,6 +58,7 @@ const listedGoodsTreatments:OperationTreatment[] = [
|
||||
['insecticides','Insecticidas','Bem transmitido identificado como insecticida; não qualquer produto químico'],
|
||||
].map(([id,label,condition])=>({id:`listed_${id}`,label:`Lista legal — ${label}`,field:'05',rate:0,article:'CIVA arts.9(10),19(1)(b)(v), redacção Lei22/2022',source:'Lei22/2022 art.1, PDF2, p.2252(26),9(10); CIVA republicado Lei13/2016, PDF15, p.1426(273),19(1)(b)(v). Âmbito técnico2026/MZN',conditions:[internalOperation,'Transmissão do próprio bem enumerado, identificado; não um serviço autónomo nem extensão por sector, marca ou destino',condition]}));
|
||||
export const operationTreatments:OperationTreatment[] = [
|
||||
...temporaryBenefits.map(item=>({id:item.id,label:`Benefício terminado31/12/2025 — ${item.label}`,field:null,rate:null,article:`CIVA art.${item.article}`,source:`${item.source} CIVA republicado Lei13/2016,9(14),PDF10/p.1426(268),apenas para9(13)(b/f). Âmbito:cessação nas operações internas2026/MZN;sem cálculo histórico.`,conditions:[],logicVersion:temporaryBenefitVersion})),
|
||||
{id:'annexI_goods',label:'Equipamento, sementes e outros bens do Anexo I — artigo9(12)(h)',field:'07',rate:0,logicVersion:annexIVersion,article:'CIVA arts.9(12)(h),19(1); Lei22/2022 art.3(1)',source:`CIVA republicado Lei13/2016, PDF10, p.1426(268),9(12)(h), e PDF15, p.1426(273),19(1); rectificação BR89/2017, PDF1, p.569; ${annexISource}. Âmbito técnico2026/MZN`,conditions:[internalOperation,'O bem transmitido corresponde ao código e à designação da posição escolhida no Anexo I; não basta a actividade agrícola, a marca ou uma indicação genérica de equipamento','Qualificadores expressos na posição conferidos e fundamentados, incluindo natureza, destino, certificação, peso, capacidade ou potência quando exigidos nessa linha; não acrescentar condições a linhas que não as exigem']},
|
||||
{id:'pharma_manufacturing_inputs',label:'Bens do Anexo III para fabrico de medicamentos — estabelecimento integrado no SNS',field:'07',rate:0,logicVersion:pharmaAnnexVersion,article:'CIVA arts.9(1)(g),19(1); Lei22/2022 art.3(1)',source:`CIVA republicado Lei13/2016, PDF9, p.1426(267),9(1)(g), e PDF15, p.1426(273),19(1); rectificação BR89/2017, PDF1, p.569; ${pharmaAnnexSource}. Âmbito técnico2026/MZN`,conditions:[internalOperation,'Transmissão efectuada pelo próprio estabelecimento público ou privado identificado, integrado no Serviço Nacional de Saúde; não basta que o adquirente ou destino seja um hospital','Bens a utilizar como matérias-primas, produtos intermédios ou componentes para o fabrico de medicamentos, com utilização/fundamento identificados; não mera venda de produto acabado ou utilização genérica','Bem corresponde cumulativamente ao código e à designação da linha seleccionada no Anexo III; confirmar as especificações aplicáveis sem corrigir ou completar a pauta por suposição']},
|
||||
...([false,true] as const).map(nonprofit=>({...socialTreatment(`fundraising_${nonprofit?'nonprofit':'public'}`,`Angariação ocasional de fundos — ${nonprofit?'organismo sem finalidade lucrativa':'entidade pública'}`,'h',['Actividade habitual isenta identificada:9(1)(a),9(2)(a/b/c/d/e/g) ou9(3)(a/b), com condições dessa isenção e fundamento identificados','Manifestação ocasional destinada à angariação de fundos em proveito exclusivo da entidade organizadora','Total anual declarado inclui esta manifestação e as realizadas fora deste dossier; não superior a oito no ano'],nonprofit),logicVersion:'fundraising/1',source:'CIVA republicado Lei13/2016, PDF10, p.1426(268),9(2)(h); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF1, p.2252(25),9(1–3), e PDF2, p.2252(26),10(1–2). Âmbito técnico2026/MZN'})),
|
||||
@@ -114,7 +116,7 @@ export const operationTreatments:OperationTreatment[] = [
|
||||
{id:'foreign_operation',label:'Operação no estrangeiro — analisar territorialidade',field:null,rate:null,article:'CIVA art.6: localização por natureza da operação; art.19(1)(b)(ii) não determina sozinho a caixa06 do formulário',source:source2016,conditions:[]},
|
||||
{id:'other',label:'Outra operação / enquadramento ainda desconhecido',field:null,rate:null,article:'Fundamento específico ainda necessário; não presumir taxa ou isenção',source:'Sem fonte operacional determinada para esta linha',conditions:[]},
|
||||
];
|
||||
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption;fundraiser?:FundraisingContext;pharmaAnnexItem?:string;annexIItem?:string;annexIOnly?:boolean; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
|
||||
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption;fundraiser?:FundraisingContext;pharmaAnnexItem?:string;annexIItem?:string;annexIOnly?:boolean;temporaryBenefitOrigin?:string; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
|
||||
export type SalesBook = {currency:string;rows:SalesOperation[];periods:Array<{period:string;complete:boolean}>; result?:SalesResult; history?:SalesResult[]};
|
||||
export type SalesLineResult = {id:string;field:string|null;net:string|null;vat:string|null;gross:string|null;rate:number|null;status:'calculated'|'exempt'|'indeterminate';reason:string;article:string;source:string;exact?:{net:FiscalFraction;vat:FiscalFraction;gross:FiscalFraction}};
|
||||
export type SalesResult = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;currency:string;key:string;inputs:SalesOperation[];rows:SalesLineResult[];periods:Array<{period:string;fields:Record<string,string|null>;exactFields?:Record<string,FiscalFraction|null>;reasons:string[]}>;rounding:string};
|
||||
@@ -125,6 +127,11 @@ export function continueAnnexIOperation(row:SalesOperation,targetId:string):Sale
|
||||
if(row.treatment!=='annexI_goods'||!entry?.relatedTreatments?.includes(targetId)||!target)throw new Error('O tratamento de continuidade não corresponde à posição escolhida; dados conservados.');
|
||||
return {...row,treatment:targetId,confirmed:[],annexIOnly:false,source:`${row.source}; continuidade expressamente escolhida a partir de ${annexIReference(entry)} para ${target.label}; rever condições do tratamento, sem alterar montante/moeda/contexto`};
|
||||
}
|
||||
export function continueTemporaryOperation(row:SalesOperation,targetId:string):SalesOperation {
|
||||
const benefit=temporaryBenefit(row.treatment),target=operationTreatments.find(item=>item.id===targetId);
|
||||
if(!benefit||!target||temporaryBenefit(targetId))throw new Error('Escolha um enquadramento actual distinto do benefício terminado; dados conservados.');
|
||||
return {...row,treatment:targetId,confirmed:[],temporaryBenefitOrigin:benefit.id,source:`${row.source}; continuidade expressamente escolhida de ${benefit.id}/${benefit.branch}, CIVA art.${benefit.article}, termo31/12/2025, ${temporaryBenefitVersion}, para ${target.label}; ${benefit.source}; montante/moeda/período conservados; rever condições, sem cálculo automático`};
|
||||
}
|
||||
export function salesBook(draft:Record<string,unknown>):SalesBook {
|
||||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||||
if(store.salesOperations!==undefined&&(!store.salesOperations||typeof store.salesOperations!=='object'||Array.isArray(store.salesOperations)))throw new Error('Formato das operações IVA não recuperável; dados preservados, sem substituição.');
|
||||
@@ -164,6 +171,7 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
|
||||
const stop=(reason:string)=>({...result,reason});
|
||||
if(currency!=='MZN'||row.sourceCurrency!==currency)return stop('Moeda fora de MZN ou divergente da origem; sem conversão silenciosa.');
|
||||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('Período fora da cobertura de2026; sem cálculo histórico presumido.');
|
||||
if(temporaryBenefit(row.treatment))return stop(temporaryBenefitNotice(row.treatment,row.period));
|
||||
if(!rule||rule.rate===null||!rule.field)return stop('Operação não calculada: delimite incidência, territorialidade e fundamento desta linha. A remissão do campo06 não constitui uma regra de classificação de todas as operações no estrangeiro.');
|
||||
if(n===null||!['net','included'].includes(row.basis))return stop('Montante ou indicação de IVA incluído/sem IVA em falta; vazio não significa zero.');
|
||||
if(!row.description.trim()||!row.establishment.trim())return stop('Indique a operação e o estabelecimento a que pertence; não inferidos da localização do investimento.');
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export const temporaryBenefitVersion = 'temporary-benefit-cessation/1';
|
||||
export const temporaryBenefitEnd = '2025-12-31';
|
||||
|
||||
const amendments = 'Lei10/2025, BR248/suplemento1, art.1, PDF12/p.2180(12): altera3,6,12,18,19,21,23,25,26,27,31,32,33,34, sem prorrogar9(13)/9(16); art.5, PDF15/p.2180(15), efeitos01/01/2026. Art.12(1)(a) retira as remissões a9(13)/9(16) nas importações; este bloco trata operações internas, não calcula importações.';
|
||||
const sugarOilSource = `Lei3/2025, BR95/suplemento, art.1, PDF1–2/p.478(1)–(2):9(13)(a–f), termo31/12/2025; art.2, PDF2, entrada na publicação21/05/2025. ${amendments}`;
|
||||
|
||||
export const temporaryBenefits = [
|
||||
{ id: 'temporary_sugar', branch: 'P038', label: 'Transmissão do açúcar', article: '9(13)(a)', historicalScope: 'Transmissão do açúcar.', source: sugarOilSource },
|
||||
{ id: 'temporary_sugar_industry_goods', branch: 'P039', label: 'Bens transmitidos pela indústria nacional do açúcar', article: '9(13)(b);9(14)', historicalScope: 'Matérias-primas, produtos intermediários, peças, equipamentos e componentes: a redacção de2025 diz efectuadas pela indústria nacional do açúcar. Art.9(14): documentos aduaneiros apropriados ou declaração do adquirente, consoante o caso, sobre incorporação no processo de produção.', source: sugarOilSource },
|
||||
{ id: 'temporary_oils_soap', branch: 'P040', label: 'Óleos alimentares e sabões', article: '9(13)(c)', historicalScope: 'Transmissões de óleos alimentares e de sabões.', source: sugarOilSource },
|
||||
{ id: 'temporary_oils_soap_factory', branch: 'P041', label: 'Produção industrial de óleo alimentar e sabões', article: '9(13)(d)', historicalScope: 'Bens resultantes da actividade industrial de produção de óleo alimentar e de sabões, transmitidos pelas respectivas fábricas.', source: sugarOilSource },
|
||||
{ id: 'temporary_oils_soap_inputs', branch: 'P042', label: 'Matérias-primas para óleo/sabões — Anexo II', article: '9(13)(e)', historicalScope: 'Bens para utilização como matéria-prima na indústria de óleo e sabões, constantes da Pauta e discriminados noAnexoII daLei3/2025,PDF2. A lista não prorroga o prazo nem determina por si outra isenção.', source: sugarOilSource },
|
||||
{ id: 'temporary_sugar_cane', branch: 'P043', label: 'Actividade agrícola de cana-de-açúcar para a indústria', article: '9(13)(f);9(14)', historicalScope: 'Bens e serviços no âmbito da actividade agrícola de produção de cana-de-açúcar destinados à indústria. Art.9(14): documentos aduaneiros apropriados ou declaração do adquirente, consoante o caso, sobre incorporação no processo de produção. A produção agrícola própria do9(7)(a) exige análise distinta.', source: sugarOilSource },
|
||||
{ id: 'temporary_solar_inputs', branch: 'P045', label: 'Factores de produção de painéis solares para electrificação rural', article: '9(16)', historicalScope: 'Factores de produção de painéis solares para electrificação rural, constantes da Pauta e discriminados noAnexoIV. Não é uma isenção genérica de painéis ou de projectos de energia.', source: `Lei22/2022, art.1, PDF2/p.2252(26),9(16), termo31/12/2025;art.3(2),PDF4/p.2252(28),aditaAnexoIV;art.6,PDF4,efeitos01/01/2023. ${amendments}` },
|
||||
] as const;
|
||||
|
||||
export function temporaryBenefit(id: string | undefined) {
|
||||
return temporaryBenefits.find(item => item.id === id);
|
||||
}
|
||||
|
||||
export function temporaryBenefitNotice(id: string, period: string): string {
|
||||
const benefit = temporaryBenefit(id);
|
||||
if (!benefit) throw new Error('Benefício temporário não identificado; dados conservados.');
|
||||
if (!/^2026-(0[1-9]|1[0-2])$/.test(period)) return 'Período fora da cobertura de2026; não se calcula nem se certifica a isenção histórica.';
|
||||
return `O benefício temporário do art.${benefit.article} terminou em31/12/2025 e não é aplicável a${period}. A cessação não determina, por si, a taxa ou outra isenção desta operação. Escolha expressamente o enquadramento actual e confirme as respectivas condições; montantes e imposto ficam por determinar até essa escolha.`;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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 IvaTemporaryBenefitDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_cessation_and_explicit_continuations_recover_with_their_opportunity_and_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/iva-l1-g3-temporary-20261024/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], '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('opportunity:322', $recovered['context_opportunity']);
|
||||
$this->assertSame('Manica', $recovered['selection_labels']['province_id']);
|
||||
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$book = $store['salesOperations']['opportunity:322'];
|
||||
$this->assertSame('opportunity:322', $book['result']['opportunity']);
|
||||
$this->assertSame('MZN', $book['result']['currency']);
|
||||
$this->assertSame('2026-10', $book['result']['periods'][0]['period']);
|
||||
$this->assertSame('700.00', $book['result']['periods'][0]['fields']['01']);
|
||||
$this->assertSame('112.00', $book['result']['periods'][0]['fields']['02']);
|
||||
$this->assertSame('100.00', $book['result']['periods'][0]['fields']['05']);
|
||||
$this->assertCount(8, $book['rows']);
|
||||
$this->assertSame('116', $book['rows'][0]['amount']);
|
||||
$this->assertSame('included', $book['rows'][0]['basis']);
|
||||
$this->assertSame('temporary_sugar', $book['rows'][0]['temporaryBenefitOrigin']);
|
||||
$this->assertSame('temporary_solar_inputs', $book['rows'][6]['temporaryBenefitOrigin']);
|
||||
$this->assertSame('production_agriculture', $book['rows'][7]['treatment']);
|
||||
$this->assertStringContainsString('P045', $book['rows'][6]['source']);
|
||||
$this->assertCount(7, $book['history'][0]['rows']);
|
||||
$this->assertNull($book['history'][0]['periods'][0]['fields']['01']);
|
||||
$this->assertStringContainsString('terminou em31/12/2025', $book['history'][0]['rows'][0]['reason']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-annex-i-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
foreach (json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['salesOperations'] as $context => $original) {
|
||||
$this->assertSame($original, $store['salesOperations'][$context]);
|
||||
}
|
||||
file_put_contents(base_path('outputs/iva-l1-g3-temporary-20261024/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,90 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { temporaryBenefits, temporaryBenefit, temporaryBenefitNotice, temporaryBenefitVersion } from '../../resources/js/lib/iva-temporary-benefits.ts';
|
||||
import { continueTemporaryOperation, evaluateSalesOperation, newSalesOperation, operationTreatments, recordSales, salesBook, salesCurrent, saveSalesBook } from '../../resources/js/lib/iva-operations.ts';
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
|
||||
const frozen = JSON.parse(fs.readFileSync(new URL('./fixtures/iva-annex-i-inputs.json', import.meta.url), 'utf8'));
|
||||
function operation(benefit, index = 0) {
|
||||
return { ...newSalesOperation(`g3-${index}`, '2026-10', 'MZN'), treatment: benefit.id,
|
||||
description: `SYNTHETIC QA — ${benefit.branch} / ${benefit.label}`, establishment: 'SYNTHETIC QA estabelecimento',
|
||||
amount: index === 0 ? '116' : '100', basis: index === 0 ? 'included' : 'net',
|
||||
evidence: 'SYNTHETIC QA — natureza e contraprestação pressupostas para demonstração; não certificação' };
|
||||
}
|
||||
export function fixture() {
|
||||
const draft = { ...structuredClone(frozen), project_title: 'SYNTHETIC QA — G3 / Cessação de benefícios temporários', context_opportunity: 'opportunity:322' };
|
||||
const pending = recordSales(draft, { currency: 'MZN', rows: temporaryBenefits.map(operation), periods: [{ period: '2026-10', complete: true }] });
|
||||
const rows = pending.rows.map(row => {
|
||||
const next = continueTemporaryOperation(row, 'general');
|
||||
next.confirmed = operationTreatments.find(rule => rule.id === next.treatment).conditions.map((_, i) => i);
|
||||
return next;
|
||||
});
|
||||
const agricultural = continueTemporaryOperation({ ...operation(temporaryBenefits[5], 8), id: 'g3-cane-independent' }, 'production_agriculture');
|
||||
agricultural.confirmed = operationTreatments.find(rule => rule.id === agricultural.treatment).conditions.map((_, i) => i);
|
||||
rows.push(agricultural);
|
||||
const book = recordSales(draft, { ...pending, rows });
|
||||
draft.modelo_a_snapshot = saveSalesBook(draft, book);
|
||||
return { draft, book, pending };
|
||||
}
|
||||
|
||||
test('G3 has seven stable branches; expired benefits never become zero-rated or taxable automatically in2026', () => {
|
||||
assert.deepEqual(temporaryBenefits.map(item => item.branch), ['P038', 'P039', 'P040', 'P041', 'P042', 'P043', 'P045']);
|
||||
for (const benefit of temporaryBenefits) {
|
||||
const row = operation(benefit), result = evaluateSalesOperation(row, 'MZN');
|
||||
assert.equal(result.status, 'indeterminate');
|
||||
for (const field of ['field', 'rate', 'net', 'vat', 'gross']) assert.equal(result[field], null, field);
|
||||
assert.match(result.reason, /terminou em31\/12\/2025/);
|
||||
assert.match(result.source, /Lei10\/2025/);
|
||||
for (const period of ['2026-01', '2026-12']) assert.match(temporaryBenefitNotice(benefit.id, period), /não é aplicável/);
|
||||
for (const period of ['2025-12', '2027-01', '']) assert.match(evaluateSalesOperation({ ...row, period }, 'MZN').reason, /fora da cobertura/);
|
||||
assert.match(evaluateSalesOperation({ ...row, amount: '', evidence: '' }, 'MZN').reason, /ficam por determinar/);
|
||||
}
|
||||
assert.match(temporaryBenefits[1].historicalScope, /efectuadas pela indústria/);
|
||||
assert.match(temporaryBenefits[1].historicalScope, /9\(14\)/);
|
||||
assert.match(temporaryBenefits[5].historicalScope, /9\(14\)/);
|
||||
assert.throws(() => temporaryBenefitNotice('unknown', '2026-10'));
|
||||
});
|
||||
|
||||
test('explicit continuation keeps amounts/origin/identity and requires new conditions; cane has an independent agricultural route', () => {
|
||||
for (const [i, benefit] of temporaryBenefits.entries()) {
|
||||
const row = operation(benefit, i), next = continueTemporaryOperation(row, 'general');
|
||||
for (const field of ['id', 'period', 'description', 'establishment', 'amount', 'basis', 'evidence', 'sourceAmount', 'sourcePeriod', 'sourceCurrency']) assert.deepEqual(next[field], row[field], field);
|
||||
assert.equal(next.temporaryBenefitOrigin, benefit.id); assert.deepEqual(next.confirmed, []);
|
||||
assert.ok(next.source.startsWith(row.source)); assert.ok(next.source.includes(benefit.branch));
|
||||
assert.ok(next.source.includes(temporaryBenefitVersion));
|
||||
assert.equal(evaluateSalesOperation(next, 'MZN').vat, null);
|
||||
const confirmed = { ...next, confirmed: [0, 1] };
|
||||
assert.equal(evaluateSalesOperation(confirmed, 'MZN').vat, '16.00');
|
||||
assert.equal(evaluateSalesOperation({ ...confirmed, amount: '' }, 'MZN').vat, null);
|
||||
assert.equal(evaluateSalesOperation({ ...confirmed, evidence: '' }, 'MZN').vat, null);
|
||||
assert.equal(evaluateSalesOperation({ ...confirmed, sourceCurrency: 'USD' }, 'MZN').vat, null);
|
||||
assert.throws(() => continueTemporaryOperation(row, 'missing'));
|
||||
assert.throws(() => continueTemporaryOperation(row, benefit.id));
|
||||
}
|
||||
const { book } = fixture();
|
||||
assert.equal(book.result.rows.at(-1).field, '05'); assert.equal(book.result.rows.at(-1).vat, '0.00');
|
||||
assert.equal(book.result.rows[0].net, '100.00'); assert.equal(book.result.rows[0].vat, '16.00');
|
||||
assert.equal(book.rows[0].amount, '116'); assert.equal(book.rows[0].basis, 'included');
|
||||
});
|
||||
|
||||
test('actual cessation→choice→calculation→stored snapshot→report preserves all13 delivered books and history', () => {
|
||||
const { draft, book, pending } = fixture();
|
||||
for (const value of Object.values(pending.result.periods[0].fields)) assert.equal(value, null);
|
||||
assert.deepEqual(book.history.at(-1), pending.result);
|
||||
assert.equal(book.result.periods[0].fields['01'], '700.00');
|
||||
assert.equal(book.result.periods[0].fields['02'], '112.00');
|
||||
assert.equal(book.result.periods[0].fields['05'], '100.00');
|
||||
assert.equal(salesCurrent(draft, book), true);
|
||||
assert.deepEqual(salesBook(JSON.parse(JSON.stringify(draft))), book);
|
||||
const before = JSON.parse(frozen.modelo_a_snapshot).salesOperations, after = JSON.parse(draft.modelo_a_snapshot).salesOperations;
|
||||
assert.equal(Object.keys(before).length, 13);
|
||||
for (const [context, original] of Object.entries(before)) {
|
||||
assert.deepEqual(after[context], original, context);
|
||||
assert.equal(salesCurrent({ ...draft, context_opportunity: context }, after[context]), true, context);
|
||||
}
|
||||
const model = createProjectDossierReport(draft, { selectedSections: new Set(['finance']), version: 'QA G3 — Cessação2026' });
|
||||
const encoded = JSON.stringify(model);
|
||||
for (const token of ['opportunity:322', 'Manica', '2026-10', '700.00', '112.00', '100.00', '31/12/2025', temporaryBenefitVersion, 'P045', 'Lei3/2025', 'Lei22/2022']) assert.ok(encoded.includes(token), token);
|
||||
if (process.env.IVA_G3_CALCULATED) fs.writeFileSync(process.env.IVA_G3_CALCULATED, JSON.stringify(draft));
|
||||
});
|
||||
Reference in New Issue
Block a user