Implement L2 D allocation and provisional proportions with exact totals and frozen history

This commit is contained in:
2026-10-25 13:00:59 +02:00
parent 855ab066c4
commit 2c2a2f2010
12 changed files with 435 additions and 20 deletions
@@ -25525,5 +25525,75 @@
"next": "D:affectation partial/proportions;E by effective dependencies",
"L1": "operacional com limitações explícitas;129entregues+P058/P084/P085+P001=133;sem reabertura",
"beverage_refund": "L3,not delivered here"
},
"l2_d_allocation_qa_20261025": {
"status": "L2-D concluído em QA no âmbito demonstrado;F acompanha;sem instalação deD",
"report": "docs/investmoz/simulations/l2-d-allocation-qa-20261025.json",
"installed": "819ae791",
"L1": {
"delivered": 129,
"functional_residuals": [
"P058",
"P084",
"P085"
],
"documentary_residual": "P001 — exclusivamente05/06",
"state": "operacional com limitações explícitas;sem reabertura"
},
"F": {
"example_MZN": {
"09": "13.44",
"11": "10.72",
"14": "1000.00",
"15": "24.16",
"16": "160.00",
"17": "135.84",
"18": "0.00"
},
"exact_composition": "16×1/3=16/3;gasóleo16×50%×1/3=8/3;pro rata1/3→34%,2/3→67%;somar fracções exactas antes da apresentação decimal.",
"incomplete_parameters": "Denominador ausente/zero,numerador maior que denominador,factos/fontes/ano por esclarecer→indeterminado,não0/100% presumidos.",
"output_limit": "Existindo bases reduzidas no mesmo âmbito/período,IVA liquidado ainda não consolidado impede totais14/16/17/18 completos,mesmo se não são a origem directa da aquisição seleccionada. Dedução15 conserva apuramento próprio. Adicionar/alterar essas origens invalida a versão actual do apuramentoD.",
"specific_example_MZN": {
"15": "6.40",
"14": null,
"16": null,
"17": null,
"18": null
},
"L3": "19–27,créditos anteriores,pagamento/reembolso e restituição de bebidas fora deste incremento."
},
"validation": {
"frontend": "26testes dirigidos no conjunto;21anteriores deABC/compras/continuidade reutilizados e5novosD. Os5D repetidos após os ajustes finais de fonte/guardaF:todos passaram.",
"api": "1teste/89asserções;current_database=simulator_fiscal_delta_20261021_test,port55441;DatabaseTransactions rollback. Gravação/recuperação conserva fracções,factos,método,referências,histórico e todos os mapas anterioresABC/L1.",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"ui": "Componente real;denominador0→totais indeterminados;reutilização de origem18(6);dois âmbitos sintéticos separados,gravar→recuperar→relatório.",
"proofs": [
"outputs/iva-l2-d-20261025/ui-recovered-totals.png",
"outputs/iva-l2-d-20261025/ui-reduced-output-limit.png"
],
"preservation": "129linhasL1 entregues e quatro residuais comparados sem alterações;livrosABC comparados integralmente e mantidos actuais."
},
"scope_limits": [
"Combinação de18(6)com afectação mista não demonstrada:não aplicar produto genérico de percentagens.",
"Bases reduzidas mantêm limitação de IVA liquidado/fecho integral dos totais explicitamente identificada.",
"Excepções ministeriais específicas22(9),ou condições/imposições concretas não identificadas,não são inventadas nem activadas genericamente.",
"Pro rata é provisório;regularização anual22(6) depende do resultado histórico e dos valores anuais efectivos emE.",
"Activos/outros motivos de regularização não são concluídos por este incrementoD."
],
"E_next_by_dependency": {
"first_executable": "Acerto anual22(6) do pro rata provisório:seleccionar resultadoD congelado,ano/âmbito e parcela original;registar bases anuais finais22(4–5)/quociente22(8);calcular diferença face ao efectivamente deduzido,no último período do ano,com evento único e sentidos12/13 separados. Não substituir o histórico.",
"reused_from_D": [
"Identidade da aquisição/documento/linha",
"Moeda/período/âmbito",
"IVA elegível antesD (já limitado porC)",
"Quociente original e aplicado",
"Deduções exactas e não dedutível",
"Revisão,data/fontes e resultados congelados"
],
"other_motives": "Activos e restantes regularizações seguem documento/evento/prazo e fonte específicos;não recebem regra genérica de percentagem anual. A divergência textual de remissões11/12 versus caixas12/13 permanece identificada separadamente deP001.",
"code_status": "E ainda não implementado;dependênciaD agora disponível emQA. Isto não declaraE entregue nem fechaL2."
},
"active_writes": false
}
}
@@ -0,0 +1,101 @@
{
"status": "L2-D concluído em QA no âmbito demonstrado;F acompanha;sem instalação deD",
"matrix_anchor": "AUD-SIM-INT02",
"installed_revision": "819ae791",
"installed_receipt": "855ab066",
"L1": {
"delivered": 129,
"functional_residuals": [
"P058",
"P084",
"P085"
],
"documentary_residual": "P001 — exclusivamente05/06",
"state": "operacional com limitações explícitas;sem reabertura"
},
"implemented": {
"actual": "22(2):numerador/denominador,mesma unidade/critério,âmbito e fonte;comunicação prévia referenciada e condições concretamente aplicáveis. Sem aprovador novo. Fracção real exacta,sem arredondamento de pro rata.",
"pro_rata": "22(1)/(4)/(5)/(6)/(7)/(8):bases anuais semIVA,numerador com direito18/19,denominador de todas operações incluindo fora de campo;exclui vendas de activo usado e operações imobiliárias/financeiras acessórias. Provisório do ano anterior ou estimativa expressa de início/alteração substancial com referência da declaração. Quociente arredondado à centésima superior;original e aplicado conservados. Imposição concreta de afectação real22(3) não é substituída por pro rata.",
"specific_18_6": "Reutiliza origem actual e demonstradaP096/P097/P098/P099,no mesmo âmbito por oportunidade,com proporções62/85/40/75%. Aquisição exclusivamente afecta à origem;alteração/invalidação da origem pede adopção expressa,sem recalcular histórico. Sem segunda redução da base e sem taxaIVA inferida.",
"conditions_ABC": "Condições própriasA/B/C permanecem. ExclusãoC não é afastada porD;combustível50% compõe-se uma única vez com a afectaçãoD. Documento aduaneiro mantém titularidade,IVA próprio,exigibilidade/posse e cobertura11,sem fornecedor interno.",
"reuse": "Reutiliza livro/documento,valores,moeda,mês,condições e referências existentes;19(2) já declarado no vínculo pode ser reutilizado. Campos anuais/critério/comunicação apenas onde necessários;vínculoA conservado,sem o reinterpretar como âmbito misto. Mudanças deliberadas/históricos preservados."
},
"F": {
"example_MZN": {
"09": "13.44",
"11": "10.72",
"14": "1000.00",
"15": "24.16",
"16": "160.00",
"17": "135.84",
"18": "0.00"
},
"exact_composition": "16×1/3=16/3;gasóleo16×50%×1/3=8/3;pro rata1/3→34%,2/3→67%;somar fracções exactas antes da apresentação decimal.",
"incomplete_parameters": "Denominador ausente/zero,numerador maior que denominador,factos/fontes/ano por esclarecer→indeterminado,não0/100% presumidos.",
"output_limit": "Existindo bases reduzidas no mesmo âmbito/período,IVA liquidado ainda não consolidado impede totais14/16/17/18 completos,mesmo se não são a origem directa da aquisição seleccionada. Dedução15 conserva apuramento próprio. Adicionar/alterar essas origens invalida a versão actual do apuramentoD.",
"specific_example_MZN": {
"15": "6.40",
"14": null,
"16": null,
"17": null,
"18": null
},
"L3": "19–27,créditos anteriores,pagamento/reembolso e restituição de bebidas fora deste incremento."
},
"validation": {
"frontend": "26testes dirigidos no conjunto;21anteriores deABC/compras/continuidade reutilizados e5novosD. Os5D repetidos após os ajustes finais de fonte/guardaF:todos passaram.",
"api": "1teste/89asserções;current_database=simulator_fiscal_delta_20261021_test,port55441;DatabaseTransactions rollback. Gravação/recuperação conserva fracções,factos,método,referências,histórico e todos os mapas anterioresABC/L1.",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"ui": "Componente real;denominador0→totais indeterminados;reutilização de origem18(6);dois âmbitos sintéticos separados,gravar→recuperar→relatório.",
"proofs": [
"outputs/iva-l2-d-20261025/ui-recovered-totals.png",
"outputs/iva-l2-d-20261025/ui-reduced-output-limit.png"
],
"preservation": "129linhasL1 entregues e quatro residuais comparados sem alterações;livrosABC comparados integralmente e mantidos actuais."
},
"scope_limits": [
"Combinação de18(6)com afectação mista não demonstrada:não aplicar produto genérico de percentagens.",
"Bases reduzidas mantêm limitação de IVA liquidado/fecho integral dos totais explicitamente identificada.",
"Excepções ministeriais específicas22(9),ou condições/imposições concretas não identificadas,não são inventadas nem activadas genericamente.",
"Pro rata é provisório;regularização anual22(6) depende do resultado histórico e dos valores anuais efectivos emE.",
"Activos/outros motivos de regularização não são concluídos por este incrementoD."
],
"E_next_by_dependency": {
"first_executable": "Acerto anual22(6) do pro rata provisório:seleccionar resultadoD congelado,ano/âmbito e parcela original;registar bases anuais finais22(4–5)/quociente22(8);calcular diferença face ao efectivamente deduzido,no último período do ano,com evento único e sentidos12/13 separados. Não substituir o histórico.",
"reused_from_D": [
"Identidade da aquisição/documento/linha",
"Moeda/período/âmbito",
"IVA elegível antesD (já limitado porC)",
"Quociente original e aplicado",
"Deduções exactas e não dedutível",
"Revisão,data/fontes e resultados congelados"
],
"other_motives": "Activos e restantes regularizações seguem documento/evento/prazo e fonte específicos;não recebem regra genérica de percentagem anual. A divergência textual de remissões11/12 versus caixas12/13 permanece identificada separadamente deP001.",
"code_status": "E ainda não implementado;dependênciaD agora disponível emQA. Isto não declaraE entregue nem fechaL2."
},
"schema_changes": false,
"active_writes": false,
"runtime_delta": [
{
"path": "resources/js/lib/iva-acquisitions.ts",
"sha256_LF": "7933ca0a080af2b8ea3a8ee02f75dd4151a3452da161911960c877f5c404a2e1"
},
{
"path": "resources/js/lib/iva-acquisition-bc.ts",
"sha256_LF": "4ce47274449e075e5e22a02f7bc497e4df6925c1d628f1e9cd057748d016012f"
},
{
"path": "resources/js/lib/iva-acquisition-allocation.ts",
"sha256_LF": "df960bab63e89c3b3cea3c61b5c57481194b19d5dd0894830a181cdbe10dc972"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "c11971ff255db1faced4c5cd0f48587f66565a2c4d58f7e786bb8e01214be9e2"
},
{
"path": "resources/js/components/acquisition-allocation.tsx",
"sha256_LF": "222204ee4c699b2cbddb9cb921a37397f6be313a215905163ee135aa2b26f104"
}
]
}
@@ -1,5 +1,29 @@
# Rastreabilidade, utilização e formação
## 25/10/2026 — L2-D emQA,comF e dependências deE
ABC/F entregue819ae791,recibo855ab066.D concluído emQA no âmbito demonstrado,sem instalação nova.L1 operacional com limitações:129entregues+P058/P084/P085+P001=133;nenhuma linhaL1 alterada.LivrosABC e históricos preservados,actuais e comparados integralmente.
Na aquisição existente,use «Preparar afectação parcial e proporções — D».Conserva documento,montantes,moeda,mês e condiçõesB/C.Escolher afectação real/pro rata identifica afectação mista;18(6),afectação exclusiva à origem. O vínculo individualA conserva-se,sem definir silenciosamente o âmbito misto.A falta de condiçãoC/documento não é suprida por uma proporçãoD.
**Afectação real22(2):** indicar parte com direito/total na mesma unidade e critério,âmbito/fonte,repartição real,comunicação prévia referenciada e condições concretamente aplicáveis.Não existe novo aprovador do portal.Usa fracção exacta,sem arredondamento do pro rata.
**Pro rata provisório22:** bases anuais semIVA,numerador com direito18/19,denominador com todas as operações incluindo fora de campo;excluir vendas de activo usado e imobiliárias/financeiras acessórias22(4–5).Ano anterior22(6),ou estimativa expressa de início/alteração substancial22(7) com referência da declaração.Não usar só vendas de um mês/opção individual como total anual.O quociente é arredondado à centésima superior22(8),conservando original e aplicado:1/3→34%,2/3→67%.Imposição concreta de afectação real22(3) não é substituída por pro rata.Montantes anuais em falta/denominadorzero não recebem percentagem presumida.
**18(6):** reutilizar origem demonstradaP096–P099 no mesmo âmbito,adoptando expressamente alterações e confirmando correspondência.Reutiliza62/85/40/75%,sem taxaIVA presumida ou segunda redução da base.Afectação mista combinada com18(6) não está demonstrada neste percurso;fica indeterminada.Condições/fonte de origem alteradas invalidam o resultado actual;históricos não são recalculados.
**F:** preservar parcela dedutível/não dedutível e composiçãoC antesD.Gasóleo16×50%×1/3=8/3,sem dupla aplicação.Exemplo de afectação real/pro rata:09=13,44;11=10,72;14=1000,00;15=24,16;16=160,00;17=135,84;18=0,00MZN.Quando existem bases reduzidas no mesmo âmbito/período,IVA liquidado não consolidado mantém14/16/17/18 indeterminados,mesmo sem vínculo directo da compra a essas bases.Exemplo18(6):dedução15=6,40,sem imposto de saída presumido0. Não se conclui cálculo fiscal integral.L3 mantém créditos anteriores,restituição de bebidas,pagamento/reembolso.
Guardar versão,gravar privadamente,recuperar e abrir relatório conserva método,bases,quocientes,fonte,comunicação,fracções exactas e histórico. Alterar entradas/origens assinala resultado desactualizado,sem substituir o original.ProvaAPI:1teste/89asserções,baseQA55441 com guarda e rollback;26testes dirigidos,5D repetidos após ajustes finais;tipos/build e componente real com dois âmbitos recuperados.ProvasABC/L1 válidas reutilizadas.
**E,por dependência efectiva:** primeiro motivo executável é o acerto anual22(6),usando resultadoD congelado,IVA elegível antesD,quociente aplicado e montante efectivamente deduzido. Recolher bases anuais finais e aplicar22(4–5)/(8),calcular diferença no último período do ano,com identidade única do evento e sentidos12/13 separados;jamais alterar dedução histórica.Activos/outros motivos precisam evento,documento,prazo e fonte específicos.A divergência textual11/12 versus caixas12/13 continua identificada separadamente deP001.E ainda não implementado;este registo não fechaE ouL2.
Relatório:docs/investmoz/simulations/l2-d-allocation-qa-20261025.json.Provas:outputs/iva-l2-d-20261025/ui-recovered-totals.png eui-reduced-output-limit.png.
## 25/10/2026 — inventário anterior à implementação/testesD
ABC/F instalado819ae791,recibo855ab066.L1129fechos/quatroresiduais preservados,sem perdas confirmadas. D reutiliza livro de compras,condiçõesB/C,fracções exactas e bases reduzidasP096–P099. Cotejo dirigido22(1–8),Lei13/2016PDF16/p1426(274),e18(6),Lei10/2025PDF13;sem reconstruir motor. Comunicação prévia22(2) é condição da fonte,sem novo aprovador. Regularização anual22(6) segueE.
## 25/10/2026 — entrega conjunta L2-A/B/C comF
Instalado819ae791,candidatafc7e590d,incluiA56c948ec. Uma integração pelo processoGit,backup novo e restauroDocker real isolado semrede antes da instalação;sem migrações/importações. Delta instalado:6ficheiros de execução,3originais fiscais,10marcadores compilados,manifesto/revisão e simuladorHTTP200;dados/tabelas/ficheiros persistentes/configuração comparados com identidadepostgres equivalente. Apenascache/sessions podem variar.ProvasQA aceites reutilizadas,sem repetirL1. L1 operacional com limitações:129entregues+P058/P084/P085+P001=133. Via de dedução de bebidas no recorte demonstrado;restituição continuaL3. PróximoD:afectação parcial/proporções;E pelas dependências efectivas.F acompanha cada incremento.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
import type {Acquisition} from '@/lib/iva-acquisitions';
import {newAcquisitionBC} from '@/lib/iva-acquisition-bc';
import {newAcquisitionAllocation,allocationReducedChoices,adoptAllocationReduced,acquisitionAllocationDecision,type AcquisitionAllocation} from '@/lib/iva-acquisition-allocation';
export function AcquisitionAllocationEditor({draft,row,onChange}:{draft:Record<string,unknown>;row:Acquisition;onChange:(patch:Partial<Acquisition>)=>void}){
const d=row.d;
if(!d)return <button type="button" onClick={()=>onChange({d:{...newAcquisitionAllocation(row.period),noExcludedPayments:row.deductionLink?.noExcludedPayments??''},bc:row.bc??newAcquisitionBC()})}>Preparar afectação parcial e proporções — D (conserva dados e histórico)</button>;
const update=(value:Partial<AcquisitionAllocation>)=>onChange({d:{...d,...value}});
const fact=(key:string,label:string)=><label className="block" key={key}>{label}<select value={d.facts[key]??''} onChange={e=>update({facts:{...d.facts,[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>;
const text=(key:'numerator'|'denominator'|'unit'|'referenceYear'|'scopeRef'|'source'|'communicationRef'|'declarationRef',label:string)=><label className="block" key={key}>{label}<input value={d[key]} onChange={e=>update({[key]:e.target.value})}/></label>;
const decision=acquisitionAllocationDecision(draft,row),choices=d.method==='reduced'?allocationReducedChoices(draft):[];
return <fieldset className="space-y-2 rounded border p-3"><legend>Afectação parcial e proporções — D</legend>
<p>Reutiliza documento,montantes,moeda,período e condiçõesB/C. Escolher afectação real/pro rata identifica esta aquisição como mista;18(6),como exclusiva ao âmbito de origem. O vínculo individual deA permanece conservado,sem definir silenciosamente a afectação mista. A versão anterior não é recalculada.</p>
<label className="block">Método de afectação/dedução<select value={d.method} onChange={e=>{const method=e.target.value as AcquisitionAllocation['method'];onChange({d:{...d,method},...(method?{allocation:method==='reduced'?'taxable_only':'mixed'}:{})});}}><option value="">Por identificar</option><option value="actual">Afectação real22(2)</option><option value="pro_rata">Pro rata provisório22(1)/(4)/(6)/(7)</option><option value="reduced">Proporção específica18(6) — origemP096–P099</option></select></label>
{text('scopeRef','Âmbito da aquisição/actividade:entidade,operações,período e critério aplicáveis')}
{text('source','Fonte/documento ou pressuposto expresso das bases e do método')}
{fact('scopeConfirmed','Esta aquisição corresponde ao âmbito e à fonte identificados;sem usar apenas o sector ou a oportunidade')}
<label className="block">Aquisição fora dos pagamentos excluídos15(6)(c)/19(2)<select value={d.noExcludedPayments} onChange={e=>update({noExcludedPayments:e.target.value as AcquisitionAllocation['noExcludedPayments']})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
{d.method==='reduced'?<><label className="block">Reutilizar base reduzida existente<select value={d.reducedId} onChange={e=>{if(e.target.value)update(adoptAllocationReduced(draft,d,e.target.value));}}><option value="">Escolher origem correspondente</option>{choices.map(c=><option key={c.id} value={c.id}>{c.label}</option>)}</select></label>
{!choices.length&&<p>Não há base reduzida registada nesta oportunidade. Prepare a origem no percurso existente;sem percentagem por defeito.</p>}
{!!d.reducedId&&<button type="button" onClick={()=>update(adoptAllocationReduced(draft,d,d.reducedId))}>Adoptar expressamente a origem actual e rever o âmbito</button>}
<p>Reutiliza62%,85%,40% ou75% segundo a origem demonstrada. São proporções de dedução18(6),não taxasIVA. Não volta a reduzir o montante de aquisição nem duplica o desconto da base.</p>
</>:d.method&&<>{fact('rightBasis','A parte identificada dá direito à dedução18/19,com fundamento na fonte indicada;isenção não implica automaticamente direito')}
{text('numerator',d.method==='actual'?'Parte efectivamente afecta a operações com direito — numerador':'Montante anual semIVA das operações com direito18/19 — numerador')}
{text('denominator',d.method==='actual'?'Utilização total no mesmo critério/unidade — denominador':'Montante anual semIVA de todas as operações,incluindo fora de campo — denominador')}
{d.method==='actual'?<>{text('unit','Unidade e critério de afectação real (mesma base em ambas as parcelas)')}{fact('actualScope','Repartição corresponde à afectação real desta aquisição/parte,documentada no critério identificado')}{fact('communication','Comunicação prévia à Direcção Geral de Impostos22(2),conforme a fonte — não é nova aprovação do portal')}{text('communicationRef','Referência da comunicação prévia22(2)')}{fact('conditionsMet','Condições especiais concretamente aplicáveis ao método,quando existentes,estão reflectidas na fonte e cumpridas')}</>:<>
<label className="block">Base do pro rata provisório<select value={d.basis} onChange={e=>update({basis:e.target.value as AcquisitionAllocation['basis']})}><option value="">Por indicar</option><option value="prior_year">Operações do ano anterior22(6)</option><option value="initial_changed">Estimativa expressa por início/alteração substancial22(7)</option></select></label>
{text('referenceYear','Ano das bases anuais utilizadas')}
{fact('annualScope','Totais cobrem o âmbito anual do sujeito passivo identificado,sem substituir pelo mês ou apenas por uma oportunidade')}
{fact('netExclusions','Bases semIVA22(4–5):incluem operações fora de campo;excluem vendas de activo usado e imobiliárias/financeiras acessórias')}
{d.basis==='initial_changed'&&<>{fact('initialChanged','Há início de actividade ou alteração substancial que fundamenta a estimativa22(7)')}{text('declarationRef','Referência da declaração de início/alterações que contém a estimativa')}</>}
<label className="block">Existe imposição concreta de afectação real22(3) para este âmbito?<select value={d.actualRequired} onChange={e=>update({actualRequired:e.target.value as AcquisitionAllocation['actualRequired']})}><option value="">Nenhuma imposição concreta identificada neste registo</option><option value="yes">Sim — usar método correspondente e fonte</option><option value="no">Não</option></select></label>
<p>O quociente é arredondado à centésima imediatamente superior22(8). Mantêm-se o quociente original e a proporção aplicada;fracções monetárias continuam exactas. O acerto anual22(6) segueE,sem alterar versões anteriores.</p>
</>}</>}
<p role="status">{decision.reason}</p>
<button type="button" onClick={()=>onChange({d:undefined})}>Conservar dados e regressar ao vínculo individual deA (histórico preservado)</button>
</fieldset>;
}
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { AcquisitionImportsExclusions } from './acquisition-imports-exclusions';
import { AcquisitionAllocationEditor } from './acquisition-allocation';
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';
@@ -23,7 +24,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>Aquisições em MZN/2026,ligadas à operação de destino:com direito,sem direito ou indeterminadas. A/B/C distinguem compras internas,importações de bens e exclusões/excepções. Documento e dedução no mesmo mês;afectação mista,proporções18(6),dedução posterior e regularizações conservam percursos próprios. Nas projecções,cada confirmação é um pressuposto. Compras internas:indique dois valores entre líquido,IVA e bruto. Importações:IVA expressamente identificado no bilhete;custos líquido/bruto são opcionais,sem calcular direitos ou presumir taxa.</p>
<p>Aquisições em MZN/2026,ligadas à operação de destino:com direito,sem direito ou indeterminadas. A/B/C/D distinguem compras internas,importações de bens,exclusões/excepções e afectação/proporções. Documento e dedução no mesmo mês;afectação mista,proporções18(6),dedução posterior e regularizações conservam percursos próprios. Nas projecções,cada confirmação é um pressuposto. Compras internas:indique dois valores entre líquido,IVA e bruto. Importações:IVA expressamente identificado no bilhete;custos líquido/bruto são opcionais,sem calcular direitos ou presumir taxa.</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>
@@ -34,7 +35,8 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<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,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)}/>
<AcquisitionAllocationEditor draft={draft} row={row} onChange={patch=>change(row.id,patch)}/>
{!row.d?.method&&<AcquisitionOperationLink draft={draft} row={row} onChange={patch=>change(row.id,patch)}/>}
<AcquisitionImportsExclusions 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>
@@ -61,12 +63,12 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
{!(row.bc&&row.domestic==='no')&&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',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,'allocation','Afectação',[['taxable_only',row.d?.method==='reduced'?'Exclusiva à origem18(6)':row.deductionLink?'Exclusiva à operação ligada':'Exclusiva a operações tributadas'],['mixed',row.d?'Mista / parcial — métodoD':'Mista / parcial — preparar métodoD'],['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)}
</>}
</div>
<p>Fundamento:CIVA18–21,fontes e versão no resultado. {row.bc?'Aplicam-se apenas as exclusões/excepções documentadas escolhidas;gasóleo50% conserva fracção exacta. Sem pro rata,proporções18(6) ou regularização neste incremento.':'Afectação parcial,despesa especial e dedução posterior exigem percurso próprio;use a análiseC quando aplicável.'}</p>
<p>Fundamento:CIVA18–21,fontes e versão no resultado. {row.d?'MétodoD aplicado apenas à parcela elegível pelas condiçõesB/C;fracções exactas,sem regularização anual automática ou restituiçãoL3.':row.bc?'Aplicam-se apenas as exclusões/excepções documentadas escolhidas;gasóleo50% conserva fracção exacta. Sem pro rata,proporções18(6) ou regularização neste incremento.':'Afectação parcial,despesa especial e dedução posterior exigem percurso próprio;use a análiseC quando aplicável.'}</p>
{row.sourceMonth===null && <button type="button" className="mr-3 underline" onClick={()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id)})}>Remover esta aquisição introduzida directamente</button>}
{row.sourceMonth!==null && <button type="button" className="underline" onClick={()=>run(()=>{const ref=acquisitionPlanReferences(draft).find(r=>r.month===row.sourceMonth && r.kind===(row.sourceField??'inventory'));if(!ref)throw new Error('Origem não disponível; conserve o registo e reveja o plano.');if(draft.currency!==row.sourceCurrency)throw new Error('Moeda divergente; não adoptar montantes de outra moeda.');change(row.id,{sourceAmount:ref.amount,sourcePeriod:ref.period,...(row.sourceBasis==='net'?{net:ref.amount??'',gross:''}:row.sourceBasis==='gross'?{gross:ref.amount??'',net:''}:{})});})}>Adoptar explicitamente o valor actual do plano (conserva histórico)</button>}
</details>)}
@@ -0,0 +1,55 @@
import type {Acquisition,AcquisitionResult} from './iva-acquisitions.ts';
import {acquisitionScope} from './iva-acquisitions.ts';
import {evaluateAcquisitionBC} from './iva-acquisition-bc.ts';
import {reducedBaseBook,reducedBaseCurrent,reducedBaseTreatments,evaluateReducedBase} from './iva-reduced-bases.ts';
import {decimalFraction,fraction,fractionLabel,fractionPresentation,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
export const acquisitionAllocationVersion='acquisition-allocation/1';
export const acquisitionAllocationSource='L2-D: CIVA22(1–8),Lei13/2016 PDF16/p1426(274);Lei10/2025,18(6),PDF13/p2180(13).22(4)numerador anual semIVA com direito18/19;denominador anual semIVA de todas operações,incluindo fora de campo;22(5)exclui vendas de activo usado e operações imobiliárias/financeiras acessórias.22(8)quociente à centésima imediatamente superior.22(2)afectação real com comunicação prévia;22(6–7)provisório,regularização anual emE. Não substitui condições18–20 ou calcula restituiçãoL3.';
type Choice=''|'yes'|'no';
export type AcquisitionAllocation={version:1;method:''|'actual'|'pro_rata'|'reduced';numerator:string;denominator:string;unit:string;referenceYear:string;basis:''|'prior_year'|'initial_changed';scopeRef:string;source:string;communicationRef:string;declarationRef:string;actualRequired:Choice;noExcludedPayments:Choice;facts:Record<string,Choice>;reducedId:string;reducedFingerprint:string};
export function newAcquisitionAllocation(period:string):AcquisitionAllocation{return{version:1,method:'',numerator:'',denominator:'',unit:'',referenceYear:String(Number(period.slice(0,4))-1),basis:'',scopeRef:'',source:'',communicationRef:'',declarationRef:'',actualRequired:'',noExcludedPayments:'',facts:{},reducedId:'',reducedFingerprint:''};}
export function validAcquisitionAllocation(v:unknown):v is AcquisitionAllocation{if(!v||typeof v!=='object'||Array.isArray(v))return false;const d=v as AcquisitionAllocation;return d.version===1&&['','actual','pro_rata','reduced'].includes(d.method)&&['','prior_year','initial_changed'].includes(d.basis)&&['numerator','denominator','unit','referenceYear','scopeRef','source','communicationRef','declarationRef','reducedId','reducedFingerprint'].every(k=>typeof d[k as keyof AcquisitionAllocation]==='string')&&[d.actualRequired,d.noExcludedPayments].every(v=>['','yes','no'].includes(v))&&!!d.facts&&typeof d.facts==='object'&&!Array.isArray(d.facts)&&Object.values(d.facts).every(v=>['','yes','no'].includes(v));}
export function allocationReducedChoices(draft:Record<string,unknown>){return reducedBaseBook(draft).rows.map(row=>({id:row.id,label:`${row.description||row.id} — ${row.period} — ${reducedBaseTreatments.find(t=>t.id===row.treatment)?.label??row.treatment}`,row}));}
export function adoptAllocationReduced(draft:Record<string,unknown>,d:AcquisitionAllocation,id:string):AcquisitionAllocation{const source=allocationReducedChoices(draft).find(r=>r.id===id);if(!source)throw Error('Base reduzida de origem indisponível;originais preservados.');return {...d,reducedId:id,reducedFingerprint:JSON.stringify(source.row),facts:{...d.facts,scopeConfirmed:''}};}
export function allocationSourceKey(draft:Record<string,unknown>,rows:Acquisition[]):string{return rows.some(r=>r.d)?JSON.stringify({version:acquisitionAllocationVersion,reducedOperations:reducedBaseBook(draft).rows,sources:rows.filter(r=>r.d?.method==='reduced').map(r=>({row:allocationReducedChoices(draft).find(s=>s.id===r.d?.reducedId)?.row??null,current:reducedBaseCurrent(draft,reducedBaseBook(draft))}))}):'';}
export function allocationHasUnconsolidatedOutputs(draft:Record<string,unknown>,period:string):boolean{return reducedBaseBook(draft).rows.some(r=>r.period===period);}
type Decision={ratio:FiscalFraction|null;rawRatio?:FiscalFraction;reason:string;source:string};
export function acquisitionAllocationDecision(draft:Record<string,unknown>|undefined,row:Acquisition):Decision{
const d=row.d;const stop=(reason:string):Decision=>({ratio:null,reason,source:acquisitionAllocationSource});
if(!d||!validAcquisitionAllocation(d))return stop('Método/parâmetrosD não recuperáveis;sem percentagem por defeito.');
if(!draft||draft.currency!=='MZN'||row.sourceCurrency!=='MZN'||!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('ÂmbitoD2026/MZN ou contexto em falta.');
if(!d.scopeRef.trim()||!d.source.trim()||d.facts.scopeConfirmed!=='yes')return stop('Âmbito da aquisição/actividade,fonte e correspondência por confirmar;sem inferir da oportunidade.');
if(d.method==='reduced'){
if(row.allocation!=='taxable_only')return stop('18(6) neste percurso exige afectação exclusiva à operação de base reduzida;combinação com afectação mista ainda não demonstrada.');
const b=reducedBaseBook(draft),source=b.rows.find(r=>r.id===d.reducedId);if(!source||!reducedBaseCurrent(draft,b)||JSON.stringify(source)!==d.reducedFingerprint)return stop('Origem de base reduzida alterada/incompleta:rever e adoptar expressamente;histórico conservado.');
const result=evaluateReducedBase(source,draft.currency),rule=reducedBaseTreatments.find(r=>r.id===source.treatment);if(!rule||result.status!=='determined')return stop('Condições da base reduzida de origem ainda indeterminadas;não basta escolher percentagem.');
return{ratio:fraction(BigInt(rule.percent),100n),reason:`18(6):proporção específica${rule.percent}% reutilizada de${rule.branch}/${source.id};aplicada uma vez ao IVA elegível,sem converter percentagem de base em taxaIVA.`,source:acquisitionAllocationSource+';'+b.result!.source};
}
if(row.allocation!=='mixed')return stop('Método22 exige afectação mista expressamente identificada;conserve a afectação exclusiva quando aplicável.');
if(d.facts.rightBasis!=='yes')return stop('Identifique a parte/operações com direito18/19;isenção ou sector não estabelecem o direito.');
if(!/^\d{1,15}(\.\d{1,6})?$/.test(d.numerator)||!/^\d{1,15}(\.\d{1,6})?$/.test(d.denominator))return stop('Numerador/denominador em falta ou inválidos;sem percentagem presumida.');
const n=decimalFraction(d.numerator),den=decimalFraction(d.denominator);if(BigInt(den.numerator)===0n)return stop('Denominador zero:não existe quociente aplicável.');
const rawRatio=fraction(BigInt(n.numerator)*BigInt(den.denominator),BigInt(n.denominator)*BigInt(den.numerator));if(BigInt(rawRatio.numerator)>BigInt(rawRatio.denominator))return stop('Numerador excede denominador;resolver bases sem limitar silenciosamente a100%.');
if(d.method==='actual'){
if(!d.unit.trim()||d.facts.actualScope!=='yes'||d.facts.communication!=='yes'||!d.communicationRef.trim()||d.facts.conditionsMet!=='yes')return stop('Afectação real22(2):critério/unidade,repartição efectiva,comunicação prévia e condições concretamente aplicáveis por completar;sem criar aprovação adicional.');
return{ratio:rawRatio,rawRatio,reason:`Afectação real22(2):${d.numerator}/${d.denominator} ${d.unit};critério e comunicação${d.communicationRef}. Fracção real exacta,sem arredondamento de pro rata.`,source:acquisitionAllocationSource};
}
if(d.method!=='pro_rata')return stop('Escolha afectação real,pro rata provisório ou proporção específica18(6).');
if(d.actualRequired==='yes')return stop('Está identificada imposição de afectação real22(3);não aplicar pro rata em substituição.');
if(d.facts.annualScope!=='yes'||d.facts.netExclusions!=='yes')return stop('Confirmar totais anuais semIVA e âmbito22(4–5):inclui fora de campo;exclui vendas de activo usado e imobiliárias/financeiras acessórias. Não usar só vendas de um mês como total anual.');
const year=Number(row.period.slice(0,4));
if(d.basis==='prior_year'){if(d.referenceYear!==String(year-1))return stop('Pro rata provisório22(6) usa operações do ano anterior ao exercício;sem substituir pelo mês corrente.');}
else if(d.basis==='initial_changed'){if(d.referenceYear!==String(year)||d.facts.initialChanged!=='yes'||!d.declarationRef.trim())return stop('Estimativa22(7):início/alteração substancial,ano e referência da declaração de início/alterações por identificar.');}
else return stop('Indique base provisória22(6) ou estimativa expressa22(7);regularização final22(6) segueE.');
const numerator=BigInt(rawRatio.numerator),denominator=BigInt(rawRatio.denominator);const hundredths=(numerator*100n+denominator-1n)/denominator;
return{ratio:fraction(hundredths,100n),rawRatio,reason:`Pro rata provisório22(4/6/7/8):quociente${fractionLabel(rawRatio)}→${hundredths}% (quociente à centésima superior). Base anual${d.referenceYear},${d.basis==='prior_year'?'ano anterior':'estimativa declarada'}. Acerto anual emE,sem recalcular históricos.`,source:acquisitionAllocationSource};
}
export function evaluateAcquisitionAllocation(row:Acquisition,currency:string,registered:Choice,draft:Record<string,unknown>|undefined,raw:AcquisitionResult):AcquisitionResult{
const decision=acquisitionAllocationDecision(draft,row),d=row.d!;
const unknown=(reason:string):AcquisitionResult=>({...raw,deductible:null,reason,eligibility:{status:'indeterminate',reason,source:decision.source}});
if(!decision.ratio)return unknown(decision.reason);if(!row.bc)return unknown('Complete a natureza/condiçõesB/C da aquisição antes da proporção;sem isenção de condições documentais.');
const base=evaluateAcquisitionBC(row,currency,registered,draft,raw,{right:{status:'eligible',reason:decision.reason,source:decision.source,scopeId:draft?acquisitionScope(draft):undefined},noExcludedPayments:d.noExcludedPayments});
if(base.deductible===null||base.eligibility?.status!=='eligible')return base;
const before=base.deductibleExact??decimalFraction(base.deductible);const exact=fraction(BigInt(before.numerator)*BigInt(decision.ratio.numerator),BigInt(before.denominator)*BigInt(decision.ratio.denominator));const reason=base.reason+' '+decision.reason;
return {...base,deductible:fractionPresentation(exact).decimal,deductibleExact:exact,nonDeductibleExact:subtractFractions(decimalFraction(base.vat!),exact),reason,eligibility:{...base.eligibility,status:'eligible',reason},allocationCalculation:{version:acquisitionAllocationVersion,ratio:decision.ratio,rawRatio:decision.rawRatio??decision.ratio,before,source:decision.source}};
}
+9 -7
View File
@@ -1,4 +1,5 @@
import type {Acquisition,AcquisitionResult} from './iva-acquisitions.ts';
import type {AcquisitionEligibility} from './iva-acquisition-right.ts';
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
import {linkedOperationRight,linkedBeverageDeductionRight} from './iva-acquisition-right.ts';
import {acquisitionTreatmentIssue} from './iva-acquisition-continuity.ts';
@@ -27,8 +28,8 @@ export function reuseAcquisitionImport(draft:Record<string,unknown>,row:Acquisit
}
export function acquisitionBCSourceKey(draft:Record<string,unknown>,rows:Acquisition[]):string{return rows.some(r=>r.bc)?JSON.stringify({version:acquisitionBCVersion,sources:rows.filter(r=>r.bc?.source).map(r=>acquisitionImportSources(draft).find(s=>s.map===r.bc!.source!.map&&s.row.id===r.bc!.source!.id)?.row??null)}):'';}
function validDate(value:string){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 evaluateAcquisitionBC(row:Acquisition,currency:string,registered:Choice,draft:Record<string,unknown>|undefined,raw:AcquisitionResult):AcquisitionResult{
const bc=row.bc!;let right=row.deductionLink&&draft?linkedOperationRight(draft,row.deductionLink):{status:'indeterminate' as const,reason:'Vínculo à operação de destino em falta.',source:acquisitionBCSource};
export function evaluateAcquisitionBC(row:Acquisition,currency:string,registered:Choice,draft:Record<string,unknown>|undefined,raw:AcquisitionResult,partial?:{right:AcquisitionEligibility;noExcludedPayments:Choice}):AcquisitionResult{
const bc=row.bc!;let right=partial?.right??(row.deductionLink&&draft?linkedOperationRight(draft,row.deductionLink):{status:'indeterminate' as const,reason:'Vínculo à operação de destino em falta.',source:acquisitionBCSource});
const stop=(reason:string,status:'eligible'|'no_right'|'indeterminate'='indeterminate',percent?:number):AcquisitionResult=>{
const exact=percent!==undefined&&raw.vat!==null?decimalFraction(raw.vat):null;
const deductibleExact=exact?fraction(BigInt(exact.numerator)*BigInt(percent!),BigInt(exact.denominator)*100n):undefined;
@@ -48,13 +49,14 @@ export function evaluateAcquisitionBC(row:Acquisition,currency:string,registered
if(bc.source){const src=draft?acquisitionImportSources(draft).find(s=>s.map===bc.source!.map&&s.row.id===bc.source!.id):undefined;if(!src||JSON.stringify(src.row)!==bc.source.fingerprint)return stop('OrigemL1 da importação alterada:adopte explicitamente os dados actuais,conservando edições e histórico.');if(['service_in_customs_value','accessory_service_first_destination'].includes(src.row.treatment??''))return stop('Origem é serviço incluído na base aduaneira12(1)(e),não IVA próprio da importação de bens;identifique o bilhete dos bens,sem somar novamente IVA do serviço.');}
if(registered!=='yes')return stop('Enquadramento do adquirente no regime normal por confirmar.');
if(row.allocation==='no_right'||row.integrity==='no'||row.exclusion==='excluded')return stop('Sem direito pela afectação,exclusão expressa ou impedimento18(3–5) declarado.','no_right',0);
if(row.allocation==='mixed')return stop('Afectação parcial/pro rata depende deD;sem dedução integral presumida.');
if(!row.deductionLink||row.deductionLink.scopeConfirmed!=='yes'||!row.deductionLink.allocationRef.trim()||row.allocation!=='taxable_only')return stop('Confirme afectação exclusiva e referência à operação ligada.');
if(bc.expense==='beverages'&&bc.facts.beverageSpecial==='yes'&&bc.beveragePath==='deduction'&&draft)right=linkedBeverageDeductionRight(draft,row.deductionLink,bc.facts.notCoastalFishing==='yes');
if(row.allocation==='mixed'&&!partial)return stop('Afectação parcial/pro rata depende deD;sem dedução integral presumida.');
if(!partial&&(!row.deductionLink||row.deductionLink.scopeConfirmed!=='yes'||!row.deductionLink.allocationRef.trim()||row.allocation!=='taxable_only'))return stop('Confirme afectação exclusiva e referência à operação ligada.');
if(!partial&&bc.expense==='beverages'&&bc.facts.beverageSpecial==='yes'&&bc.beveragePath==='deduction'&&draft&&row.deductionLink)right=linkedBeverageDeductionRight(draft,row.deductionLink,bc.facts.notCoastalFishing==='yes');
if(right.status==='no_right')return stop(right.reason,'no_right',0);
if(right.status!=='eligible')return stop(right.reason);
if(row.deductionLink.noExcludedPayments==='no')return stop('Pagamentos15(6)(c)/19(2) excluídos.','no_right',0);
if(row.deductionLink.noExcludedPayments!=='yes')return stop('Exclusão19(2) por esclarecer.');
const noExcludedPayments=partial?.noExcludedPayments??row.deductionLink?.noExcludedPayments;
if(noExcludedPayments==='no')return stop('Pagamentos15(6)(c)/19(2) excluídos.','no_right',0);
if(noExcludedPayments!=='yes')return stop('Exclusão19(2) por esclarecer.');
if(!validDate(row.documentDate)||row.documentDate.slice(0,7)!==row.period)return stop('Documento e exercício no mesmo mês neste incremento;dedução posterior não presumida.');
if(!row.documentRef.trim()||row.legalDocument!=='yes'||row.held!=='yes'||row.integrity!=='yes')return stop('Referência,forma legal,posse e integridade do documento por completar.');
if(row.domestic==='no'){
+17 -8
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 {evaluateAcquisitionAllocation,validAcquisitionAllocation,allocationSourceKey,allocationHasUnconsolidatedOutputs,acquisitionAllocationVersion,acquisitionAllocationSource,type AcquisitionAllocation} from './iva-acquisition-allocation.ts';
import {evaluateAcquisitionBC,validAcquisitionBC,acquisitionBCSource,acquisitionBCSourceKey,acquisitionBCDetails,expenseOptions,type AcquisitionBC} from './iva-acquisition-bc.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';
@@ -21,9 +22,10 @@ export type Acquisition = {
acquisitionTreatment?: AcquisitionTreatment;
deductionLink?: AcquisitionRightLink;
bc?:AcquisitionBC;
d?:AcquisitionAllocation;
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; eligibility?:AcquisitionEligibility;deductibleExact?:FiscalFraction;nonDeductibleExact?:FiscalFraction};
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string; eligibility?:AcquisitionEligibility;deductibleExact?:FiscalFraction;nonDeductibleExact?:FiscalFraction;allocationCalculation?:{version:string;ratio:FiscalFraction;rawRatio:FiscalFraction;before:FiscalFraction;source:string}};
type PeriodInput = {period:string; complete:boolean; absent:string[];importsComplete?:boolean};
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;
@@ -43,6 +45,7 @@ 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?.rows?.some(row=>row.d!==undefined&&!validAcquisitionAllocation(row.d)))throw new Error('AfectaçãoD não recuperável;originais preservados.');
if(book?.rows?.some(row=>row.bc!==undefined&&!validAcquisitionBC(row.bc))||book?.periods?.some(p=>p.importsComplete!==undefined&&typeof p.importsComplete!=='boolean'))throw new Error('CondiçõesB/C ou cobertura de importações inválidas;originais preservados.');
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.');
@@ -117,6 +120,7 @@ function evaluateAcquisitionBase(row:Acquisition,currency:string,taxpayerRegiste
}
export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegistered:Choice='',draft?:Record<string,unknown>):AcquisitionResult {
const result=evaluateAcquisitionBase(row,currency,taxpayerRegistered);
if(row.d)return evaluateAcquisitionAllocation(row,currency,taxpayerRegistered,draft,result);
if(row.bc)return evaluateAcquisitionBC(row,currency,taxpayerRegistered,draft,result);
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};
@@ -137,7 +141,7 @@ function sourceKey(draft:Record<string,unknown>,book:AcquisitionBook):string {
// 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}`:''}${book.rightVersion||book.rows.some(r=>r.deductionLink)?`:${acquisitionRightVersion}`:''}${book.rows.some(r=>r.bc)?`:${acquisitionBCSourceKey(draft,book.rows)}`:''}`;
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}`:''}${book.rows.some(r=>r.bc)?`:${acquisitionBCSourceKey(draft,book.rows)}`:''}${book.rows.some(r=>r.d)?`:${acquisitionAllocationVersion}:${allocationSourceKey(draft,book.rows)}`:''}`;
}
function location(draft:Record<string,unknown>):string {
const labels=draft.selection_labels as Record<string,unknown>|undefined;
@@ -163,7 +167,7 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
}
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}};
if(!row.deductionLink&&!row.d&&(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 ?? ''));
@@ -178,7 +182,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)&&!(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.');
if((!sales || sales.net_sales===null || sales.vat===null)&&!(book.rows.some(r=>r.deductionLink||r.d)&&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) {
@@ -190,7 +194,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=>!r.deductionLink&&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&&!r.d&&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).`);
}
@@ -210,20 +214,24 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
fields['17']=state===null||taxpayer===null?null:money(state>taxpayer?state-taxpayer:0n);
fields['18']=state===null||taxpayer===null?null:money(taxpayer>state?taxpayer-state:0n);
const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null;
if(classified?.exactFields||book.rows.some(r=>r.bc)) {
if(classified?.exactFields||book.rows.some(r=>r.bc||r.d)) {
const exactFields:Record<string,FiscalFraction|null>={};
for(const f of ['01','02','03','04','05','06','07','08','09','10','11','12','13'])exactFields[f]=classified?.exactFields?.[f]??(fields[f]===null?null:decimalFraction(fields[f]!));
for(const f of ['08','09','10','11'])if(fields[f]!==null){const matching=book.rows.filter(r=>r.period===period&&r.field===f).map(r=>rows.find(v=>v.id===r.id)!);exactFields[f]=addFractions(matching.map(r=>r.deductibleExact??decimalFraction(r.deductible!)));}
for(const [f,parts] of [['14',['01','03','05','06','07']],['15',['08','09','10','11','12']],['16',['02','04','13']]] as const)exactFields[f]=parts.some(k=>exactFields[k]===null)?null:addFractions(parts.map(k=>exactFields[k]!));
exactFields['17']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['16'],exactFields['15'])):null;
exactFields['18']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['15'],exactFields['16'])):null;
if(book.rows.some(r=>r.d)&&allocationHasUnconsolidatedOutputs(draft,period)){
for(const f of ['14','16','17','18'])exactFields[f]=null;
reasons.push('Origem18(6) identifica base/proporção,sem IVA liquidado consolidado no livro de operações. Não presumir impostozero nem duplicar a base:totais14/16/17/18 permanecem indeterminados;dedução15 conserva apuramento próprio.');
}
for(const f of Object.keys(exactFields))fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null;
reasons.push('Totais derivados das fracções exactas, sem somar aproximações das linhas; decimais a6casas são apresentação técnica, não arredondamento declarativo.');
return {period,fields,exactFields,reasons};
}
return {period,fields,reasons};
});
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:book.rows.some(r=>r.bc)?acquisitionBCSource:acquisitionBasis,sources:acquisitionSources+(book.rows.some(r=>r.acquisitionTreatment==='former_exploration_exemption'||r.acquisitionTreatmentOrigin)?`; ${formerExplorationSource}`:''),declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:book.rows.some(r=>r.d)?acquisitionAllocationSource:book.rows.some(r=>r.bc)?acquisitionBCSource:acquisitionBasis,sources:acquisitionSources+(book.rows.some(r=>r.acquisitionTreatment==='former_exploration_exemption'||r.acquisitionTreatmentOrigin)?`; ${formerExplorationSource}`:''),declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
}
export function recordAcquisitions(draft:Record<string,unknown>,book:AcquisitionBook,now=new Date().toISOString()):AcquisitionBook {
const result=calculateAcquisitions(draft,book,now);
@@ -238,8 +246,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(row.d)out.push(['IVA mensal',`AfectaçãoD — ${row.id}`,`${{actual:'Afectação real22(2)',pro_rata:'Pro rata provisório22',reduced:'Proporção específica18(6)','':'Método por identificar'}[row.d.method]};âmbito${row.d.scopeRef};quociente${r.allocationCalculation?fractionLabel(r.allocationCalculation.rawRatio):'Indeterminado'};proporção aplicada${r.allocationCalculation?fractionLabel(r.allocationCalculation.ratio):'Indeterminada'};IVA elegível antesD${r.allocationCalculation?fractionLabel(r.allocationCalculation.before):'Indeterminado'};dedutível${r.deductibleExact?fractionLabel(r.deductibleExact):'Indeterminado'};não dedutível${r.nonDeductibleExact?fractionLabel(r.nonDeductibleExact):'Indeterminado'}`,`${row.d.source};${row.d.method==='reduced'?'origem '+row.d.reducedId:'bases '+row.d.numerator+'/'+row.d.denominator+' '+(row.d.method==='actual'?row.d.unit:'MZN')+';ano '+(row.d.method==='pro_rata'?row.d.referenceYear:row.period.slice(0,4))+';comunicação '+row.d.communicationRef+';declaração '+row.d.declarationRef};${r.allocationCalculation?.source??r.eligibility?.source??''}`]);
if(row.bc)out.push(['IVA mensal',`CondiçõesB/C — ${row.id}`,`${row.field==='11'?'Bilhete de despacho de importação;importador '+row.bc.importer+';bens '+row.bc.goodsRef+';IVA '+({due:'devido identificado',exempt:'isento',zero:'zero expresso',unknown:'desconhecido','':'por indicar'}[row.bc.vatBasis])+';fonte '+row.bc.vatRef:'Despesa '+(expenseOptions.find(([id])=>id===row.bc!.expense)?.[1]??'por identificar')+';IVA suportado17-A:'+({yes:'Sim',no:'Não','':'Por indicar'}[row.bc.reducedSupply])};dedutível exacto:${r.deductibleExact?fractionLabel(r.deductibleExact):'Indeterminado'};não dedutível:${r.nonDeductibleExact?fractionLabel(r.nonDeductibleExact):'Indeterminado'}`,`${row.bc.evidence};${acquisitionBCDetails(row.bc)};origem${row.bc.source?'registo de importaçãoL1 '+row.bc.source.id:'introdução directa'};${acquisitionBCSource}`]);
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??''}`]);
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}; ${row.d?'âmbitoD '+row.d.scopeRef:'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: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':row.documentType==='customs'?'Bilhete de despacho de importação':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]);
@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IvaAcquisitionAllocationDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_allocation_recovers_with_exact_fractions_and_preserved_ABC(): 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-d-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:370'];
$this->assertCount(4, $book['rows']);
$this->assertSame(['eligible','eligible','eligible','eligible'], array_column(array_column($book['result']['rows'], 'eligibility'), 'status'));
$this->assertSame('24.16', $book['result']['periods'][0]['fields']['15']);
$this->assertCount(1, $book['history']);
$this->assertSame('10.72', $book['result']['periods'][0]['fields']['11']);
$this->assertSame('135.84', $book['result']['periods'][0]['fields']['17']);
$this->assertSame('customs', $book['rows'][3]['documentType']);
$this->assertSame('actual', $book['rows'][0]['d']['method']);
$this->assertSame(['numerator'=>'16','denominator'=>'3'], $book['result']['rows'][0]['deductibleExact']);
$this->assertSame(['numerator'=>'17','denominator'=>'50'], $book['result']['rows'][1]['allocationCalculation']['ratio']);
$this->assertSame('yes', $book['rows'][0]['d']['facts']['communication']);
$this->assertSame('6.40', $store['books']['opportunity:371']['result']['periods'][0]['fields']['15']);
$this->assertNull($store['books']['opportunity:371']['result']['periods'][0]['fields']['16']);
$this->assertSame('public_works', $store['books']['opportunity:371']['rows'][0]['d']['reducedId']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l2d-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-d-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,66 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {recordSales,saveSalesBook} from '../../resources/js/lib/iva-operations.ts';
import {recordAcquisitions,evaluateAcquisition,saveAcquisitionBook,acquisitionBook,acquisitionCurrent,acquisitionReportRows,readAcquisitionStore} from '../../resources/js/lib/iva-acquisitions.ts';
import {saveImportExemptionBook} from '../../resources/js/lib/iva-import-exemptions.ts';
import {newAcquisitionAllocation,acquisitionAllocationDecision,adoptAllocationReduced} from '../../resources/js/lib/iva-acquisition-allocation.ts';
import {newReducedBase,reducedBaseConditions,ownReducedConditions,recordReducedBases,saveReducedBaseBook,reducedBaseBook} from '../../resources/js/lib/iva-reduced-bases.ts';
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l2d-preserved.json',import.meta.url),'utf8'));
export function allocationFixture(){
const draft={...structuredClone(frozen),context_opportunity:'opportunity:370',project_title:'SYNTHETIC QA — L2-D afectação e proporções'};const store=JSON.parse(frozen.modelo_a_snapshot);
draft.modelo_a_snapshot=saveSalesBook(draft,recordSales(draft,structuredClone(store.salesOperations['opportunity:369'])));
draft.modelo_a_snapshot=saveImportExemptionBook(draft,structuredClone(store.importExemptions['opportunity:369']));
const base=store.books['opportunity:369'];
const d=(method,patch={})=>({...newAcquisitionAllocation('2026-12'),method,numerator:'1',denominator:'3',unit:'m2 utilizados',scopeRef:'SYNTHETIC QA entidade/actividades/ano2026',source:'SYNTHETIC QA contabilidade/critério',communicationRef:'SYNTHETIC QA comunicação prévia22(2)',noExcludedPayments:'yes',basis:'prior_year',facts:{scopeConfirmed:'yes',rightBasis:'yes',actualScope:'yes',communication:'yes',conditionsMet:'yes',annualScope:'yes',netExclusions:'yes'},...patch});
const row=(id,method='actual',patch={},dpatch={})=>({...structuredClone(base.rows[3]),id,description:'SYNTHETIC QA '+id,documentRef:'SYNTHETIC QA '+id,deductionLink:undefined,allocation:'mixed',d:d(method,dpatch),...patch});
const imported={...structuredClone(base.rows[0]),id:'imported',deductionLink:undefined,allocation:'mixed',d:d('pro_rata',{numerator:'2',denominator:'3'})};
const book={currency:'MZN',rightVersion:'acquisition-operation-right/1',taxpayerRegistered:'yes',rows:[row('actual'),row('pro-rata','pro_rata'),row('diesel','actual',{bc:{...base.rows[1].bc}}),imported],periods:structuredClone(base.periods)};
return{draft,book,row,d};
}
test('actual allocation uses exact fraction and prior communication,without inventing approval or bypassing C',()=>{
const{draft,row}=allocationFixture();const r=row('actual');let value=evaluateAcquisition(r,'MZN','yes',draft);assert.deepEqual(value.deductibleExact,{numerator:'16',denominator:'3'});assert.deepEqual(value.nonDeductibleExact,{numerator:'32',denominator:'3'});assert.equal(value.eligibility.status,'eligible');
for(const patch of [{numerator:''},{denominator:'0'},{denominator:'0.1'},{unit:''},{communicationRef:''},{scopeRef:''},{source:''},{facts:{...r.d.facts,communication:''}},{facts:{...r.d.facts,conditionsMet:''}},{facts:{...r.d.facts,rightBasis:''}}])assert.equal(evaluateAcquisition({...r,d:{...r.d,...patch}},'MZN','yes',draft).deductible,null,JSON.stringify(patch));
for(const patch of [{held:''},{documentRef:''},{legalDocument:''},{documentDate:'2026-11-10'}])assert.equal(evaluateAcquisition({...r,...patch},'MZN','yes',draft).deductible,null);
assert.equal(evaluateAcquisition({...r,bc:{...r.bc,expense:'travel'}},'MZN','yes',draft).deductible,'0.00');
assert.equal(evaluateAcquisition({...r,bc:{...r.bc,reducedSupply:'yes'}},'MZN','yes',draft).deductible,'0.00');
});
test('pro rata uses annual prior year or declared initial estimate;ceil hundredth is preserved separately from money',()=>{
const{draft,row}=allocationFixture();const r=row('pro','pro_rata');const calc=patch=>evaluateAcquisition({...r,d:{...r.d,...patch}},'MZN','yes',draft);
const v=calc({});assert.deepEqual(v.allocationCalculation.rawRatio,{numerator:'1',denominator:'3'});assert.deepEqual(v.allocationCalculation.ratio,{numerator:'17',denominator:'50'});assert.equal(v.deductible,'5.44');assert.match(v.reason,/34%/);
assert.equal(calc({numerator:'0'}).deductible,'0.00');assert.equal(calc({numerator:'3'}).deductible,'16.00');
assert.equal(calc({numerator:'1',denominator:'2'}).deductible,'8.00');
for(const patch of [{referenceYear:'2026'},{actualRequired:'yes'},{basis:''},{facts:{...r.d.facts,annualScope:''}},{facts:{...r.d.facts,netExclusions:''}}])assert.equal(calc(patch).deductible,null,JSON.stringify(patch));
const initial={basis:'initial_changed',referenceYear:'2026',declarationRef:'SYNTHETIC QA início/alteração',facts:{...r.d.facts,initialChanged:'yes'}};assert.equal(calc(initial).deductible,'5.44');assert.equal(calc({...initial,declarationRef:''}).deductible,null);
});
test('F composes C diesel only once with D and import11,keeps exact fractions and unknown dependencies',()=>{
const{draft,book}=allocationFixture();const result=recordAcquisitions(draft,book).result;assert.deepEqual(result.rows[2].allocationCalculation.before,{numerator:'8',denominator:'1'});assert.deepEqual(result.rows[2].deductibleExact,{numerator:'8',denominator:'3'});
assert.deepEqual(['09','11','14','15','16','17','18'].map(k=>result.periods[0].fields[k]),['13.44','10.72','1000.00','24.16','160.00','135.84','0.00']);
const incomplete=structuredClone(book);incomplete.rows[0].d.denominator='0';assert.equal(recordAcquisitions(draft,incomplete).result.periods[0].fields['15'],null);
const coverage=structuredClone(book);coverage.periods[0].importsComplete=false;assert.equal(recordAcquisitions(draft,coverage).result.periods[0].fields['17'],null);
const duplicate=structuredClone(book);duplicate.rows.push({...book.rows[0],id:'duplicate'});assert.throws(()=>recordAcquisitions(draft,duplicate),/repetido/);
const saved=recordAcquisitions(draft,book);draft.modelo_a_snapshot=saveAcquisitionBook(draft,saved);addReduced(draft);assert.equal(acquisitionCurrent(draft,saved),false);const outputGap=recordAcquisitions(draft,saved).result;assert.equal(outputGap.periods[0].fields['15'],'24.16');assert.equal(outputGap.periods[0].fields['16'],null);
});
function addReduced(draft){const rows=['regulated_energy','aeronautical_fees','public_works','public_water'].map(t=>({...newReducedBase(t,'MZN'),treatment:t,description:'SYNTHETIC QA '+t,period:'2026-12',operationDate:'2026-12-10',documentDate:'2026-12-10',documentRef:'SYNTHETIC QA '+t,priceRef:'SYNTHETIC QA preço/taxa',priceFrom:'2026-01-01',priceScope:'SYNTHETIC QA âmbito',consideration:'100',projectRef:'SYNTHETIC QA contrato público',workCategory:'roads',compositionRef:'SYNTHETIC QA composição semIVA',evidence:'SYNTHETIC QA condições',conditions:Object.fromEntries([...reducedBaseConditions,...ownReducedConditions(t)].map(([key])=>[key,'yes']))}));draft.modelo_a_snapshot=saveReducedBaseBook(draft,recordReducedBases(draft,{rows}));}
test('18(6) reuses all four demonstrated reduced bases;no double base reduction or invented output tax',()=>{
const{draft,row}=allocationFixture();addReduced(draft);
const expected={regulated_energy:'9.92',aeronautical_fees:'13.60',public_works:'6.40',public_water:'12.00'};
for(const[id,value]of Object.entries(expected)){
const r=row('purchase-'+id,'reduced',{allocation:'taxable_only'});r.d=adoptAllocationReduced(draft,r.d,id);r.d.facts.scopeConfirmed='yes';const result=evaluateAcquisition(r,'MZN','yes',draft);assert.equal(result.deductible,value,id);assert.equal(r.net,'100');assert.equal(r.vat,'16');
const book=recordAcquisitions(draft,{currency:'MZN',taxpayerRegistered:'yes',rows:[r],periods:[{period:'2026-12',complete:true,importsComplete:true,absent:['12','13']}]});assert.equal(book.result.periods[0].fields['15'],value);for(const f of ['14','16','17','18'])assert.equal(book.result.periods[0].fields[f],null,f);
const saved={...draft,modelo_a_snapshot:saveAcquisitionBook(draft,book)};assert.ok(acquisitionCurrent(saved,book));const removed={...saved,modelo_a_snapshot:saveReducedBaseBook(saved,{rows:reducedBaseBook(saved).rows})};assert.equal(acquisitionCurrent(removed,book),false);
assert.equal(evaluateAcquisition({...r,allocation:'mixed'},'MZN','yes',draft).deductible,null);
const changed=reducedBaseBook(draft);changed.rows.find(x=>x.id===id).description+=' alteração';const other={...draft,modelo_a_snapshot:saveReducedBaseBook(draft,recordReducedBases(draft,changed))};assert.equal(evaluateAcquisition(r,'MZN','yes',other).deductible,null);
}
});
test('save recovery/report preserves every prior ABC/L1 book and frozen D histories;no automatic recalculation',()=>{
let{draft,book}=allocationFixture();const prior=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.ok(acquisitionCurrent(recovered,acquisitionBook(recovered)));assert.deepEqual(book.history,[first]);
const current=JSON.parse(draft.modelo_a_snapshot);for(const k of Object.keys(prior).filter(k=>k!=='version'))for(const[id,v]of Object.entries(prior[k]))assert.deepEqual(current[k][id],v,k+id);
for(const scope of ['opportunity:368','opportunity:369'])assert.ok(acquisitionCurrent({...recovered,context_opportunity:scope},prior.books[scope]));
const report=JSON.stringify(acquisitionReportRows(recovered));for(const token of ['AfectaçãoD','16/3','1/3','34%','24.16','135.84','22(2)'])assert.ok(report.includes(token),token);
book.rows[0].d.numerator='2';draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);assert.equal(acquisitionCurrent(draft,book),false);assert.deepEqual(book.history,[first]);assert.match(JSON.stringify(acquisitionReportRows(draft)),/Histórico desactualizado/);
const broken=JSON.parse(recovered.modelo_a_snapshot);broken.books['opportunity:370'].rows[0].d.facts.scopeConfirmed='maybe';assert.throws(()=>readAcquisitionStore(JSON.stringify(broken)),/AfectaçãoD/);
if(process.env.IVA_L2D_CALCULATED){
const special={...recovered,context_opportunity:'opportunity:371'};special.modelo_a_snapshot=saveSalesBook(special,recordSales(special,structuredClone(current.salesOperations['opportunity:370'])));addReduced(special);
const reduced=allocationFixture().row('reduced-public-works','reduced',{allocation:'taxable_only'});reduced.d=adoptAllocationReduced(special,reduced.d,'public_works');reduced.d.facts.scopeConfirmed='yes';const reducedBook=recordAcquisitions(special,{currency:'MZN',taxpayerRegistered:'yes',rows:[reduced],periods:[{period:'2026-12',complete:true,importsComplete:true,absent:['12','13']}]});special.modelo_a_snapshot=saveAcquisitionBook(special,reducedBook);
fs.writeFileSync(process.env.IVA_L2D_CALCULATED,JSON.stringify({...special,context_opportunity:'opportunity:370'}));
}
});