Implement L2-A acquisition deduction rights with preserved history and affected totals

This commit is contained in:
2026-10-25 12:12:29 +02:00
parent 940d69b82a
commit 56c948ec45
11 changed files with 295 additions and 14 deletions
@@ -25120,5 +25120,40 @@
"matrix_anchor": "AUD-SIM-INT02",
"source_of_truth": "matriz e manual existentes;estrutura abaixo é recibo de reconciliação,não inventário paralelo de requisitos."
}
},
"l2_a_acquisition_right_qa_20261025": {
"state": "L2-A concluído em QA no âmbito demonstrado; não instalado",
"report": "docs/investmoz/simulations/l2-a-acquisition-right-qa-20261025.json",
"matrix_anchor": "AUD-SIM-INT02",
"l1_preserved": {
"state": "operacional com limitações explícitas",
"delivered": 129,
"functional_residuals": [
"P058",
"P084",
"P085"
],
"documentary_residual": "P001 — exclusivamente correspondência05/06"
},
"F": {
"accompanies": "L2-A",
"example_MZN": {
"base14": "500.00",
"deduction15": "48.00",
"output16": "21.00",
"difference17": "0.00",
"difference18": "27.00"
},
"missing_document": "Campos09/15/17/18 indeterminados,sem substituir porzero.",
"duplicate_guard": "Mesma linha de factura em projecção/efectivo bloqueada no livro; operações de outras oportunidades não são consolidadas silenciosamente.",
"not_included": "Campos19–27,créditos anteriores,pagamentos/reembolsosL3; diferenças17/18 não equivalem a ordem de pagamento/reembolso."
},
"remaining_dependencies": {
"B_C": "Preparar conjuntamente quando compatíveis:documento aduaneiro/importação e exclusões/excepções;não tratar fornecedor estrangeiro como fornecedor interno. Bebidas permanecem no âmbitoL2/L3.",
"D": "Afectação parcial/pro rata e proporções específicas18(6);não presumir dedução integral.",
"E": "Regularizações dependem de evento,documento e histórico original;sem recalcular versões anteriores.",
"F": "Repetir sódelta dos totais afectados em cada incremento."
},
"active_writes": false
}
}
@@ -0,0 +1,55 @@
{
"state": "L2-A concluído em QA no âmbito demonstrado; não instalado",
"matrix_anchor": "AUD-SIM-INT02",
"installed_revision": "ada9629b",
"installed_receipt": "7febb6f7",
"l1": {
"state": "operacional com limitações explícitas",
"delivered": 129,
"functional_residuals": [
"P058",
"P084",
"P085"
],
"documentary_residual": "P001 — exclusivamente correspondência05/06"
},
"scope": "Extensão dos livros de compras e da classificação existentes. Aquisições internas MZN/2026,afectação exclusiva,documento e dedução no mesmo período,sem despesas especiais. Vínculo explícito à operação; motivos e fontes individuais; elegível/sem direito/indeterminado. Projecção permanece pressuposto. Sem reconstruir o motor.",
"reuse": "Documento,valores,moeda,período e edições deliberadas preservados. Seleccionar origem existente reutiliza classificação,sem copiar valor da venda para compra nem criar segunda aquisição. Operação alterada exige adopção expressa e revisão da afectação.",
"preservation": "Histórico congelado,sem recálculo automático. Livros anteriores sem vínculo conservam cálculo e chaves anteriores. Livro que iniciaL2-A conserva essa versão ao remover vínculos: informação em falta permanece indeterminada. Todos os mapas anteriores da fixture comparados integralmente em frontend e API.",
"legal_delta": "Cotejo delimitado de fontes oficiais já aceites:Lei13/2016 PDF15/p1426(273),art19(1);Lei10/2025 PDF13/p2180(13),18(2)/19(2).17-A sem direito apesar de IVA liquidado. Não inferir direito pela caixa05/06/07 ou IVAzero. Correspondência não implementada fica indeterminada.",
"F": {
"accompanies": "L2-A",
"example_MZN": {
"base14": "500.00",
"deduction15": "48.00",
"output16": "21.00",
"difference17": "0.00",
"difference18": "27.00"
},
"missing_document": "Campos09/15/17/18 indeterminados,sem substituir porzero.",
"duplicate_guard": "Mesma linha de factura em projecção/efectivo bloqueada no livro; operações de outras oportunidades não são consolidadas silenciosamente.",
"not_included": "Campos19–27,créditos anteriores,pagamentos/reembolsosL3; diferenças17/18 não equivalem a ordem de pagamento/reembolso."
},
"validation": {
"frontend": "16 testes dirigidos, incluindo12 anteriores reutilizados de compras/continuidade e4 novos de direito",
"api": "1teste/74asserções;DatabaseTransactions rollback;guardcurrent_database=simulator_fiscal_delta_20261021_test,port55441",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"ui": "Componente real em127.0.0.1:18708;posse em falta→Indeterminado→gravarficheiroQA→recuperar→relatório com motivo/fonte;API privada conferida separadamente",
"ui_proof": "outputs/iva-l2-a-20261025/ui-recovered-report.png"
},
"remaining_dependencies": {
"B_C": "Preparar conjuntamente quando compatíveis:documento aduaneiro/importação e exclusões/excepções;não tratar fornecedor estrangeiro como fornecedor interno. Bebidas permanecem no âmbitoL2/L3.",
"D": "Afectação parcial/pro rata e proporções específicas18(6);não presumir dedução integral.",
"E": "Regularizações dependem de evento,documento e histórico original;sem recalcular versões anteriores.",
"F": "Repetir sódelta dos totais afectados em cada incremento."
},
"active_writes": false,
"schema_changes": false,
"hashes": {
"resources/js/lib/iva-acquisition-right.ts": "ba63b02a99f476ae035a9301fdab6558363c603470e3f5f1e8ee061a6fbf9ed5",
"resources/js/lib/iva-acquisitions.ts": "3eff66925378d81353b6c9d3dccf1ca53f9fc5ad5ba3eef963d7ce47c0b1d2b0",
"resources/js/components/acquisition-operation-link.tsx": "8cb2afa1048507733d1a8e26f99a982858c86749fa8595655e17195477e20c1c",
"resources/js/components/simulation-acquisitions.tsx": "2595d6667b3adebba631ff6aa56490d1fdb8c4fdc127aa54234318006d42d8eb"
}
}
@@ -1,5 +1,25 @@
# Rastreabilidade, utilização e formação
## 25/10/2026 — L2-A em QA: aquisição → direito à dedução
L1 permanece operacional com limitações explícitas:129entregues+P058/P084/P085+P001=133,sem reabertura dos fechos.Homologaçãoada9629b/recibo7febb6f7 inalterada.L2-A está preparado emQA,no âmbito de aquisição internaMZN/2026,afectação exclusiva e dedução no mês do documento;não representa conclusão deL2.
No livro de compras existente,abra a aquisição e reutilize a operação já classificada. Confirme a afectação exclusiva e registe o motivo/referência. Montantes,documento,moeda,período e edições anteriores não são substituídos pela venda ligada.Não é criada outra compra.O direito resulta da classificação e das condições próprias da aquisição:Elegível,Sem direito ouIndeterminado,sempre com motivo e fonte.O caso17-A não ganha direito por ter IVA liquidado.Isenção não implica automaticamente ausência de direito;os fundamentos expressos19(1)(b) são distinguidos.
Complete apenas informação em falta sobre documento,fornecedor,posse,afectação e exclusões. Uma classificação alterada exige adopção expressa e nova conferência da afectação;históricos permanecem congelados.Retirar um vínculo num livroL2-A deixa o direito indeterminado,sem recuperar silenciosamente dedução anterior.Livros anteriores conservam comportamento e versões existentes.Não se calcula importação,despesa especial,afectação mista/proporção18(6),dedução posterior ou regularização neste incremento.
Use «Calcular e guardar versão do apuramento parcial» e a gravação privada do simulador.A recuperação conserva origem,estado,motivo,fonte,montantes e histórico.O relatório usa o resultado congelado,assinalando entradas desactualizadas.Sem documento/posse completos,não aplicar montante dedutível;sem correspondência do direito,não inferir pelas caixas05/06/07.Mesma linha de factura repetida entre projecção e efectivo é impedida neste livro.
F acompanha este incremento:exemploQA base14=500,00;dedução15=48,00;IVA16=21,00;diferença17=0,00;diferença18=27,00MZN.Retirar a posse de um documento deixa09/15/17/18 indeterminados,semzero presumido.Campos19–27,créditos e restituições continuam fora deste incremento,com ligação aL3.
Validação dirigida:16testesfrontend;API1teste/74asserções na baseQA isolada55441 com transacção revertida;tipos e build;componente real com gravação→recuperação→relatório.Os129fechos e os quatro residuais foram comparados sem alterações;mapas anteriores da fixture preservados integralmente.Próximo conjuntoB/C por dependências compatíveis;D/E seguem afectação e eventos originais;F acompanha cada entrega.Sem novas pesquisasL1 ou repetição das suas provas aceites.
ReciboQA:docs/investmoz/simulations/l2-a-acquisition-right-qa-20261025.json.Prova visual:outputs/iva-l2-a-20261025/ui-recovered-report.png.
## 25/10/2026 — preparação QA de L2-A: aquisição e direito à dedução
Inventário antes dos testes:129fechos e quatro residuais L1 preservados;L1 operacional com limitações explícitas,homologação ada9629b/recibo7febb6f7.L2-A estende os livros de compras/classificação existentes,sem reconstruir motor e sem alteração no activo.Cotejo restrito de18/19 para mapear direito da operação versus condições da aquisição;17-A não confere direito apesar de existir imposto.Não calcular proporção18(6),pro rata,importações,excepções ou regularizações neste incremento.Provas anteriores reutilizadas e delta novo dirigido.
## 25/10/2026 — L1 operacional com limitações explícitas; delimitação executável de L2
**Estado registado: L1 operacional com limitações explícitas**, sobre `ada9629b`, recibo `7febb6f7`. **129 entregues + 3 ramos funcionais pendentes + P001 = 133**. Não significa133itens concluídos. Os quatro residuais permanecem abertos na mesma matriz; o registo permite avançar comL2 independente. Origem:instrução do utilizador nesta conversa. A apreciação não constitui conferência independente dos anexos nem aceitação em nome daTempo; não se acrescenta validação organizacional a esse registo.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
import {acquisitionOperationChoices,createAcquisitionRightLink,linkedOperationRight,type AcquisitionRightLink} from '@/lib/iva-acquisition-right';
import type {Acquisition} from '@/lib/iva-acquisitions';
export function AcquisitionOperationLink({draft,row,onChange}:{draft:Record<string,unknown>;row:Acquisition;onChange:(patch:Partial<Acquisition>)=>void}){
const choices=acquisitionOperationChoices(draft),link=row.deductionLink;
const adopt=(id:string)=>onChange({deductionLink:createAcquisitionRightLink(draft,id)});
const patch=(value:Partial<AcquisitionRightLink>)=>{if(link)onChange({deductionLink:{...link,...value}});};
const result=link?linkedOperationRight(draft,link):null;
return <fieldset className="space-y-2 rounded border p-3"><legend>Aquisição → direito à dedução — L2-A</legend>
<p>Reutilize a classificação existente. A ligação conserva montantes, documento e escolhas da aquisição; não cria outra compra. Importações, despesas especiais e afectação parcial conservam enquadramento próprio.</p>
{choices.length===1&&!link?<button type="button" onClick={()=>adopt(choices[0].id)}>Reutilizar operação existente: {choices[0].label}</button>:<label>Operação de destino<select value={link?.operationId??''} onChange={e=>e.target.value?adopt(e.target.value):onChange({deductionLink:undefined})}><option value="">Sem vínculo L2-A</option>{choices.map(c=><option key={c.id} value={c.id}>{c.label}</option>)}</select></label>}
{!choices.length&&<p>Não há operação classificada nesta oportunidade. A compra e o histórico permanecem disponíveis.</p>}
{link&&<><p>{result?.reason}</p><small>{result?.source}</small>
<label className="block">Afectação exclusiva à operação ligada<select value={link.scopeConfirmed} onChange={e=>patch({scopeConfirmed:e.target.value as AcquisitionRightLink['scopeConfirmed']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / por rever</option></select></label>
<label className="block">Motivo ou referência da afectação<input value={link.allocationRef} onChange={e=>patch({allocationRef:e.target.value})}/></label>
{result?.status==='eligible'&&<label className="block">Aquisição fora dos pagamentos excluídos15(6)(c)/19(2)<select value={link.noExcludedPayments} onChange={e=>patch({noExcludedPayments:e.target.value as AcquisitionRightLink['noExcludedPayments']})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>}
{result?.status==='indeterminate'&&choices.some(c=>c.id===link.operationId)&&<button type="button" onClick={()=>adopt(link.operationId)}>Adoptar explicitamente a classificação actual e rever afectação</button>}
</>}
</fieldset>;
}
@@ -1,4 +1,6 @@
import { useState } from 'react';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { acquisitionRightVersion } from '@/lib/iva-acquisition-right';
import { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, recordAcquisitions, newAcquisition, reuseAcquisitions, saveAcquisitionBook, type Acquisition, type AcquisitionBook } from '@/lib/iva-acquisitions';
import { acquisitionTreatmentIssue, continueWithoutFormerExploration, formerExplorationSource, type AcquisitionTreatment } from '@/lib/iva-acquisition-continuity';
@@ -8,7 +10,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
let book:AcquisitionBook;
try {book=acquisitionBook(draft);} catch(cause) {return <p role="alert">{String(cause)}</p>;}
const save=(next:AcquisitionBook)=>{try {onChange('modelo_a_snapshot',saveAcquisitionBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<Acquisition>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
const change=(id:string,patch:Partial<Acquisition>)=>save({...book,...('deductionLink' in patch?{rightVersion:acquisitionRightVersion}:{}),rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
let references:ReturnType<typeof acquisitionPlanReferences>=[];
try {references=acquisitionPlanReferences(draft);}catch { /* The action reports the concrete currency/source error without replacing saved values. */ }
@@ -20,7 +22,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<p>Oportunidade em tratamento: {String(draft.context_opportunity||'entrada geral')}. Moeda de origem: {book.currency||'por indicar'}. A compra registada não gera automaticamente IVA dedutível.</p>
{book.currency && draft.currency!==book.currency && <p role="alert">As compras conservam {book.currency}; o projecto está em {String(draft.currency||'moeda por indicar')}. Reveja a moeda no projecto. Os montantes e o resultado anterior não foram convertidos ou reinterpretados.</p>}
{!book.currency && !!draft.currency && <button type="button" className="underline" onClick={()=>save({...book,currency:String(draft.currency),rows:book.rows.map(row=>row.sourceCurrency?row:{...row,sourceCurrency:String(draft.currency)})})}>Confirmar explicitamente {String(draft.currency)} como moeda de origem dos valores sem moeda, conservando os montantes</button>}
<p>Primeiro âmbito: aquisições internas em MZN, afectação exclusiva a operações tributadas, sem exclusões ou dedução parcial, documento e dedução no mesmo período de 2026. Nas projecções, cada confirmação é um pressuposto, sem certificar documento ou declaração efectiva. Indique dois valores entre líquido, IVA e bruto; o terceiro é obtido por diferença ou soma, sem taxa de compra presumida.</p>
<p>L2-A: aquisições internas em MZN e afectação exclusiva a uma operação classificada, com direito, sem direito ou indeterminado. Documento e dedução no mesmo período de2026; importações, exclusões especiais, afectação parcial e regularizações conservam percursos próprios. Nas projecções, cada confirmação é um pressuposto. Indique dois valores entre líquido, IVA e bruto; o terceiro é obtido sem taxa de compra presumida.</p>
<label className="block">Adquirente sujeito passivo de IVA no regime normal (efectivo ou pressuposto da projecção)<select className="block rounded border p-2" value={book.taxpayerRegistered??''} onChange={e=>save({...book,taxpayerRegistered:e.target.value as AcquisitionBook['taxpayerRegistered']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / outro enquadramento — fora deste lote</option></select></label>
{error&&<p role="alert">{error}</p>}
<details className="rounded border p-2"><summary>Indicar mês da aquisição pelo documento ou pressuposto da projecção</summary>
@@ -29,8 +31,9 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<button type="button" onClick={()=>run(()=>{if(!/^2026-(0[1-9]|1[0-2])$/.test(manualPeriod))throw new Error('Indique um mês válido de2026.');if(book.periods.some(p=>p.period===manualPeriod))throw new Error('Esse mês já consta da cobertura; valores conservados.');save({...book,periods:[...book.periods,{period:manualPeriod,complete:false,absent:[]}]});})}>Acrescentar mês indicado</button>
</details>
<button type="button" className="underline" onClick={()=>run(()=>save(reuseAcquisitions(draft,book)))}>Reutilizar compras e períodos existentes</button>
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>save({...book,rows:[...book.rows,newAcquisition(crypto.randomUUID(),book.periods[0]?.period??'',book.currency)]})}>Acrescentar aquisição em falta</button>
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>save({...book,rightVersion:acquisitionRightVersion,rows:[...book.rows,newAcquisition(crypto.randomUUID(),book.periods[0]?.period??'',book.currency)]})}>Acrescentar aquisição em falta</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Aquisição adicional'} — {row.period} — campo{row.field}</summary>
<AcquisitionOperationLink draft={draft} row={row} onChange={patch=>change(row.id,patch)}/>
<details className="rounded border p-2"><summary>Enquadramento especial da aquisição / referência anterior</summary>
<label>Tratamento da aquisição<select value={row.acquisitionTreatment??''} onChange={e=>row.acquisitionTreatment==='former_exploration_exemption'&&e.target.value!=='former_exploration_exemption'?run(()=>save({...book,rows:book.rows.map(item=>item.id===row.id?continueWithoutFormerExploration(item):item)})):change(row.id,{acquisitionTreatment:e.target.value as AcquisitionTreatment})}><option value="">Enquadramento normal; sem benefício especial indicado</option><option value="former_exploration_exemption">Antiga isenção de prospecção/pesquisa —14(2)(c),eliminada</option><option value="normal">Continuidade pelo enquadramento normal</option></select></label>
{acquisitionTreatmentIssue(row.acquisitionTreatment)&&<p role="status">{acquisitionTreatmentIssue(row.acquisitionTreatment)}</p>}
@@ -56,7 +59,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
{select(row,'supplierRegistered','Fornecedor inscrito no IVA',yesNo)}
{select(row,'legalDocument','Documento emitido em forma legal',yesNo)}
{select(row,'held','Documento na posse do adquirente / previsto na projecção',yesNo)}
{select(row,'allocation','Afectação',[['taxable_only','Exclusiva a operações tributadas'],['mixed','Mista / parcial — fora deste lote'],['no_right','Operações sem direito à dedução']])}
{select(row,'allocation','Afectação',[['taxable_only',row.deductionLink?'Exclusiva à operação ligada':'Exclusiva a operações tributadas'],['mixed','Mista / parcial — fora deste lote'],['no_right','Operações sem direito à dedução']])}
{select(row,'exclusion','Exclusões de dedução',[['none','Sem exclusões ou casos especiais dos arts.18–20'],['excluded','Despesa excluída'],['special','Veículos, combustível, viagens, alojamento, comunicações, luxo ou outra situação especial — por enquadrar']])}
{select(row,'integrity','Operação real / pressuposto regular, sem simulação, margem de segunda mão ou impedimentos do art.18(3–5)',yesNo)}
</>}
+26
View File
@@ -0,0 +1,26 @@
import {acquisitionScope} from './iva-acquisitions.ts';
import {operationTreatments,salesBook,salesCurrent,type SalesOperation,type SalesLineResult} from './iva-operations.ts';
export const acquisitionRightVersion='acquisition-operation-right/1';
export const acquisitionRightSource='L2-A — CIVA19(1)(a)/(b)(i/iv/v),19(2),Lei13/2016 PDF15/p1426(273);Lei10/2025 PDF13/p2180(13):18(2),19(2),exclusão17-A. Condições da aquisição18/20/21 continuam próprias. Sem resolverP00105/06,importações,afectação parcial,excepções ou regularizações.';
export type AcquisitionRightLink={version:1;scopeId:string;operationId:string;fingerprint:string;scopeConfirmed:''|'yes'|'no';allocationRef:string;noExcludedPayments:''|'yes'|'no'};
export type AcquisitionEligibility={status:'eligible'|'no_right'|'indeterminate';reason:string;source:string;operationId?:string;operationPeriod?:string;operationTreatment?:string;operationDescription?:string;operationArticle?:string;scopeId?:string};
export function validAcquisitionRightLink(value:unknown):value is AcquisitionRightLink{if(!value||typeof value!=='object'||Array.isArray(value))return false;const v=value as AcquisitionRightLink;return v.version===1&&['scopeId','operationId','fingerprint','allocationRef'].every(k=>typeof v[k as keyof AcquisitionRightLink]==='string')&&['','yes','no'].includes(v.scopeConfirmed)&&['','yes','no'].includes(v.noExcludedPayments);}
function fingerprint(row:SalesOperation,result:SalesLineResult|undefined){return JSON.stringify({version:acquisitionRightVersion,input:row,classification:result?{article:result.article,source:result.source,status:result.status}:null});}
export function acquisitionOperationChoices(draft:Record<string,unknown>){const book=salesBook(draft);return book.rows.map(row=>({id:row.id,label:`${row.description||row.id} — ${row.period} — ${operationTreatments.find(t=>t.id===row.treatment)?.label??row.treatment}`,row,current:salesCurrent(draft,book)}));}
export function createAcquisitionRightLink(draft:Record<string,unknown>,operationId:string):AcquisitionRightLink{const b=salesBook(draft),row=b.rows.find(r=>r.id===operationId);if(!row)throw Error('Operação de origem não encontrada nesta oportunidade;dados preservados.');return{version:1,scopeId:acquisitionScope(draft),operationId,fingerprint:fingerprint(row,b.result?.rows.find(r=>r.id===operationId)),scopeConfirmed:'',allocationRef:'',noExcludedPayments:''};}
export function linkedOperationRight(draft:Record<string,unknown>,link:AcquisitionRightLink):AcquisitionEligibility{
const base:AcquisitionEligibility={status:'indeterminate',reason:'',source:acquisitionRightSource,operationId:link.operationId,scopeId:link.scopeId};const stop=(reason:string,status:AcquisitionEligibility['status']='indeterminate')=>({...base,status,reason});
if(!validAcquisitionRightLink(link)||link.scopeId!==acquisitionScope(draft))return stop('Vínculo inválido ou pertencente a outra oportunidade;sem direito presumido.');
const b=salesBook(draft),row=b.rows.find(r=>r.id===link.operationId),line=b.result?.rows.find(r=>r.id===link.operationId);if(!row)return stop('Operação ligada removida/não recuperável;escolha uma origem disponível,sem apagar o histórico.');
base.operationPeriod=row.period;base.operationTreatment=row.treatment;base.operationDescription=row.description;base.operationArticle=line?.article;base.source+=';'+(line?.source??'Fonte da classificação por recuperar');
if(!salesCurrent(draft,b)||!line||line.status==='indeterminate')return stop('Classificação L1 de origem incompleta ou alterada;recupere/actualize a classificação,sem dedução por rótulo.');
if(link.fingerprint!==fingerprint(row,line))return stop('Operação de origem alterada:reveja e adopte explicitamente a origem actual;montantes e histórico da aquisição conservados.');
const rule=operationTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Correspondência do direito ainda não identificada para esta operação.');
if(/17-A/.test(rule.article))return stop('Operação abrangida por17-A:19(2),redacçãoLei10/2025,não permite dedução apesar de ter IVA liquidado.','no_right');
if(/19\(1\)\(b\)\((i|iv|v)\)/.test(rule.article))return stop('Direito da operação identificado em'+rule.article+';a aquisição continua sujeita às suas condições próprias.','eligible');
if(line.status==='calculated'&&rule.rate===16)return stop('Operação sujeita e não isenta:direito19(1)(a),sujeito a19(2) e condições próprias da aquisição.','eligible');
// This is a bounded match to the already implemented article9 catalogue, after
// checking its express article19 exceptions. Never use the ModeloA box as law.
if(line.status==='exempt'&&/\b9\(/.test(rule.article))return stop('Operação isenta do artigo9 fora das correspondências com direito identificadas19(1)(b);não confere dedução neste fundamento.','no_right');
return stop('Classificação disponível,mas correspondência do direito não implementada neste percurso;não inferir direito pela caixa05/06/07 ou por IVAzero.');
}
+38 -9
View File
@@ -2,6 +2,7 @@ import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { salesBook, salesCurrent as salesOperationsCurrent } from './iva-operations.ts';
import { linkedOperationRight, validAcquisitionRightLink, acquisitionRightVersion, acquisitionRightSource, type AcquisitionRightLink, type AcquisitionEligibility } from './iva-acquisition-right.ts';
import { acquisitionTreatmentIssue, acquisitionContinuityVersion, formerExplorationSource, type AcquisitionTreatment } from './iva-acquisition-continuity.ts';
import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.ts';
@@ -17,11 +18,12 @@ export type Acquisition = {
allocation: ''|'taxable_only'|'mixed'|'no_right'; exclusion: ''|'none'|'excluded'|'special';
integrity: Choice; description: string;
acquisitionTreatment?: AcquisitionTreatment;
deductionLink?: AcquisitionRightLink;
acquisitionTreatmentOrigin?: {id:'former_exploration_exemption';source:string};
};
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string};
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string; eligibility?:AcquisitionEligibility};
type PeriodInput = {period:string; complete:boolean; absent:string[]};
export type AcquisitionBook = {currency:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation;history?:AcquisitionCalculation[]};
export type AcquisitionBook = {currency:string; rightVersion?:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation;history?:AcquisitionCalculation[]};
export type AcquisitionCalculation = {version:string; revision:number; calculatedAt:string; opportunity:string; location:string; currency:string; sourceKey:string; basis:string; sources:string;
declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record<string,string|null>; exactFields?:Record<string,FiscalFraction|null>; reasons:string[]}>};
type Store = {version:1; books:Record<string,AcquisitionBook>};
@@ -39,6 +41,8 @@ export function readAcquisitionStore(raw:unknown):Store {
const value=JSON.parse(String(raw));
if (value?.version!==1 || !value.books || typeof value.books!=='object' || Array.isArray(value.books)) throw new Error('Compras fiscais guardadas não recuperáveis; dados preservados.');
for(const book of Object.values(value.books) as AcquisitionBook[]) {
if(book?.rightVersion!==undefined&&book.rightVersion!==acquisitionRightVersion)throw new Error('Versão do direito da aquisição não recuperável; original preservado.');
if(book?.rows?.some(row=>row.deductionLink!==undefined&&!validAcquisitionRightLink(row.deductionLink)))throw new Error('Vínculo da aquisição não recuperável; original preservado.');
if(book?.history!==undefined&&!Array.isArray(book.history))throw new Error('Histórico de aquisições não recuperável; originais preservados, sem substituição.');
if(!book || !Array.isArray(book.rows) || !Array.isArray(book.periods) || typeof book.currency!=='string') throw new Error('Formato de compras não recuperável; sem substituição.');
const stringKeys=Object.keys(newAcquisition('','','')).filter(key=>!['sourceMonth','sourceAmount'].includes(key));
@@ -86,7 +90,7 @@ function amount(raw:unknown):bigint|null {
function money(value:bigint):string {const a=value/1000000n,b=(value%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0');return `${a}.${b}`;}
function sum(values:Array<string|null>):string|null {const parsed=values.map(amount);return parsed.some(v=>v===null)?null:money((parsed as bigint[]).reduce((a,b)=>a+b,0n));}
function dateValid(raw:string):boolean {const date=new Date(`${raw}T00:00:00Z`);return /^\d{4}-\d{2}-\d{2}$/.test(raw) && Number.isFinite(date.getTime()) && date.toISOString().slice(0,10)===raw;}
export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegistered:Choice=''):AcquisitionResult {
function evaluateAcquisitionBase(row:Acquisition,currency:string,taxpayerRegistered:Choice=''):AcquisitionResult {
let n=amount(row.net),v=amount(row.vat),g=amount(row.gross);
if(n!==null && v!==null && g===null && !row.gross.trim()) g=n+v;
if(g!==null && v!==null && n===null && !row.net.trim() && g>=v) n=g-v;
@@ -99,7 +103,7 @@ export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegi
if(treatmentIssue)return stop(treatmentIssue);
if(n===null || v===null || g===null || n+v!==g) return stop('Indique dois valores válidos; líquido + IVA deve corresponder ao bruto.');
if(row.sourceMonth!==null && (!row.sourceBasis || amount(row.sourceAmount)!==(row.sourceBasis==='net'?n:g))) return stop('Classifique ou resolva a divergência do valor de origem; não foi reinterpretado.');
if(v===0n) return stop('IVA explicitamente indicado como zero; não existe imposto a deduzir nesta aquisição.','0.00');
if(v===0n&&!row.deductionLink) return stop('IVA explicitamente indicado como zero; não existe imposto a deduzir nesta aquisição.','0.00');
if(taxpayerRegistered!=='yes') return stop('Confirme o enquadramento do adquirente como sujeito passivo de IVA no regime normal; não é inferido da organização.');
if(row.allocation==='no_right' || row.exclusion==='excluded' || row.supplierRegistered==='no' || row.legalDocument==='no' || row.integrity==='no') return stop('Sem dedução no âmbito declarado: afectação, exclusão, fornecedor ou documento não elegível.','0.00');
if(row.domestic!=='yes') return stop('Aquisição interna por confirmar; importações fora deste lote.');
@@ -108,12 +112,28 @@ export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegi
if(!dateValid(row.documentDate) || row.documentDate.slice(0,7)!==row.period) return stop('Data do documento e período de dedução devem coincidir neste lote; dedução posterior não presumida.');
return stop(row.origin==='projected'?'Dedução projectada sob pressupostos explícitos; não certifica factura ou declaração.':'Dedução segundo dados e condições declarados; sem submissão ou certificação pela AT.',money(v));
}
export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegistered:Choice='',draft?:Record<string,unknown>):AcquisitionResult {
const result=evaluateAcquisitionBase(row,currency,taxpayerRegistered);
if(!row.deductionLink)return result;
const right=draft?linkedOperationRight(draft,row.deductionLink):{status:'indeterminate' as const,reason:'Contexto da operação em falta.',source:acquisitionRightSource};
const finish=(status:AcquisitionEligibility['status'],reason:string,deductible:string|null=null):AcquisitionResult=>({...result,deductible,reason,eligibility:{...right,status,reason,source:right.source+'; '+acquisitionBasis}});
if(currency!=='MZN'||row.sourceCurrency!==currency||!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||acquisitionTreatmentIssue(row.acquisitionTreatment)||result.net===null||result.vat===null||result.gross===null||(amount(result.net)! + amount(result.vat)!)!==amount(result.gross))return finish('indeterminate',result.reason);
if(row.deductionLink.scopeConfirmed!=='yes'||!row.deductionLink.allocationRef.trim())return finish('indeterminate','Confirme a afectação exclusiva à operação ligada e indique o motivo/referência; não inferida da oportunidade.');
if(row.domestic!=='yes'||row.allocation==='mixed'||row.exclusion==='special')return finish('indeterminate','Importação, afectação parcial ou despesa especial: enquadramento específico B/C/D ainda necessário; sem dedução integral presumida.');
if(right.status==='indeterminate')return finish('indeterminate',right.reason);
if(right.status==='no_right')return finish('no_right',right.reason,'0.00');
if(row.allocation==='no_right'||row.exclusion==='excluded'||row.supplierRegistered==='no'||row.legalDocument==='no'||row.integrity==='no')return finish('no_right',result.reason,'0.00');
if(row.deductionLink.noExcludedPayments==='no')return finish('no_right','Afectação a pagamentos do artigo15(6)(c): exclusão19(2).','0.00');
if(row.deductionLink.noExcludedPayments!=='yes')return finish('indeterminate','Indique se a aquisição está fora dos pagamentos excluídos15(6)(c)/19(2).');
if(result.deductible===null)return finish('indeterminate',result.reason);
return finish('eligible',right.reason+' '+result.reason,result.deductible);
}
function sourceKey(draft:Record<string,unknown>,book:AcquisitionBook):string {
const raw=JSON.stringify([acquisitionScope(draft),draft.currency??'',draft.province_id??'',draft.district_id??'',draft.business_plan_snapshot,draft.monthly_iva_settings,draft.monthly_iva_snapshot,book.currency,book.taxpayerRegistered,book.rows,book.periods]);
// Change detection only, never an authorization or authenticity token.
let hash=14695981039346656037n;for(let i=0;i<raw.length;i++)hash=BigInt.asUintN(64,(hash^BigInt(raw.charCodeAt(i)))*1099511628211n);
const operations=(readAcquisitionStore(draft.modelo_a_snapshot) as Store & {salesOperations?:Record<string,unknown>}).salesOperations?.[acquisitionScope(draft)];
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}${book.rows.some(r=>r.acquisitionTreatment||r.acquisitionTreatmentOrigin)?`:${acquisitionContinuityVersion}`:''}`;
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}${book.rows.some(r=>r.acquisitionTreatment||r.acquisitionTreatmentOrigin)?`:${acquisitionContinuityVersion}`:''}${book.rightVersion||book.rows.some(r=>r.deductionLink)?`:${acquisitionRightVersion}`:''}`;
}
function location(draft:Record<string,unknown>):string {
const labels=draft.selection_labels as Record<string,unknown>|undefined;
@@ -133,7 +153,15 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
const current=references.find(p=>p.month===row.sourceMonth && p.kind===(row.sourceField??'inventory'));
if(!current || current.amount!==row.sourceAmount || (current.kind!=='asset' && row.sourcePeriod && current.period!==row.sourcePeriod)) throw new Error('As compras ou orçamentos do plano mudaram. Resolva a divergência de origem antes de calcular; histórico preservado.');
}
const rows=book.rows.map(row=>evaluateAcquisition(row,book.currency,book.taxpayerRegistered));
if(book.rightVersion||book.rows.some(r=>r.deductionLink)){
const identities=book.rows.filter(r=>r.documentRef.trim()&&r.supplier.trim()).map(r=>JSON.stringify([r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine.trim()]));
if(new Set(identities).size!==identities.length)throw new Error('Documento/linha repetido entre projecção e efectivo; sem dupla dedução.');
}
const rows=book.rows.map(row=>{
const result=evaluateAcquisition(row,book.currency,book.taxpayerRegistered,draft);
if(!row.deductionLink&&(book.rightVersion||book.rows.some(r=>r.deductionLink)))return {...result,deductible:null,reason:'Aquisição sem vínculo à operação de destino; direito indeterminado.',eligibility:{status:'indeterminate' as const,reason:'Aquisição sem vínculo à operação de destino; direito indeterminado.',source:acquisitionRightSource}};
return result;
});
const record=readMonthlyIvaRecord(String(draft.monthly_iva_snapshot ?? ''));
let salesCurrent=false;
try {salesCurrent=!!record && record.execution.status==='completed' && monthlyIvaKey(record.input)===monthlyIvaKey(monthlyIvaInput(draft,readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''))));} catch { /* Preserve history, never fabricate 01/02. */ }
@@ -146,7 +174,7 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
if(input?.absent.includes('05') && (declaredAmount('exports')||declaredAmount('sales_zero_rate'))) throw new Error('Existem exportações ou vendas de outro enquadramento. Não confirme ausência no campo05 sem resolver a classificação.');
const sales=salesCurrent?projection?.rows?.find(r=>r.date.slice(0,7)===period):null;
fields['01']=sales?.net_sales??null;fields['02']=sales?.vat??null;
if(!sales || sales.net_sales===null || sales.vat===null) reasons.push('01/02: vendas ou IVA mensal em falta, execução incompleta ou desactualizada.');
if((!sales || sales.net_sales===null || sales.vat===null)&&!(book.rows.some(r=>r.deductionLink)&&salesOperationsCurrent(draft,salesBook(draft))&&salesBook(draft).result?.periods.some(p=>p.period===period))) reasons.push('01/02: vendas ou IVA mensal em falta, execução incompleta ou desactualizada.');
for(const field of excluded) fields[field]=input?.absent.includes(field)?'0.00':null;
const operations=salesBook(draft);
if(operations.rows.length || operations.result) {
@@ -158,7 +186,7 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
for(const field of ['08','09','10']) {
const matching=book.rows.filter(r=>r.period===period && r.field===field);
fields[field]=input?.complete ? sum(matching.map(r=>rows.find(x=>x.id===r.id)!.deductible)) : null;
if((operations.rows.length || operations.result) && operations.rows.some(r=>r.period===period && !['general','export_goods'].includes(r.treatment)) && matching.some(r=>rows.find(x=>x.id===r.id)!.deductible!=='0.00')) {
if((operations.rows.length || operations.result) && operations.rows.some(r=>r.period===period && !['general','export_goods'].includes(r.treatment)) && matching.some(r=>!r.deductionLink&&rows.find(x=>x.id===r.id)!.deductible!=='0.00')) {
fields[field]=null;
reasons.push(`Campo${field}: operações reduzidas/isentas/outras exigem afectar cada aquisição; não reutilizar dedução exclusiva geral como se fosse universal (L2).`);
}
@@ -200,8 +228,9 @@ export function acquisitionReportRows(draft:Record<string,unknown>):Array<[strin
const out:Array<[string,string,string,string]>=[['IVA mensal','Apuramento parcial Modelo A',acquisitionCurrent(draft,book)?'Versão correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Fundamento das aquisições',result.basis,result.sources]];
for(const row of result.inputs) {
const r=result.rows.find(x=>x.id===row.id)!;
if(r.eligibility)out.push(['IVA mensal',`Direito da aquisição ${row.id}`,`${{eligible:'Elegível',no_right:'Sem direito',indeterminate:'Indeterminado'}[r.eligibility.status]}: ${r.eligibility.reason}`,`${r.eligibility.source}; operação ${r.eligibility.operationId??'por identificar'}; ${r.eligibility.operationPeriod??''}; ${r.eligibility.operationArticle??''}; afectação ${row.deductionLink?.allocationRef??''}`]);
const source=row.sourceMonth===null?'Introduzido pelo utilizador':`Plano (${row.sourceField==='asset'?'orçamento de activos':row.sourceField==='operating'?'orçamento operacional':'compras/encomendas'}), mês ${row.sourceMonth}, ${row.sourcePeriod??'período de origem não indicado'}, ${row.sourceAmount??'valor em falta'} ${row.sourceCurrency}; ${row.sourceBasis==='net'?'sem IVA':row.sourceBasis==='gross'?'com IVA':'base por classificar'}`;
const allocation={taxable_only:'exclusiva a operações tributadas',mixed:'mista',no_right:'sem direito à dedução','':'por indicar'}[row.allocation];
const allocation={taxable_only:row.deductionLink?'exclusiva à operação ligada':'exclusiva a operações tributadas',mixed:'mista',no_right:'sem direito à dedução','':'por indicar'}[row.allocation];
out.push(['IVA mensal',`Aquisição ${row.id} — campo${row.field} — ${row.period}`,`${row.description}; líquido ${r.net??'Indeterminado'}; IVA ${r.vat??'Indeterminado'}; bruto ${r.gross??'Indeterminado'}; dedutível ${r.deductible??'Indeterminado'}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${source}; ${row.documentType==='invoice'?'Factura':row.documentType==='equivalent'?'Documento equivalente':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]);
if(row.acquisitionTreatment==='former_exploration_exemption'||row.acquisitionTreatmentOrigin)out.push(['IVA mensal',`Continuidade14(2)(c) — ${row.id}`,row.acquisitionTreatment==='former_exploration_exemption'?'Antiga isenção eliminada; dedução indeterminada por esta referência':'Continuidade normal expressamente escolhida; montantes e documento conservados; condições de dedução da aquisição, sem aplicar antiga isenção',`${context}; ${formerExplorationSource}; origem conservada ${row.acquisitionTreatmentOrigin?.id??'former_exploration_exemption'}`]);
}
@@ -0,0 +1,38 @@
<?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 IvaAcquisitionRightDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_right_recovers_with_sources_totals_history_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()]));
$draft = json_decode(file_get_contents(base_path('outputs/iva-l2-a-20261025/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']);
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$book = $store['books']['opportunity:368'];
$this->assertCount(5, $book['rows']);
$this->assertSame(['eligible','eligible','eligible','no_right','no_right'], array_column(array_column($book['result']['rows'], 'eligibility'), 'status'));
$this->assertSame('48.00', $book['result']['periods'][0]['fields']['15']);
$this->assertCount(1, $book['history']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l2a-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
foreach (json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR) as $key => $map) {
if ($key === 'version') continue;
foreach ($map as $context => $original) $this->assertSame($original, $store[$key][$context]);
}
file_put_contents(base_path('outputs/iva-l2-a-20261025/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,54 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newSalesOperation,recordSales,saveSalesBook,salesBook,operationTreatments} from '../../resources/js/lib/iva-operations.ts';
import {newAcquisition,evaluateAcquisition,recordAcquisitions,saveAcquisitionBook,acquisitionBook,acquisitionCurrent,acquisitionReportRows,readAcquisitionStore} from '../../resources/js/lib/iva-acquisitions.ts';
import {createAcquisitionRightLink,linkedOperationRight} from '../../resources/js/lib/iva-acquisition-right.ts';
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l2a-preserved.json',import.meta.url),'utf8'));
export function rightFixture(){
const draft={...structuredClone(frozen),context_opportunity:'opportunity:368',project_title:'SYNTHETIC QA — L2-A aquisição e direito'};
const types=['general','export_goods','listed_maize','housing_rent','private_health'];
const operations={currency:'MZN',periods:[{period:'2026-12',complete:true}],rows:types.map((t,i)=>({...newSalesOperation('operation-'+i,'2026-12','MZN'),description:'SYNTHETIC QA '+t,amount:'100',basis:'net',establishment:'Mozambique',treatment:t,confirmed:operationTreatments.find(x=>x.id===t).conditions.map((_,j)=>j),evidence:'SYNTHETIC QA fonte e condições'}))};
draft.modelo_a_snapshot=saveSalesBook(draft,recordSales(draft,operations));
const rows=types.map((_,i)=>({...newAcquisition('purchase-'+i,'2026-12','MZN'),net:'100',vat:'16',gross:'116',description:'SYNTHETIC QA compra '+i,documentType:'invoice',documentRef:'SYNTHETIC QA '+i,documentDate:'2026-12-10',supplier:'SYNTHETIC QA fornecedor',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes',deductionLink:{...createAcquisitionRightLink(draft,'operation-'+i),scopeConfirmed:'yes',allocationRef:'Afectação exclusiva SYNTHETIC QA '+i,noExcludedPayments:'yes'}}));
return {draft,book:{currency:'MZN',taxpayerRegistered:'yes',rows,periods:[{period:'2026-12',complete:true,absent:['11','12','13']}]}};
}
test('right follows classified operation, including exempt with right and taxed17-A without right',()=>{
const{draft,book}=rightFixture();
const results=book.rows.map(r=>evaluateAcquisition(r,'MZN','yes',draft));
assert.deepEqual(results.map(r=>r.eligibility.status),['eligible','eligible','eligible','no_right','no_right']);
assert.deepEqual(results.map(r=>r.deductible),['16.00','16.00','16.00','0.00','0.00']);
for(const r of results){assert.ok(r.eligibility.reason);assert.match(r.eligibility.source,/Lei10\/2025/);}
assert.match(results[4].reason,/17-A/);
});
test('own documentary conditions, missing data, imports and mixed use remain distinct from operation right',()=>{
const{draft,book}=rightFixture(),row=book.rows[0];
for(const patch of [{held:''},{documentRef:''},{documentDate:'2026-11-10'},{allocation:'mixed'},{domestic:'no'},{exclusion:'special'},{sourceCurrency:'USD'},{vat:'0',gross:'100',held:''},{sourceMonth:1,sourceAmount:'999',sourceBasis:'net'}]){
const r=evaluateAcquisition({...row,...patch},'MZN','yes',draft);assert.equal(r.eligibility.status,'indeterminate',JSON.stringify(patch));assert.equal(r.deductible,null);
}
for(const patch of [{exclusion:'excluded'},{supplierRegistered:'no'},{integrity:'no'}])assert.equal(evaluateAcquisition({...row,...patch},'MZN','yes',draft).eligibility.status,'no_right');
assert.equal(evaluateAcquisition({...row,deductionLink:{...row.deductionLink,scopeConfirmed:''}},'MZN','yes',draft).deductible,null);
assert.equal(evaluateAcquisition({...row,deductionLink:{...row.deductionLink,noExcludedPayments:'no'}},'MZN','yes',draft).deductible,'0.00');
assert.equal(evaluateAcquisition(row,'MZN','yes').deductible,null);
});
test('totals F include only eligible amounts; unknown never becomes zero; no duplicated projected/effective invoice',()=>{
const{draft,book}=rightFixture();const result=recordAcquisitions(draft,book).result;
assert.equal(result.periods[0].fields['09'],'48.00');assert.equal(result.periods[0].fields['15'],'48.00');
assert.deepEqual(['14','15','16','17','18'].map(f=>result.periods[0].fields[f]),['500.00','48.00','21.00','0.00','27.00']);
const cleared=structuredClone(book);cleared.rightVersion='acquisition-operation-right/1';cleared.rows=cleared.rows.map(({deductionLink,...row})=>row);const empty=recordAcquisitions(draft,cleared).result;assert.ok(empty.rows.every(r=>r.eligibility.status==='indeterminate'));assert.equal(empty.periods[0].fields['15'],null);
const unknown=structuredClone(book);unknown.rows[0].held='';assert.equal(recordAcquisitions(draft,unknown).result.periods[0].fields['15'],null);
const duplicate=structuredClone(book);duplicate.rows.push({...duplicate.rows[0],id:'copy',origin:'effective'});assert.throws(()=>recordAcquisitions(draft,duplicate),/dupla dedução/);
});
test('source edits require explicit adoption; save recovery report and historical versions preserve old books byte for byte',()=>{
let{draft,book}=rightFixture();const before=JSON.parse(frozen.modelo_a_snapshot);book=recordAcquisitions(draft,book);draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);
const first=structuredClone(book.result);book=recordAcquisitions(draft,book);draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);
const recovered=JSON.parse(JSON.stringify(draft));assert.deepEqual(acquisitionBook(recovered),book);assert.ok(acquisitionCurrent(recovered,book));assert.deepEqual(book.history,[first]);
const after=JSON.parse(draft.modelo_a_snapshot);for(const key of Object.keys(before)){if(key==='version')continue;for(const [id,value]of Object.entries(before[key]))assert.deepEqual(after[key][id],value,key+id);}
const report=JSON.stringify(acquisitionReportRows(recovered));for(const word of ['Elegível','Sem direito','Lei10/2025','48.00','operation-4'])assert.ok(report.includes(word),word);
const ops=salesBook(draft);ops.rows[0].description='Alteração deliberada';draft.modelo_a_snapshot=saveSalesBook(draft,recordSales(draft,ops));assert.equal(acquisitionCurrent(draft,book),false);
assert.equal(linkedOperationRight(draft,book.rows[0].deductionLink).status,'indeterminate');assert.deepEqual(book.history[0],first);assert.deepEqual(book.result,recovered?acquisitionBook(recovered).result:null);
const adopted=createAcquisitionRightLink(draft,book.rows[0].deductionLink.operationId);assert.equal(adopted.scopeConfirmed,'');assert.equal(adopted.allocationRef,'');assert.equal(linkedOperationRight(draft,adopted).status,'eligible');assert.equal(evaluateAcquisition({...book.rows[0],deductionLink:adopted},'MZN','yes',draft).deductible,null);
assert.equal(linkedOperationRight({...draft,context_opportunity:'opportunity:329'},adopted).status,'indeterminate');
const malformed=JSON.parse(draft.modelo_a_snapshot);malformed.books['opportunity:368'].rows[0].deductionLink.version=9;assert.throws(()=>readAcquisitionStore(JSON.stringify(malformed)),/Vínculo/);
if(process.env.IVA_L2A_CALCULATED)fs.writeFileSync(process.env.IVA_L2A_CALCULATED,JSON.stringify(recovered));
});