feat(iva): prepare goods allocation classification and current acquisition-cost bases in QA

This commit is contained in:
2026-10-25 00:15:19 +02:00
parent 1b67fa85ff
commit 9b95e5e7a8
8 changed files with 204 additions and 0 deletions
@@ -0,0 +1,35 @@
import {useState} from 'react';
import {newGoodsAllocation,switchAllocationTreatment,switchAllocationValuation,goodsAllocationBook,goodsAllocationConditions,allocationSpecificConditions,goodsAllocationCurrent,goodsAllocationReportRows,goodsAllocationSource,goodsAllocationTreatments,recordGoodsAllocations,saveGoodsAllocationBook,type GoodsAllocationBook,type GoodsAllocationRow} from '@/lib/iva-goods-allocation';
export function SimulationGoodsAllocation({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let book:GoodsAllocationBook;
try{book=goodsAllocationBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
const save=(next:GoodsAllocationBook)=>{try{onChange('modelo_a_snapshot',saveGoodsAllocationBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<GoodsAllocationRow>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
return<details className="space-y-3 rounded border p-3">
<summary>Afectação de bens — classificação ebase de aquisição/custo</summary>
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. P124identifica a transmissão assimilada;P088determina a base. Uma operação conserva a sua identidade nos dois requisitos,não se soma duas vezes. Uso temporário de bens como serviçoP089segue o seu percurso.</p>
<p>A dedução anterior total/parcial é um facto histórico dos bens ou elementos;não é calculada nem concedida aqui. A base usa preço de aquisição reportado ao momento da operação ou,na sua falta demonstrada,preço de custo.Não substituir um preço desconhecido por zero ou por custo automaticamente.</p>
<p>{goodsAllocationSource}</p>{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newGoodsAllocation(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar afectação ou transmissão gratuita</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Operação por identificar'} — {row.period||'mês por indicar'}</summary>
<div className="grid gap-3 md:grid-cols-2">
<label>Percurso de afectação dos bens<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,switchAllocationTreatment(row,e.target.value))}><option value="">Por indicar</option>{goodsAllocationTreatments.map(t=><option key={t.id} value={t.id}>{t.label}</option>)}</select></label>
<label>Natureza dos dados<select className="block rounded border p-2" value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as GoodsAllocationRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
{([['description','Operação identificada'],['operationRef','Referência da operação / pressuposto'],['currency','Moeda da base'],['operationDate','Data da operação / prevista'],['period','Mês da operação'],['documentRef','Documento / referência da previsão'],['documentDate','Data do documento / prevista'],['goodsRef','Bens/elementos e identificação do conjunto afectado'],['deductionRef','Referência da dedução anterior total ou parcial'],['destinationRef','Destino,finalidade ou sector eâmbito'],['evidence','Fundamento factual / pressupostos']] as const).map(([field,label])=><label key={field}>{label}<input className="block rounded border p-2" type={['operationDate','documentDate'].includes(field)?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{['permanent_private_goods','free_goods_transfer'].includes(row.treatment)&&<label>Factos/referência sobre excepção de amostras/ofertas de pequeno valor<input className="block rounded border p-2" value={row.exceptionRef} onChange={e=>change(row.id,{exceptionRef:e.target.value})}/></label>}
{row.treatment==='fixed_asset_20a'&&<label>Enquadramento dos bens20(1)a eafectação ao activo imobilizado<input className="block rounded border p-2" value={row.asset20aRef} onChange={e=>change(row.id,{asset20aRef:e.target.value})}/></label>}
<label>Modo de determinação da base15(2)b<select className="block rounded border p-2" value={row.valuationMode} onChange={e=>change(row.id,switchAllocationValuation(row,e.target.value))}><option value="">Por esclarecer</option><option value="acquisition">Preço de aquisição reportado ao momento da operação</option><option value="cost">Preço de custo,na falta demonstrada de preço de aquisição</option></select></label>
{row.valuationMode==='acquisition'&&<><label>Preço de aquisição para o conjunto identificado,sem próprioIVA<input className="block rounded border p-2" value={row.acquisitionPrice} onChange={e=>change(row.id,{acquisitionPrice:e.target.value})}/></label><label>Referência do preço de aquisição reportado ao momento<input className="block rounded border p-2" value={row.acquisitionRef} onChange={e=>change(row.id,{acquisitionRef:e.target.value})}/></label></>}
{row.valuationMode==='cost'&&<><label>Preço de custo para o conjunto identificado,sem próprioIVA<input className="block rounded border p-2" value={row.currentCost} onChange={e=>change(row.id,{currentCost:e.target.value})}/></label><label>Referência do custo reportado ao momento<input className="block rounded border p-2" value={row.costRef} onChange={e=>change(row.id,{costRef:e.target.value})}/></label><label>Falta de preço de aquisição demonstrada para esta operação<select className="block rounded border p-2" value={row.conditions.acquisitionUnavailable??''} onChange={e=>change(row.id,{conditions:{...row.conditions,acquisitionUnavailable:e.target.value as ''|'yes'|'no'}})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label></>}
<label>Momento a que o preço de aquisição/custo é reportado<input type="date" className="block rounded border p-2" value={row.valuationDate} onChange={e=>change(row.id,{valuationDate:e.target.value})}/></label>
<label>Âmbito ecomposição da base,bens/elementos abrangidos<input className="block rounded border p-2" value={row.valuationScope} onChange={e=>change(row.id,{valuationScope:e.target.value})}/></label>
<label>Base reportada ao momento desta operação,sem próprioIVA ecom âmbito correspondente<select className="block rounded border p-2" value={row.conditions.valuationConfirmed??''} onChange={e=>change(row.id,{conditions:{...row.conditions,valuationConfirmed:e.target.value as ''|'yes'|'no'}})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
</div>
<p>Mudar percurso/modo conserva referências e valores ocultos,mas exige rever confirmações próprias. Não se presume limiar de oferta,custo histórico actualizado ou nova dedução.</p>
<fieldset><legend>Factos próprios da operação</legend>{[...goodsAllocationConditions,...allocationSpecificConditions(row.treatment)].map(([key,label])=><label className="block" key={key}>{label}<select className="block rounded border p-2" value={row.conditions[key]??''} onChange={e=>change(row.id,{conditions:{...row.conditions,[key]:e.target.value as ''|'yes'|'no'}})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset>
</details>)}
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try{save(recordGoodsAllocations(draft,book));}catch(cause){setError(String(cause));}}}>Classificar afectações,determinar base eguardar versão</button>}
{book.result&&<><p role="status">{goodsAllocationCurrent(draft,book)?'Classificações/bases correspondentes às entradas guardadas.':'Entradas alteradas;resultados históricos conservados. Reveja e determine novamente.'} {book.history?.length??0} versões anteriores. Dedução histórica não é nova dedução/campo11;sem IVA ou regularização calculados.</p><dl>{goodsAllocationReportRows(draft).map(([,label,value,origin],index)=><div key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</details>;
}
@@ -20,6 +20,7 @@ import { SimulationServiceLocation } from './simulation-service-location';
import { SimulationListedServices } from './simulation-listed-services';
import { SimulationReducedBases } from './simulation-reduced-bases';
import { SimulationOwnName } from './simulation-own-name';
import { SimulationGoodsAllocation } from './simulation-goods-allocation';
import { SimulationOfferPreparations } from './simulation-offer-preparations';
import { SimulationOperations } from './simulation-operations';
@@ -118,6 +119,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
<SimulationListedServices draft={draft} onChange={onChange} />
<SimulationReducedBases draft={draft} onChange={onChange} />
<SimulationOwnName draft={draft} onChange={onChange} />
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
<SimulationOfferPreparations draft={draft} onChange={onChange} />
<SimulationModeloA draft={draft} />
</section>;
+65
View File
@@ -0,0 +1,65 @@
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
export const goodsAllocationVersion='goods-allocation-base/1';
export const goodsAllocationSource='CIVA3(3)e/f,Lei13/2016PDF6–7/p1426(264–265):afectação permanente de bens a uso próprio/particular/alheio,transmissão gratuita,afectação a sector isento ou ao activo imobilizado de bens20(1)a,com dedução total/parcial anterior nos bens/elementos;excepção de amostras/ofertas de pequeno valor na alíneae. Art15(2)b,PDF13/p1426(271):preço de aquisição ou,na sua falta,preço de custo,reportados ao momento da operação. Art20(1)a,PDF15/p1426(273). Lei22/2022PDF3mantém15(2)b e20(1)a;Lei10/2025mantém3(3)e/f e não altera15/20. Recorte2026/MZN,sem nova dedução/campo11,regularização,taxa,IVA ou totaisModeloA. Uso temporário de bens como serviçoP089 é distinto.';
export const goodsAllocationTreatments=[{id:'permanent_private_goods',label:'Afectação permanente a uso próprio,particular ou fins alheios',clause:'e'},{id:'free_goods_transfer',label:'Transmissão gratuita de bens',clause:'e'},{id:'exempt_sector_goods',label:'Afectação de bens a sector de actividade isento',clause:'f'},{id:'fixed_asset_20a',label:'Afectação ao activo imobilizado de bens20(1)a',clause:'f'}] as const;
export const goodsAllocationConditions=[['businessGoods','São bens da empresa identificados,não uma prestação de serviços ou venda onerosa normal'],['priorDeduction','Houve dedução total ou parcial anterior nos bens ou elementos que os constituem,com referência histórica'],['factsCorrespond','Objecto,destino,operação,data eâmbito correspondem ao percurso identificado']] as const;
export const allocationOwnConditions={
permanent_private_goods:[['permanentAllocation','A afectação dos bens é permanente,não mero uso temporário'],['privatePurpose','Uso próprio do titular,pessoal ou fins alheios à empresa identificado'],['notSmallGift','Não se trata de amostra ou oferta de pequeno valor abrangida pela excepção3(3)e']],
free_goods_transfer:[['gratuitousTransfer','Está identificada uma transmissão gratuita dos bens'],['notSmallGift','Não se trata de amostra ou oferta de pequeno valor abrangida pela excepção3(3)e']],
exempt_sector_goods:[['exemptSector','Os bens são afectados ao sector de actividade isento identificado']],
fixed_asset_20a:[['fixedAsset','Está identificada a afectação ao activo imobilizado'],['goods20a','Os bens correspondem aos referidos em20(1)a,com âmbito e referência identificados']],
} as const;
export function allocationSpecificConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return allocationOwnConditions[treatment as keyof typeof allocationOwnConditions]??[];}
type Choice=''|'yes'|'no';type Condition=typeof goodsAllocationConditions[number][0];
export type GoodsAllocationRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationDate:string;operationRef:string;documentRef:string;documentDate:string;goodsRef:string;deductionRef:string;destinationRef:string;exceptionRef:string;asset20aRef:string;valuationMode:string;acquisitionPrice:string;acquisitionRef:string;currentCost:string;costRef:string;valuationDate:string;valuationScope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';classification:'assimilated_goods'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;reason:string};
export type GoodsAllocationCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:GoodsAllocationRow[];rows:LineResult[]};
export type GoodsAllocationBook={rows:GoodsAllocationRow[];result?:GoodsAllocationCalculation;history?:GoodsAllocationCalculation[]};
type Store=ReturnType<typeof readAcquisitionStore>&{regulatedGoodsAllocations?:Record<string,GoodsAllocationBook>};
export function newGoodsAllocation(id:string,currency:string):GoodsAllocationRow{return{id,treatment:'',description:'',period:'',origin:'projected',currency,operationDate:'',operationRef:'',documentRef:'',documentDate:'',goodsRef:'',deductionRef:'',destinationRef:'',exceptionRef:'',asset20aRef:'',valuationMode:'',acquisitionPrice:'',acquisitionRef:'',currentCost:'',costRef:'',valuationDate:'',valuationScope:'',evidence:'',conditions:{}};}
export function switchAllocationTreatment(row:GoodsAllocationRow,treatment:string):GoodsAllocationRow{return{...row,treatment,conditions:Object.fromEntries(goodsAllocationConditions.map(([key])=>[key,row.conditions[key]??'']))};}
export function switchAllocationValuation(row:GoodsAllocationRow,valuationMode:string):GoodsAllocationRow{return{...row,valuationMode,conditions:{...row.conditions,valuationConfirmed:'',acquisitionUnavailable:''}};}
function readStore(draft:Record<string,unknown>):Store{
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.regulatedGoodsAllocations!==undefined&&(!store.regulatedGoodsAllocations||typeof store.regulatedGoodsAllocations!=='object'||Array.isArray(store.regulatedGoodsAllocations)))throw new Error('Afectações de bens não recuperáveis;originais preservados.');
const fields=Object.keys(newGoodsAllocation('','')).filter(key=>key!=='conditions');
for(const book of Object.values(store.regulatedGoodsAllocations??{}))if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(key=>typeof row[key as keyof GoodsAllocationRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de afectações de bens não recuperáveis;originais preservados,sem substituição por lista vazia.');
return store;
}
export function goodsAllocationBook(draft:Record<string,unknown>):GoodsAllocationBook{return readStore(draft).regulatedGoodsAllocations?.[acquisitionScope(draft)]??{rows:[]};}
export function saveGoodsAllocationBook(draft:Record<string,unknown>,book:GoodsAllocationBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,regulatedGoodsAllocations:{...current.regulatedGoodsAllocations,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
function decimal(raw:unknown):bigint|null{if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const[whole,fraction='']=raw.trim().split('.');return BigInt(whole)*1000000n+BigInt(fraction.padEnd(6,'0'));}
function money(n:bigint):string{return`${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
function validDate(value:string):boolean{const d=new Date(value+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===value;}
export function evaluateGoodsAllocation(row:GoodsAllocationRow,currency:unknown):LineResult{
const result:LineResult={id:row.id,status:'indeterminate',classification:'indeterminate',baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,classification:status==='not_applicable'?'not_applicable' as const:result.classification,reason});
const rule=goodsAllocationTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique afectação permanente,transmissão gratuita,sector isento ou activo imobilizado20(1)a.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin)||!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period)return stop('Identifique mês2026,data correspondente e dados efectivos ou pressupostos expressos.');
const conditions=[...goodsAllocationConditions,...allocationSpecificConditions(row.treatment)];if(conditions.some(([key])=>row.conditions[key]==='no'))return stop('Este percurso de transmissão assimilada não se aplica aos factos declarados;conferir natureza,dedução histórica ou excepção,não presumir isenção/IVAzero.','not_applicable');
if(conditions.some(([key])=>row.conditions[key]!=='yes'))return stop('Factos próprios ou dedução anterior por esclarecer;desconhecido não é não aplicável ou zero. Dedução histórica não é nova dedução concedida.');
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.goodsRef.trim()||!row.deductionRef.trim()||!row.destinationRef.trim()||!row.evidence.trim())return stop('Identifique bens,operação,documento/data,dedução histórica,destino eâmbito factual ou pressupostos expressos;reutilize referências,sem certificado/aprovador novo.');
if(rule.clause==='e'&&!row.exceptionRef.trim())return stop('Identifique os factos/referência que distinguem a operação da excepção de amostras/ofertas de pequeno valor,ou pressuposto expresso;sem limiar inventado.');
if(row.treatment==='fixed_asset_20a'&&!row.asset20aRef.trim())return stop('Identifique enquadramento dos bens20(1)a eafectação ao activo imobilizado,com referência/âmbito.');
result.classification='assimilated_goods';
if(currency!=='MZN'||row.currency!==currency)return stop('Operação3(3)'+rule.clause+' identificada;moeda fora do recorteMZN ou divergente,base indeterminada,sem conversão silenciosa.');
if(!['acquisition','cost'].includes(row.valuationMode))return stop('Operação3(3)'+rule.clause+' identificada;base15(2)b por esclarecer:preço aquisição ou,na sua falta demonstrada,preço custo,reportados ao momento da operação.');
if(row.conditions.valuationConfirmed!=='yes'||!validDate(row.valuationDate)||row.valuationDate!==row.operationDate||!row.valuationScope.trim())return stop('Operação assimilada identificada;base por esclarecer. Confirme preço de aquisição/custo reportado ao momento desta operação,data/âmbito ecomposição sem próprioIVA;não usar custo histórico automaticamente.');
if(row.valuationMode==='cost'&&row.conditions.acquisitionUnavailable!=='yes')return stop('Operação assimilada identificada;não substituir aquisição desconhecida por custo. O custo15(2)b só é usado na falta demonstrada de preço aquisição;conferir esse facto.');
const raw=row.valuationMode==='acquisition'?row.acquisitionPrice:row.currentCost,ref=row.valuationMode==='acquisition'?row.acquisitionRef:row.costRef;const amount=decimal(raw);
if(amount===null||!ref.trim())return stop('Operação assimilada identificada;indique o preço e referência correspondentes ao modo escolhido,reportados ao momento da operação;desconhecido não é zero.');
return{...result,status:'determined',baseAmount:money(amount),reason:`P124:transmissão assimilada3(3)${rule.clause};P088:base15(2)b=${money(amount)}MZN,${row.valuationMode==='acquisition'?'preço aquisição':'preço custo,na falta demonstrada de preço aquisição'},reportado a${row.operationDate},${row.origin==='projected'?'sob pressupostos expressos':'segundo dados efectivos declarados'}. Dedução anterior total/parcial é facto histórico dos bens/elementos,não nova dedução/campo11. Não confundir afectação/transmissão de bens comserviço por uso temporárioP089. Sem calcular IVA,taxa,regularização ou totaisModeloA.`};
}
function key(draft:Record<string,unknown>,book:GoodsAllocationBook):string{return JSON.stringify({version:goodsAllocationVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
export function goodsAllocationCurrent(draft:Record<string,unknown>,book:GoodsAllocationBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
export function recordGoodsAllocations(draft:Record<string,unknown>,book:GoodsAllocationBook):GoodsAllocationBook{
if(!book.rows.length||new Set(book.rows.map(row=>row.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos;originais conservados.');
const result:GoodsAllocationCalculation={version:goodsAllocationVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:goodsAllocationSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateGoodsAllocation(row,draft.currency))};
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveGoodsAllocationBook(draft,next);return next;
}
export function goodsAllocationReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
try{const book=goodsAllocationBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>{const line=result.rows[index],rule=goodsAllocationTreatments.find(t=>t.id===row.treatment);return['IVA mensal',`Afectação P124/baseP088 — ${row.period} — ${row.id}`,`${row.description};classificação:${line.classification==='assimilated_goods'?'Transmissão assimilada de bens':line.classification==='not_applicable'?'Percurso não aplicável':'Indeterminada'};base:${line.baseAmount??'Indeterminada'} ${row.currency};IVA: Indeterminado;${line.reason}`,`${result.opportunity};${result.location};${goodsAllocationCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin==='projected'?'Projecção':'Dados efectivos declarados'};${rule?.label??'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};bens ${row.goodsRef};dedução histórica ${row.deductionRef};destino ${row.destinationRef};${rule?.clause==='e'?`excepção amostras/ofertas ${row.exceptionRef};`:''}${row.treatment==='fixed_asset_20a'?`enquadramento20(1)a ${row.asset20aRef};`:''}base ${row.valuationMode};${row.valuationMode==='cost'?`custo ${row.currentCost},${row.costRef};falta aquisição ${row.conditions.acquisitionUnavailable??'por indicar'}`:`aquisição ${row.acquisitionPrice},${row.acquisitionRef}`};data/âmbito base ${row.valuationDate},${row.valuationScope};documento ${row.documentRef},${row.documentDate};condições ${[...goodsAllocationConditions,...allocationSpecificConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};sem nova dedução/campo11/IVA/regularização/totaisModeloA.`];});}
catch{return[['IVA mensal','Afectação de bens','Dados não recuperáveis','Originais preservados;sem base ou classificação por defeito.']];}
}
+2
View File
@@ -19,6 +19,7 @@ import { serviceLocationReportRows } from './iva-service-location.ts';
import { listedServiceReportRows } from './iva-listed-services.ts';
import { reducedBaseReportRows } from './iva-reduced-bases.ts';
import { ownNameReportRows } from './iva-own-name.ts';
import { goodsAllocationReportRows } from './iva-goods-allocation.ts';
import { offerPreparationReportRows } from './iva-offer-preparations.ts';
import { salesReportRows } from './iva-operations.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
@@ -648,6 +649,7 @@ export function buildProjectDossierRows(
rows.push(...listedServiceReportRows(draft));
rows.push(...reducedBaseReportRows(draft));
rows.push(...ownNameReportRows(draft));
rows.push(...goodsAllocationReportRows(draft));
rows.push(...offerPreparationReportRows(draft));
const operationRows = salesReportRows(draft);
rows.push(...operationRows);
@@ -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 IvaGoodsAllocationDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_goods_allocation_recover_privately_preserving_all_delivered_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()]));
$draft = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-goods-allocation.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:353', $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['regulatedGoodsAllocations']['opportunity:353'];
$this->assertCount(4, $book['rows']);
$this->assertSame(['permanent_private_goods','free_goods_transfer','exempt_sector_goods','fixed_asset_20a'],array_column($book['rows'],'treatment'));
$this->assertSame(['1000.25','800.15','1000.25','800.15'],array_column($book['result']['rows'],'baseAmount'));
$this->assertSame(array_fill(0,4,'assimilated_goods'),array_column($book['result']['rows'],'classification'));
foreach ($book['result']['rows'] as $row) {
$this->assertNull($row['vat']);$this->assertNull($row['rate']);
}
$this->assertSame($book['rows'], $book['result']['inputs']);
$this->assertCount(1, $book['history']);
$this->assertSame('1000.25',$book['history'][0]['rows'][0]['baseAmount']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-goods-allocation-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$this->assertSame($prior, array_diff_key($store, ['regulatedGoodsAllocations' => true]));
$this->assertSame('1250.30', $store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['baseAmount']);
$this->assertNull($store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['vat']);
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
file_put_contents(base_path('outputs/iva-l1-goods-allocation-20261025/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
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,29 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newGoodsAllocation,switchAllocationTreatment,switchAllocationValuation,goodsAllocationConditions,allocationSpecificConditions,goodsAllocationTreatments,evaluateGoodsAllocation,recordGoodsAllocations,saveGoodsAllocationBook,goodsAllocationBook,goodsAllocationCurrent,goodsAllocationReportRows} from '../../resources/js/lib/iva-goods-allocation.ts';
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-goods-allocation-preserved.json','utf8'));const draft={...frozen,currency:'MZN',context_opportunity:'opportunity:353',project_title:'SYNTHETIC QA — Afectação de bens / Manica'};
function row(t){const cost=['free_goods_transfer','fixed_asset_20a'].includes(t);return{...newGoodsAllocation(t,'MZN'),treatment:t,description:'SYNTHETIC QA '+t,period:'2026-12',origin:'projected',operationDate:'2026-12-12',operationRef:'SYNTHETIC QA operação',documentRef:'SYNTHETIC QA documento/pressuposto',documentDate:'2026-12-12',goodsRef:'SYNTHETIC QA conjunto de bens/elementos',deductionRef:'SYNTHETIC QA dedução histórica parcial,bens/elementos identificados',destinationRef:'SYNTHETIC QA destino/finalidade/sector/activo eâmbito',exceptionRef:'SYNTHETIC QA factos fora da excepção de amostras/ofertas,não limiar inventado',asset20aRef:'SYNTHETIC QA bens20(1)a/activoimobilizado',valuationMode:cost?'cost':'acquisition',acquisitionPrice:'1000.25',acquisitionRef:'SYNTHETIC QA preço aquisição reportado ao momento',currentCost:'800.15',costRef:'SYNTHETIC QA custo reportado ao momento',valuationDate:'2026-12-12',valuationScope:'SYNTHETIC QA conjunto identificado/preço ao momento,semIVA',evidence:'SYNTHETIC QA pressupostos expressos',conditions:{...Object.fromEntries([...goodsAllocationConditions,...allocationSpecificConditions(t)].map(([key])=>[key,'yes'])),valuationConfirmed:'yes',acquisitionUnavailable:'yes'}};}
for(const rule of goodsAllocationTreatments)test('P124/P088 '+rule.id+' ownfacts,historicaldeduction and base',()=>{
const r=row(rule.id),result=evaluateGoodsAllocation(r,'MZN');assert.equal(result.status,'determined');assert.equal(result.classification,'assimilated_goods');assert.equal(result.baseAmount,r.valuationMode==='cost'?'800.15':'1000.25');assert.equal(result.vat,null);assert.equal(result.rate,null);
for(const [key] of [...goodsAllocationConditions,...allocationSpecificConditions(rule.id)]){const missing=structuredClone(r);delete missing.conditions[key];assert.equal(evaluateGoodsAllocation(missing,'MZN').classification,'indeterminate');missing.conditions[key]='no';assert.equal(evaluateGoodsAllocation(missing,'MZN').status,'not_applicable');assert.equal(evaluateGoodsAllocation(missing,'MZN').baseAmount,null);}
for(const field of ['description','operationRef','documentRef','goodsRef','deductionRef','destinationRef','evidence',...(rule.clause==='e'?['exceptionRef']:[]),...(rule.id==='fixed_asset_20a'?['asset20aRef']:[])])assert.equal(evaluateGoodsAllocation({...r,[field]:''},'MZN').classification,'indeterminate',field);
for(const patch of [{operationDate:'2026-11-12'},{documentDate:'2026-02-30'},{period:'2025-12'}])assert.equal(evaluateGoodsAllocation({...r,...patch},'MZN').status,'indeterminate');
});
test('P124classification independentof missingP088base;no newdeduction and no temporaryservice substitution',()=>{
const r=row('permanent_private_goods');for(const patch of [{valuationMode:''},{acquisitionPrice:''},{valuationDate:'2026-12-11'},{valuationScope:''},{currency:'USD'},{acquisitionRef:''}]){const result=evaluateGoodsAllocation({...r,...patch},'MZN');assert.equal(result.status,'indeterminate');assert.equal(result.classification,'assimilated_goods');assert.equal(result.baseAmount,null);}
assert.match(evaluateGoodsAllocation(r,'MZN').reason,/não nova dedução/);assert.match(evaluateGoodsAllocation(r,'MZN').reason,/P089/);
const noPrior=structuredClone(r);noPrior.conditions.priorDeduction='no';assert.equal(evaluateGoodsAllocation(noPrior,'MZN').classification,'not_applicable');assert.equal(evaluateGoodsAllocation(noPrior,'MZN').baseAmount,null);
});
test('P088costfallback explicit,currentmoment,hidden retention and exact amounts',()=>{
const r=row('free_goods_transfer');for(const flag of ['','no']){const result=evaluateGoodsAllocation({...r,conditions:{...r.conditions,acquisitionUnavailable:flag}},'MZN');assert.equal(result.classification,'assimilated_goods');assert.equal(result.baseAmount,null);}
assert.equal(evaluateGoodsAllocation({...r,acquisitionPrice:'999999'},'MZN').baseAmount,'800.15');assert.equal(evaluateGoodsAllocation({...r,currentCost:'0'},'MZN').baseAmount,'0.00');assert.equal(evaluateGoodsAllocation({...r,currentCost:'0.000001'},'MZN').baseAmount,'0.000001');for(const v of ['','-1','1e3','1,5'])assert.equal(evaluateGoodsAllocation({...r,currentCost:v},'MZN').baseAmount,null);
const acquisition=switchAllocationValuation(r,'acquisition');assert.equal(acquisition.currentCost,r.currentCost);assert.equal(acquisition.acquisitionPrice,r.acquisitionPrice);assert.equal(acquisition.conditions.valuationConfirmed,'');assert.equal(evaluateGoodsAllocation(acquisition,'MZN').baseAmount,null);acquisition.conditions.valuationConfirmed='yes';assert.equal(evaluateGoodsAllocation(acquisition,'MZN').baseAmount,'1000.25');
const changed=switchAllocationTreatment(r,'fixed_asset_20a');assert.equal(changed.exceptionRef,r.exceptionRef);assert.equal(changed.asset20aRef,r.asset20aRef);assert.equal(changed.conditions.fixedAsset,undefined);assert.equal(evaluateGoodsAllocation(changed,'MZN').classification,'indeterminate');
});
test('Sharedoperation under2requirements persists once;all previous books/history/report preserved',()=>{
const rows=goodsAllocationTreatments.map(t=>row(t.id));let book=recordGoodsAllocations(draft,{rows});book=recordGoodsAllocations(draft,book);assert.equal(book.history.length,1);assert.notEqual(book.result.inputs,book.rows);
const next={...draft,modelo_a_snapshot:saveGoodsAllocationBook(draft,book)};assert.deepEqual(goodsAllocationBook(next),book);assert.ok(goodsAllocationCurrent(next,book));assert.equal(goodsAllocationCurrent({...next,currency:'USD'},book),false);assert.equal(goodsAllocationCurrent({...next,context_opportunity:'other'},book),false);assert.equal(goodsAllocationReportRows(next).length,4);assert.ok(goodsAllocationReportRows(next).every(r=>r[1].includes('P124/baseP088')));
const store=JSON.parse(next.modelo_a_snapshot);delete store.regulatedGoodsAllocations;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));assert.throws(()=>recordGoodsAllocations(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>goodsAllocationBook({...next,modelo_a_snapshot:JSON.stringify({...JSON.parse(next.modelo_a_snapshot),regulatedGoodsAllocations:[]})}));
if(process.env.IVA_ALLOCATION_FIXTURE)fs.writeFileSync(process.env.IVA_ALLOCATION_FIXTURE,JSON.stringify(next,null,2)+'\n');
});