Prepare L3 C substitutions with frozen predecessor and payment reconciliation in QA

This commit is contained in:
2026-10-25 14:23:56 +02:00
parent 3aa851a467
commit b7a745e64f
13 changed files with 444 additions and 3 deletions
@@ -26681,5 +26681,99 @@
"registered_paid_MZN": 30,
"outstanding_MZN": 42
}
},
"l3_c_qa_20261025": {
"status": "L3-C concluído em QA no âmbito demonstrado;sem instalação",
"matrix_anchor": "AUD-SIM-INT02",
"installed_revision": "109cb0fe",
"installed_receipt": "3aa851a4",
"implemented": {
"prior": "Vínculo expresso a resultadoAB/C congelado por identidade,versão e impressão de alterações,com âmbito/período/natureza,17/18,pagamentos/reporte e saldo anterior. Reutiliza data disponível e referência da substituição anterior;divergência exige reconciliação.",
"differential": "21=(17actual−18actual)−(17anterior−18anterior),fracções exactas;22=max(21,0);24=22+juros adicionais identificados. Crédito credor separado,aguarda comunicação20;19/20/25/26/27 não atribuídos porC.",
"payments": "Só pagamentos adicionais;documentosAB/C recuperados,incluindo versões históricas,impedem reutilização. ReciprocamenteABrecusa pagamento já congelado emC. Créditos já usados não são aplicados outra vez.",
"F": "Saldo adicional e anterior separados;total por liquidar é soma sem compensação automática do crédito pendente/excedente. Total registado conserva pagamentos anteriores mais adicionais;nenhuma alteração da declaração original.",
"history": "Novo mapaivaSubstitutions no snapshot existente;entradas/resultados congelados,histórico acrescentado,relatório usa próprias entradas. Cadeia de substituições liga versão anterior ao novo diferencial;sem ciclos/duplicação de sucessores. Todos os mapasL1/L2/AB preservados.",
"UI": "Percurso no componente existente de aquisições,preparação versus apresentada fora doInvesteMoz,origens congeladas,motivo,juros e pagamentos adicionais. Gravar/recuperar/relatório privado existentes reutilizados."
},
"source_cotejo": {
"accepted_exemplar": "VAT Return.pdf SHA2569c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741,p2/quadro9;quatro combinações de17/18,positivo22,negativo só20 após comunicação.",
"regulation": "Decreto7/2008,RCIVA15,BR16,16/04/2008,PDF3/p101;distinção entre primeira declaração e substituição.",
"scope": "C substituição da declaração;rectificação de aquisições12/13 e regularizações/errosL2 mantêm âmbito próprio,sem repetir a mesma correcção como benefício adicional.",
"originals_unchanged": true,
"no_new_generic_research": true
},
"example_MZN": {
"original": {
"17": 160,
"19_used": 70,
"20_used": 20,
"24": 72,
"paid": 30,
"outstanding": 42
},
"corrected_L2_17": 220,
"substitution": {
"21": 60,
"22": 60,
"additional_interest23": 1,
"additional_calculated24": 61,
"additional_paid": 20,
"additional_outstanding": 41
},
"reconciliation": {
"total_registered_paid": 50,
"total_outstanding_without_offset": 83
},
"credit_example": "Actual18=40 versus anterior17=160→21=−200;crédito200 aguarda comunicação,não abate42anteriores nem cria26."
},
"validation": {
"frontend": "18dirigidos passaram na integraçãoAB/C (9AB+9C);após ajuste de actualidade,10C passaram;teste adicional de ciclo passou isoladamente. Total20casos dirigidos distintos(9AB+11C).",
"API": "1teste/109asserções;guarda DBsimulator_fiscal_delta_20261021_test/55441 eDatabaseTransactions rollback. Snapshot recuperado byte a byte,declaraçãoABoriginal72/30/42,mapasL1/L2/históricos preservados.",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"UI": "Componente real:recibo anteriorP1recusado como novo pagamento;reporP2 permite reconciliação;gravação→recuperação→relatório confirma61/20/41 adicionais,30/42 anteriores e50/83 totais. ComparaçãoAPI/UI confirma todos os mapas anteriores inalterados.",
"proof": "outputs/iva-l3-c-20261025/ui-recovered-substitution.png",
"logs": [
"outputs/iva-l3-c-20261025/frontend-final.txt",
"outputs/iva-l3-c-20261025/frontend-c-final.txt",
"outputs/iva-l3-c-20261025/frontend-cycle.txt",
"outputs/iva-l3-c-20261025/api-final.txt",
"outputs/iva-l3-c-20261025/types-final.txt",
"outputs/iva-l3-c-20261025/build-final.txt"
]
},
"limits": [
"Cobertura demonstrada2026/MZN com origensL2 e declarações anteriores congeladas recuperáveis. Cenários projectados permanecem pressupostos;nenhuma submissãoAT ou certificação de cobrança.",
"RCIVA15 refereModeloB;exemplar recebidoVAT Return assinala substituiçãoModeloA. Cálculo e dossier de apoio disponíveis;preenchimento/submissão do formulário regulamentar e correspondência de edição não automatizados. Não reabreP00105/06.",
"Juros adicionais:montante identificado ou inexistência expressa com fonte/motivo;sem cálculo de taxa/calendário/prazo. Pagamentos anteriores não abatidos novamente;excedente adicional não cria crédito20.",
"Diferencial credor aguarda comunicação dos serviços;não cria transporte26/reembolso27 nem compensa dívida anterior automaticamente. A comunicação20 futura é registada no percursoAB existente com documento/data;sem pedido/submissão automática ou conta corrente centralAT.",
"Controlo de duplicação limitado aos livros recuperados no mesmo rascunho/contextos efectivos;cenários de oportunidades distintas são independentes. Não é reconciliação global entre rascunhos privados diferentes.",
"Históricos permanecem intactos. Versão anterior é escolhida expressamente;alterações/preparações não substituem uma declaração apresentada. Uma mesma anterior não tem dois sucessores no mesmo âmbito recuperado;cadeias circulares recusadas.",
"L3-D reembolso geral eL3-E restituição de bebidas pendentes. L2 mantém18(6)+afectação mista,sucessivas/plurianuais,erros/incobráveis. L1 mantém129fechos+P058/P084/P085+P001=133. Estatísticas/IA adiadas."
],
"runtime_delta": [
{
"path": "resources/js/lib/iva-substitution.ts",
"sha256_LF": "0ea914bf952ff053f7e6912705f80526aaed5a8b9f4b3bd5a27f737c4aef3977"
},
{
"path": "resources/js/components/acquisition-substitution.tsx",
"sha256_LF": "6a61f59bcbd493ca45e60a14fe3c67375f581581f9cbe882426fdbffa98399c2"
},
{
"path": "resources/js/lib/iva-closure.ts",
"sha256_LF": "3e6491a5e14ee36d264fac56ce1dd363044e2aa34e99fd8325ddf997b97afedb"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "8723882e67dcd6e1ac48b4e3d6602565331cc66295dd887381d50cfdeb072787"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "320440142dff8b26f3eb3c4760b1bbc475694a7e67934e3671cd7d085f30516a"
}
],
"schema_changes": false,
"active_writes": false
}
}
@@ -0,0 +1,45 @@
{
"status": "QA_prepared_not_installed",
"candidate": "commit que contém este manifesto",
"baseline_installed": "109cb0fe9e1530dd09b8277a422ae9c5e68e0674",
"baseline_receipt": "3aa851a4",
"report": "docs/investmoz/simulations/l3-c-substitution-qa-20261025.json",
"coverage": "L3-C no âmbito demonstrado;D/E e residuaisL1/L2 mantidos",
"runtime_delta": [
{
"path": "resources/js/lib/iva-substitution.ts",
"sha256_LF": "0ea914bf952ff053f7e6912705f80526aaed5a8b9f4b3bd5a27f737c4aef3977"
},
{
"path": "resources/js/components/acquisition-substitution.tsx",
"sha256_LF": "6a61f59bcbd493ca45e60a14fe3c67375f581581f9cbe882426fdbffa98399c2"
},
{
"path": "resources/js/lib/iva-closure.ts",
"sha256_LF": "3e6491a5e14ee36d264fac56ce1dd363044e2aa34e99fd8325ddf997b97afedb"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "8723882e67dcd6e1ac48b4e3d6602565331cc66295dd887381d50cfdeb072787"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "320440142dff8b26f3eb3c4760b1bbc475694a7e67934e3671cd7d085f30516a"
}
],
"limits": [
"Cobertura demonstrada2026/MZN com origensL2 e declarações anteriores congeladas recuperáveis. Cenários projectados permanecem pressupostos;nenhuma submissãoAT ou certificação de cobrança.",
"RCIVA15 refereModeloB;exemplar recebidoVAT Return assinala substituiçãoModeloA. Cálculo e dossier de apoio disponíveis;preenchimento/submissão do formulário regulamentar e correspondência de edição não automatizados. Não reabreP00105/06.",
"Juros adicionais:montante identificado ou inexistência expressa com fonte/motivo;sem cálculo de taxa/calendário/prazo. Pagamentos anteriores não abatidos novamente;excedente adicional não cria crédito20.",
"Diferencial credor aguarda comunicação dos serviços;não cria transporte26/reembolso27 nem compensa dívida anterior automaticamente. A comunicação20 futura é registada no percursoAB existente com documento/data;sem pedido/submissão automática ou conta corrente centralAT.",
"Controlo de duplicação limitado aos livros recuperados no mesmo rascunho/contextos efectivos;cenários de oportunidades distintas são independentes. Não é reconciliação global entre rascunhos privados diferentes.",
"Históricos permanecem intactos. Versão anterior é escolhida expressamente;alterações/preparações não substituem uma declaração apresentada. Uma mesma anterior não tem dois sucessores no mesmo âmbito recuperado;cadeias circulares recusadas.",
"L3-D reembolso geral eL3-E restituição de bebidas pendentes. L2 mantém18(6)+afectação mista,sucessivas/plurianuais,erros/incobráveis. L1 mantém129fechos+P058/P084/P085+P001=133. Estatísticas/IA adiadas."
],
"required_at_delivery": [
"fetchmain fresco,preservação de concorrência",
"backup/restauro autenticados antes da instalação",
"Git existente,conferência só do delta eprovasQA reutilizadas"
],
"active_writes": false
}
@@ -0,0 +1,94 @@
{
"status": "L3-C concluído em QA no âmbito demonstrado;sem instalação",
"matrix_anchor": "AUD-SIM-INT02",
"installed_revision": "109cb0fe",
"installed_receipt": "3aa851a4",
"implemented": {
"prior": "Vínculo expresso a resultadoAB/C congelado por identidade,versão e impressão de alterações,com âmbito/período/natureza,17/18,pagamentos/reporte e saldo anterior. Reutiliza data disponível e referência da substituição anterior;divergência exige reconciliação.",
"differential": "21=(17actual−18actual)−(17anterior−18anterior),fracções exactas;22=max(21,0);24=22+juros adicionais identificados. Crédito credor separado,aguarda comunicação20;19/20/25/26/27 não atribuídos porC.",
"payments": "Só pagamentos adicionais;documentosAB/C recuperados,incluindo versões históricas,impedem reutilização. ReciprocamenteABrecusa pagamento já congelado emC. Créditos já usados não são aplicados outra vez.",
"F": "Saldo adicional e anterior separados;total por liquidar é soma sem compensação automática do crédito pendente/excedente. Total registado conserva pagamentos anteriores mais adicionais;nenhuma alteração da declaração original.",
"history": "Novo mapaivaSubstitutions no snapshot existente;entradas/resultados congelados,histórico acrescentado,relatório usa próprias entradas. Cadeia de substituições liga versão anterior ao novo diferencial;sem ciclos/duplicação de sucessores. Todos os mapasL1/L2/AB preservados.",
"UI": "Percurso no componente existente de aquisições,preparação versus apresentada fora doInvesteMoz,origens congeladas,motivo,juros e pagamentos adicionais. Gravar/recuperar/relatório privado existentes reutilizados."
},
"source_cotejo": {
"accepted_exemplar": "VAT Return.pdf SHA2569c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741,p2/quadro9;quatro combinações de17/18,positivo22,negativo só20 após comunicação.",
"regulation": "Decreto7/2008,RCIVA15,BR16,16/04/2008,PDF3/p101;distinção entre primeira declaração e substituição.",
"scope": "C substituição da declaração;rectificação de aquisições12/13 e regularizações/errosL2 mantêm âmbito próprio,sem repetir a mesma correcção como benefício adicional.",
"originals_unchanged": true,
"no_new_generic_research": true
},
"example_MZN": {
"original": {
"17": 160,
"19_used": 70,
"20_used": 20,
"24": 72,
"paid": 30,
"outstanding": 42
},
"corrected_L2_17": 220,
"substitution": {
"21": 60,
"22": 60,
"additional_interest23": 1,
"additional_calculated24": 61,
"additional_paid": 20,
"additional_outstanding": 41
},
"reconciliation": {
"total_registered_paid": 50,
"total_outstanding_without_offset": 83
},
"credit_example": "Actual18=40 versus anterior17=160→21=−200;crédito200 aguarda comunicação,não abate42anteriores nem cria26."
},
"validation": {
"frontend": "18dirigidos passaram na integraçãoAB/C (9AB+9C);após ajuste de actualidade,10C passaram;teste adicional de ciclo passou isoladamente. Total20casos dirigidos distintos(9AB+11C).",
"API": "1teste/109asserções;guarda DBsimulator_fiscal_delta_20261021_test/55441 eDatabaseTransactions rollback. Snapshot recuperado byte a byte,declaraçãoABoriginal72/30/42,mapasL1/L2/históricos preservados.",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"UI": "Componente real:recibo anteriorP1recusado como novo pagamento;reporP2 permite reconciliação;gravação→recuperação→relatório confirma61/20/41 adicionais,30/42 anteriores e50/83 totais. ComparaçãoAPI/UI confirma todos os mapas anteriores inalterados.",
"proof": "outputs/iva-l3-c-20261025/ui-recovered-substitution.png",
"logs": [
"outputs/iva-l3-c-20261025/frontend-final.txt",
"outputs/iva-l3-c-20261025/frontend-c-final.txt",
"outputs/iva-l3-c-20261025/frontend-cycle.txt",
"outputs/iva-l3-c-20261025/api-final.txt",
"outputs/iva-l3-c-20261025/types-final.txt",
"outputs/iva-l3-c-20261025/build-final.txt"
]
},
"limits": [
"Cobertura demonstrada2026/MZN com origensL2 e declarações anteriores congeladas recuperáveis. Cenários projectados permanecem pressupostos;nenhuma submissãoAT ou certificação de cobrança.",
"RCIVA15 refereModeloB;exemplar recebidoVAT Return assinala substituiçãoModeloA. Cálculo e dossier de apoio disponíveis;preenchimento/submissão do formulário regulamentar e correspondência de edição não automatizados. Não reabreP00105/06.",
"Juros adicionais:montante identificado ou inexistência expressa com fonte/motivo;sem cálculo de taxa/calendário/prazo. Pagamentos anteriores não abatidos novamente;excedente adicional não cria crédito20.",
"Diferencial credor aguarda comunicação dos serviços;não cria transporte26/reembolso27 nem compensa dívida anterior automaticamente. A comunicação20 futura é registada no percursoAB existente com documento/data;sem pedido/submissão automática ou conta corrente centralAT.",
"Controlo de duplicação limitado aos livros recuperados no mesmo rascunho/contextos efectivos;cenários de oportunidades distintas são independentes. Não é reconciliação global entre rascunhos privados diferentes.",
"Históricos permanecem intactos. Versão anterior é escolhida expressamente;alterações/preparações não substituem uma declaração apresentada. Uma mesma anterior não tem dois sucessores no mesmo âmbito recuperado;cadeias circulares recusadas.",
"L3-D reembolso geral eL3-E restituição de bebidas pendentes. L2 mantém18(6)+afectação mista,sucessivas/plurianuais,erros/incobráveis. L1 mantém129fechos+P058/P084/P085+P001=133. Estatísticas/IA adiadas."
],
"runtime_delta": [
{
"path": "resources/js/lib/iva-substitution.ts",
"sha256_LF": "0ea914bf952ff053f7e6912705f80526aaed5a8b9f4b3bd5a27f737c4aef3977"
},
{
"path": "resources/js/components/acquisition-substitution.tsx",
"sha256_LF": "6a61f59bcbd493ca45e60a14fe3c67375f581581f9cbe882426fdbffa98399c2"
},
{
"path": "resources/js/lib/iva-closure.ts",
"sha256_LF": "3e6491a5e14ee36d264fac56ce1dd363044e2aa34e99fd8325ddf997b97afedb"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "8723882e67dcd6e1ac48b4e3d6602565331cc66295dd887381d50cfdeb072787"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "320440142dff8b26f3eb3c4760b1bbc475694a7e67934e3671cd7d085f30516a"
}
],
"schema_changes": false,
"active_writes": false
}
@@ -1,5 +1,24 @@
# Rastreabilidade, utilização e formação
## 25/10/2026 — L3-C emQA no âmbito demonstrado;homologaçãoAB preservada
AB entregue109cb0fe,recibo3aa851a4;72calculados/30pagos registados/42porliquidar intactos. C não está instalado. L3 continua aberto,Dreembolso geral eErestituição de bebidas pendentes;L1 mantém129fechos+P058/P084/P085+P001=133 eL2 mantém18(6)+mista,sucessivas/plurianuais,erros/incobráveis. Estatísticas/IAadiadas.
No componente existente de aquisições,abrir «L3-C — substituições e diferencial». Escolher versão congelada da declaração anteriorAB/C e versão corrigidaL2 do mesmo período2026/MZN. A origem conserva entidade,natureza,17/18,pagamentos,reporte e saldo por liquidar. Reutilizar data disponível e referência da substituição anterior;pedir referência em falta. Indicar motivo e referência/data da preparação ou apresentação externa. Editar não submete àAT nem altera a declaração anterior. Formulário/registo externo não automatizados:RCIVA15 refereModeloB eexemplar recebido assinala substituiçãoModeloA;divergência conservada,sem impedir o cálculo/dossier e sem reabrirP00105/06.
C calcula21=(17actual−18actual)−(17anterior−18anterior) por fracções exactas,22positivo e24comjuros adicionais identificados. Não reutiliza créditos19/20 nem atribui25/26/27. Diferencial credor aguarda comunicação dos serviços para20 numa declaração posterior em prazo;não cria reporte/refund ou compensa dívida anterior automaticamente. Comunicação recebida continua documentada no percursoAB existente. Juros:montante identificado ou inexistência expressa com motivo,sem taxa/calendário presumidos.
Registar apenas novos pagamentos. Referência já congelada emAB/C ou repetida noutro âmbito efectivo recuperado exige reconciliação. AB também recusa documento já congelado emC;tentativa inválida não apaga o pagamento anterior. Excedente adicional não cria comunicação20. O controlo refere-se aos livros recuperados,não a conta corrente centralAT ou outros rascunhos privados. Preservam-se cenários projectados independentes.
Exemplo:MZN17anterior160→corrigido220,diferencial60;juros adicionais1→61calculados;novo pagamento20→41adicionais porliquidar. Declaração anterior conserva72calculados,30pagos,42porliquidar. Reconciliação mostra50registados e83porliquidar sem compensação automática. Caso credor:actual18=40 versus anterior17=160 gera−200;200aguardam comunicação,sem abater42anteriores ou criar26. Cadeia seguinte220→250gera apenas30novos;sem cobrar novamente os60anteriores. Sucessores repetidos/ciclos recusados.
Calcular e guardar substituiçõesC,gravar rascunho privado,recuperar,abrir relatório. Resultado congela as suas entradas e origens;recalcular acrescenta histórico,não o reescreve. Relatório recuperado usa documentos/valores da versão guardada. Todos os mapas anteriores comparados integralmente;20casos dirigidos distintos,API1/109asserções na baseQA55441 comrollback,tipos/build e componente real passaram. Prova:outputs/iva-l3-c-20261025/ui-recovered-substitution.png. Relatório:docs/investmoz/simulations/l3-c-substitution-qa-20261025.json.
## 25/10/2026 — inventário antes de L3-C
AB entregue109cb0fe/3aa851a4,72calculados/30pagos/42porliquidar preservados. Sem perdas confirmadas;L1129fechos/quatroresiduais eL2três famílias pendentes mantidos. Cotejo delimitado ao quadro9 do exemplar eRCIVA15. C prepara diferencial da substituição sem duplicar19/20,pagamentos ou alterar a declaração anterior. D/E permanecem pendentes.
## 25/10/2026 — entrega conjunta L3-A/B
Instalado 109cb0fe, candidata f7b778cd;7a380b76 preservada na ancestralidade e no backup/restauro anterior. IntegraçãoGit única,backup novo e restauroDocker isolado real antes da instalação. Delta:4ficheiros de execução,4originais fiscais inalterados,7marcadores compilados,manifesto/revisão e simuladorHTTP200. Comparação de dados/schema/ficheiros/configuração com identidadepostgres equivalente;apenas cache/sessions podem variar. Sem migrações/importações/recálculo de históricos;provasQA aceites reutilizadas.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
import {useState} from 'react';
import {closureSources} from '@/lib/iva-closure';
import {substitutionBook,saveSubstitutionBook,newSubstitution,previousDeclarations,recordSubstitution,substitutionReportRows,type SubstitutionBook,type SubstitutionInput} from '@/lib/iva-substitution';
export function AcquisitionSubstitution({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let b:SubstitutionBook;try{b=substitutionBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
const save=(next:SubstitutionBook)=>{try{onChange('modelo_a_snapshot',saveSubstitutionBook(draft,next));setError('');}catch(e){setError(String(e));}};
const change=(id:string,p:Partial<SubstitutionInput>)=>save({...b,rows:b.rows.map(r=>r.id===id?{...r,...p}:r)});
const previous=previousDeclarations(draft,b),sources=closureSources(draft);
return<details className="rounded border p-3"><summary>L3-C — substituições e diferencial</summary>
<p>Escolha a declaração anterior e o resultado corrigido do mesmo período. Créditos utilizados e pagamentos anteriores conservam-se no histórico;registe apenas pagamentos adicionais.</p>
<p>O diferencial credor aguarda comunicação dos serviços para utilização posterior no campo20. Não compensa automaticamente dívida nem cria reporte26. A preparação não submete à AT;formulário de substituição e formalidades permanecem fora do InvesteMoz.</p>
{error&&<p role="alert">{error}</p>}
<button type="button" onClick={()=>save({...b,rows:[...b.rows,newSubstitution(crypto.randomUUID())]})}>Preparar substituição</button>
{b.rows.map(r=><fieldset className="rounded border p-3" key={r.id}><legend>Substituição — {r.prior?.period||'período por escolher'}</legend>
<label>Declaração anterior congelada<select value={previous.findIndex(p=>JSON.stringify(p)===JSON.stringify(r.prior))} onChange={e=>{const p=previous[Number(e.target.value)];if(p){const matches=sources.filter(s=>s.period===p.period);change(r.id,{prior:structuredClone(p),...(r.priorDate?{}:{priorDate:p.declarationDate}),...(r.priorReference?{}:{priorReference:p.declarationReference}),...(r.source===null&&matches.length===1?{source:structuredClone(matches[0])}:{})});}}}><option value={-1}>Escolha a versão anterior</option>{previous.filter(p=>p.id!==r.id).map(p=><option key={JSON.stringify(p)} value={previous.indexOf(p)}>{p.period} — {p.entityRef} — {p.kind}/{p.id} — {p.calculatedAt}</option>)}</select></label>
{r.prior&&<p>Anterior:{r.prior.entityRef};{r.prior.origin==='projected'?'pressuposto de simulação':'dados efectivos declarados'};pagamentos e reporte conservados na versão escolhida.</p>}
<label>Resultado L2 corrigido congelado<select value={sources.findIndex(s=>JSON.stringify(s)===JSON.stringify(r.source))} onChange={e=>{const s=sources[Number(e.target.value)];if(s)change(r.id,{source:structuredClone(s)});}}><option value={-1}>Escolha a versão corrigida</option>{sources.filter(s=>!r.prior||s.period===r.prior.period).map(s=><option key={JSON.stringify(s)} value={sources.indexOf(s)}>{s.period} — revisão{s.revision} — {s.calculatedAt}</option>)}</select></label>
<label>Referência da declaração anterior<input value={r.priorReference} onChange={e=>change(r.id,{priorReference:e.target.value})}/></label>
<label>Data da declaração anterior<input type="date" value={r.priorDate} onChange={e=>change(r.id,{priorDate:e.target.value})}/></label>
<label>Referência da substituição ou preparação<input value={r.reference} onChange={e=>change(r.id,{reference:e.target.value})}/></label>
<label>Data da substituição ou prevista<input type="date" value={r.date} onChange={e=>change(r.id,{date:e.target.value})}/></label>
<label>Motivo da substituição<textarea value={r.reason} onChange={e=>change(r.id,{reason:e.target.value})}/></label>
<label>Situação da substituição<select value={r.stage} onChange={e=>change(r.id,{stage:e.target.value as SubstitutionInput['stage']})}><option value="preparation">Em preparação — sem submissão</option><option value="presented">Apresentada fora do InvesteMoz — registo declarado</option></select></label>
<label>Juros adicionais23<select value={r.interest} onChange={e=>change(r.id,{interest:e.target.value as SubstitutionInput['interest']})}><option value="">Por identificar</option><option value="none">Sem juros adicionais — motivo identificado</option><option value="documented">Montante adicional identificado</option></select></label>
{r.interest==='documented'&&<label>Montante dos juros adicionais (MZN)<input value={r.interestAmount} onChange={e=>change(r.id,{interestAmount:e.target.value})}/></label>}
<label>Fonte ou motivo dos juros adicionais<input value={r.interestRef} onChange={e=>change(r.id,{interestRef:e.target.value})}/></label>
<h5>Pagamentos adicionais desta substituição</h5>
{r.payments.map(p=><fieldset key={p.id}><legend>Pagamento adicional</legend>{(['reference','date','amount'] as const).map(k=><label key={k}>{k==='reference'?'Referência do pagamento adicional':k==='date'?'Data do pagamento adicional':'Montante adicional pago (MZN)'}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>change(r.id,{payments:r.payments.map(v=>v.id===p.id?{...v,[k]:e.target.value}:v)})}/></label>)}<button type="button" onClick={()=>change(r.id,{payments:r.payments.filter(v=>v.id!==p.id)})}>Remover pagamento desta preparação</button></fieldset>)}
<button type="button" onClick={()=>change(r.id,{payments:[...r.payments,{id:crypto.randomUUID(),reference:'',date:'',amount:''}]})}>Registar pagamento adicional</button>
<button type="button" onClick={()=>save({...b,rows:b.rows.filter(v=>v.id!==r.id)})}>Remover substituição desta preparação</button>
</fieldset>)}
{b.rows.length>0&&<button type="button" onClick={()=>{try{save(recordSubstitution(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar substituições C</button>}
{substitutionReportRows(draft).map((r,i)=><p key={i}><strong>{r[1]}:</strong> {r[2]}<br/>{r[3]}</p>)}
</details>;
}
@@ -1,5 +1,6 @@
import { useState } from 'react';
import {AcquisitionClosure} from './acquisition-closure';
import {AcquisitionSubstitution} from './acquisition-substitution';
import {AcquisitionRegularizations} from './acquisition-regularizations';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { AcquisitionImportsExclusions } from './acquisition-imports-exclusions';
@@ -85,6 +86,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save(recordAcquisitions(draft,book)))}>Calcular e guardar versão do apuramento parcial</button>
<p>O apuramento01–18 conserva o seu resultado antes dos créditos. O fechoA/B abaixo reutiliza a versão congelada,sem ordem de pagamento,submissãoAT ou promessa de reembolso.</p>
<AcquisitionClosure draft={draft} onChange={onChange}/>
<AcquisitionSubstitution draft={draft} onChange={onChange}/>
{book.result&&<><p role="status">{acquisitionCurrent(draft,book)?'Resultado correspondente às entradas guardadas.':'Entradas ou origem alteradas: resultado histórico preservado; actualizar o cálculo.'}</p><dl>{acquisitionReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
}
+3 -2
View File
@@ -34,7 +34,8 @@ function inputKey(r:ClosureInput){return closureFingerprint(r);}
function financialKey(r:ClosureInput){const{payments:_,...rest}=r;return closureFingerprint(rest);}
function otherUses(d:Record<string,unknown>,b:ClosureBook):ClosureInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,book])=>{validate(book);return book.rows.filter(r=>r.origin==='effective');})];}
function duplicateCredits(d:Record<string,unknown>,b:ClosureBook):Set<string>{const uses=new Map<string,number>();for(const r of otherUses(d,b))for(const c of r.credits){const k=creditIdentity(r,c);uses.set(k,(uses.get(k)??0)+1);}return new Set([...uses].filter(([,n])=>n>1).map(([k])=>k));}
function bookKey(d:Record<string,unknown>,b:ClosureBook){const sources=closureSources(d);return closureFingerprint([b.rows,b.rows.map(r=>sources.some(s=>JSON.stringify(s)===JSON.stringify(r.source))),otherUses(d,b).map(r=>[normalize(r.entityRef),r.origin,r.period,r.credits.map(c=>creditIdentity(r,c)),r.payments.map(p=>paymentIdentity(r,p))])]);}
function substitutionPayments(d:Record<string,unknown>):string[]{const m=store(d).ivaSubstitutions as Record<string,{result?:{inputs:Array<{id:string;prior:{entityRef:string;origin:string}|null;payments:ClosurePayment[]}>;rows:Array<{id:string;status:string;paid:FiscalFraction|null}>};history?:Array<{inputs:Array<{id:string;prior:{entityRef:string;origin:string}|null;payments:ClosurePayment[]}>;rows:Array<{id:string;status:string;paid:FiscalFraction|null}>}>}>|undefined;return Object.entries(m??{}).flatMap(([id,b])=>[...(b.history??[]),...(b.result?[b.result]:[])].flatMap(c=>c.inputs.filter(r=>r.prior&&(id===scope(d)||r.prior.origin==='effective')&&c.rows.some(v=>v.id===r.id&&v.status==='determined'&&v.paid)).flatMap(r=>r.payments.map(p=>closureFingerprint([normalize(r.prior!.entityRef),r.prior!.origin,normalize(p.reference)])))));}
function bookKey(d:Record<string,unknown>,b:ClosureBook){const sources=closureSources(d);const values:unknown[]=[b.rows,b.rows.map(r=>sources.some(s=>JSON.stringify(s)===JSON.stringify(r.source))),otherUses(d,b).map(r=>[normalize(r.entityRef),r.origin,r.period,r.credits.map(c=>creditIdentity(r,c)),r.payments.map(p=>paymentIdentity(r,p))])];const reserved=substitutionPayments(d),conflicts=b.rows.flatMap(r=>r.payments.map(p=>paymentIdentity(r,p))).filter(k=>reserved.includes(k));if(conflicts.length)values.push(conflicts);return closureFingerprint(values);}
export function closureCurrent(d:Record<string,unknown>,b:ClosureBook):boolean{return !!b.result&&b.result.key===bookKey(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.filter(x=>x.id===r.id&&x.inputKey===inputKey(r)).length===1);}
export function adoptPreviousCredit(c:ClosureCredit,previous:ClosureResult):ClosureCredit{if(previous.status!=='determined'||!previous.fields['26'])throw Error('Fecho anterior indeterminado;sem crédito presumido.');return{...c,kind:'previous',period:previous.period,reference:`Fecho ${previous.id}`,previousId:previous.id,carryKey:previous.carryKey,amount:fractionPresentation(previous.fields['26']).decimal,date:''};}
export function recordClosure(d:Record<string,unknown>,b:ClosureBook,now=new Date().toISOString()):ClosureBook{
@@ -81,7 +82,7 @@ export function recordClosure(d:Record<string,unknown>,b:ClosureBook,now=new Dat
if(!fields['24']){stop('Juros23 por identificar:indique montante documentado/pressuposto ou inexistência expressa com motivo;sem taxa presumida.');continue;}
r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]);
const payments:FiscalFraction[]=[];
for(const p of row.payments){if(!p.reference.trim()||!validDate(p.date)||(puses.get(paymentIdentity(row,p))??0)>1){invalid='Pagamento sem referência/data ou repetido noutro período/declaração;conserve o documento e reconcilie a imputação.';break;}try{payments.push(decimalFraction(p.amount));}catch{invalid='Montante de pagamento por identificar;sem baixa presumida.';break;}}
for(const p of row.payments){if(!p.reference.trim()||!validDate(p.date)||(puses.get(paymentIdentity(row,p))??0)>1||substitutionPayments(d).includes(paymentIdentity(row,p))){invalid='Pagamento sem referência/data ou repetido noutro período/declaração;conserve o documento e reconcilie a imputação.';break;}try{payments.push(decimalFraction(p.amount));}catch{invalid='Montante de pagamento por identificar;sem baixa presumida.';break;}}
if(invalid){stop(invalid);continue;}
r.paid=addFractions(payments);r.outstanding=positiveFraction(subtractFractions(fields['24'],r.paid));r.overpayment=positiveFraction(subtractFractions(r.paid,fields['24']));
r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]);r.reason='Saldo calculado, pagamentos registados e crédito transportado distintos. Pagamento não altera22/25/26;excedente pago não cria crédito20 sem comunicação.21 não aplicável à primeira declaração;27zero pela opção expressa de reporte integral. '+(row.origin==='projected'?'Pressupostos de simulação,sem declaração ou pagamento efectuados pelo portal.':'Dados efectivos declarados;registo não certifica cobrança nem submissãoAT.');results.push(r);
+56
View File
@@ -0,0 +1,56 @@
import {closureBook,closureSources,closureFingerprint,type ClosureSource,type ClosurePayment} from './iva-closure.ts';
import {addFractions,subtractFractions,positiveFraction,decimalFraction,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const substitutionSource='L3-C: VAT Return.pdf SHA2569c0b99a8,p2/quadro9;RCIVA15,Decreto7/2008 PDF3/p101.21=(17actual−18actual)−(17anterior−18anterior);devedor→22;credor só utilizável em declaração posterior em prazo após comunicação20. Sem repetir19/20 ou pagamentos anteriores. Exemplar recebido assinala substituiçãoModeloA;RCIVA15 refereModeloB:correspondência de formulário/submissão não automatizada.';
export type PriorDeclaration={kind:'AB'|'C';id:string;period:string;entityRef:string;origin:'projected'|'effective';calculatedAt:string;declarationDate:string;declarationReference:string;fingerprint:string;debit:FiscalFraction;credit:FiscalFraction;paid:FiscalFraction;outstanding:FiscalFraction;carry:FiscalFraction;paymentRefs:string[]};
export type SubstitutionInput={id:string;prior:PriorDeclaration|null;source:ClosureSource|null;priorReference:string;priorDate:string;date:string;reference:string;reason:string;stage:'preparation'|'presented';interest:''|'none'|'documented';interestAmount:string;interestRef:string;payments:ClosurePayment[]};
export type SubstitutionResult={id:string;status:'determined'|'indeterminate';reason:string;inputKey:string;fields:Record<string,FiscalFraction|null>;pendingCredit:FiscalFraction|null;paid:FiscalFraction|null;outstanding:FiscalFraction|null;overpayment:FiscalFraction|null;totalPaid:FiscalFraction|null;totalOutstanding:FiscalFraction|null;source:string};
export type SubstitutionCalculation={calculatedAt:string;key:string;inputs:SubstitutionInput[];rows:SubstitutionResult[]};
export type SubstitutionBook={version:1;rows:SubstitutionInput[];result?:SubstitutionCalculation;history?:SubstitutionCalculation[]};
const zero=()=>fraction(0n),scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase();
const date=(v:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(v)&&!Number.isNaN(Date.parse(v))&&new Date(v).toISOString().slice(0,10)===v;
function store(d:Record<string,unknown>):Record<string,unknown>{const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books)throw Error('Livros fiscais não recuperáveis;originais preservados.');return s;}
function maps(d:Record<string,unknown>):Record<string,SubstitutionBook>{const s=store(d).ivaSubstitutions;if(s===undefined)return{};if(!s||typeof s!=='object'||Array.isArray(s))throw Error('Substituições não recuperáveis;originais preservados.');return s as Record<string,SubstitutionBook>;}
function validate(b:SubstitutionBook):void{if(!b||b.version!==1||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)))throw Error('Livro de substituições não recuperável;sem o substituir.');for(const r of b.rows)if(!r||!['id','priorReference','priorDate','date','reference','reason','interestAmount','interestRef'].every(k=>typeof r[k as keyof SubstitutionInput]==='string')||!['preparation','presented'].includes(r.stage)||!['','none','documented'].includes(r.interest)||!Array.isArray(r.payments)||r.payments.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof ClosurePayment]==='string')))throw Error('Entradas da substituição não recuperáveis;originais preservados.');}
export function substitutionBook(d:Record<string,unknown>):SubstitutionBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
export function saveSubstitutionBook(d:Record<string,unknown>,b:SubstitutionBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaSubstitutions:{...maps(d),[scope(d)]:b}});}
export function newSubstitution(id:string):SubstitutionInput{return{id,prior:null,source:null,priorReference:'',priorDate:'',date:'',reference:'',reason:'',stage:'preparation',interest:'',interestAmount:'',interestRef:'',payments:[]};}
export function previousDeclarations(d:Record<string,unknown>,b=substitutionBook(d)):PriorDeclaration[]{
const ab=closureBook(d),out:PriorDeclaration[]=[];
for(const calc of [...(ab.history??[]),...(ab.result?[ab.result]:[])])for(const r of calc.rows){const input=calc.inputs.find(i=>i.id===r.id);if(r.status!=='determined'||!input||!r.fields['17']||!r.fields['18']||!r.fields['26']||!r.paid||!r.outstanding)continue;out.push({kind:'AB',id:r.id,period:r.period,entityRef:r.entityRef,origin:r.origin,calculatedAt:calc.calculatedAt,declarationDate:input.submissionDate,declarationReference:'',fingerprint:closureFingerprint([input,r]),debit:r.fields['17'],credit:r.fields['18'],paid:r.paid,outstanding:r.outstanding,carry:r.fields['26'],paymentRefs:input.payments.map(p=>p.reference)});}
for(const calc of [...(b.history??[]),...(b.result?[b.result]:[])])for(const r of calc.rows){const input=calc.inputs.find(i=>i.id===r.id);if(r.status!=='determined'||!input?.prior||!input.source||!r.totalPaid||!r.totalOutstanding)continue;out.push({kind:'C',id:r.id,period:input.prior.period,entityRef:input.prior.entityRef,origin:input.prior.origin,calculatedAt:calc.calculatedAt,declarationDate:input.date,declarationReference:input.reference,fingerprint:closureFingerprint([input,r]),debit:input.source.debit!,credit:input.source.credit!,paid:r.totalPaid,outstanding:r.totalOutstanding,carry:input.prior.carry,paymentRefs:[...input.prior.paymentRefs,...input.payments.map(p=>p.reference)]});}
return out.filter((r,i,a)=>a.findIndex(s=>s.kind===r.kind&&s.id===r.id&&s.fingerprint===r.fingerprint)===i);
}
function available(d:Record<string,unknown>,b:SubstitutionBook,r:SubstitutionInput){return[previousDeclarations(d,b).some(p=>JSON.stringify(p)===JSON.stringify(r.prior)),closureSources(d).some(s=>JSON.stringify(s)===JSON.stringify(r.source))];}
function pastPaymentUses(d:Record<string,unknown>,b:SubstitutionBook):Array<{id:string;entity:string;origin:string;reference:string}>{return Object.entries({...maps(d),[scope(d)]:b}).flatMap(([context,book])=>[...(book.history??[]),...(book.result?[book.result]:[])].flatMap(calc=>calc.inputs.filter(r=>r.prior&&(context===scope(d)||r.prior.origin==='effective')&&calc.rows.some(v=>v.id===r.id&&v.status==='determined'&&v.paid)).flatMap(r=>r.payments.map(p=>({id:r.id,entity:norm(r.prior!.entityRef),origin:r.prior!.origin,reference:norm(p.reference)})))));}
function abPaymentKeys(d:Record<string,unknown>):string[]{const ab=store(d).ivaClosures as Record<string,ReturnType<typeof closureBook>>|undefined;return Object.entries(ab??{}).flatMap(([id,v])=>[...v.rows,...[...(v.history??[]),...(v.result?[v.result]:[])].flatMap(c=>c.inputs)].filter(r=>id===scope(d)||r.origin==='effective').flatMap(r=>r.payments.map(p=>JSON.stringify([norm(r.entityRef),r.origin,norm(p.reference)]))));}
function key(d:Record<string,unknown>,b:SubstitutionBook){const ab=abPaymentKeys(d),past=pastPaymentUses(d,b);return closureFingerprint([b.rows,b.rows.map(r=>available(d,b,r)),uses(d,b).map(r=>[r.prior?.entityRef,r.prior?.origin,r.prior?.kind,r.prior?.id,r.payments.map(p=>norm(p.reference))]),b.rows.map(r=>r.payments.map(p=>!!r.prior&&(ab.includes(JSON.stringify([norm(r.prior.entityRef),r.prior.origin,norm(p.reference)]))||past.some(v=>v.id!==r.id&&v.entity===norm(r.prior!.entityRef)&&v.origin===r.prior!.origin&&v.reference===norm(p.reference)))))]);}
export function substitutionCurrent(d:Record<string,unknown>,b:SubstitutionBook):boolean{return!!b.result&&b.result.key===key(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.some(s=>s.id===r.id&&s.inputKey===closureFingerprint(r)));}
function uses(d:Record<string,unknown>,b:SubstitutionBook):SubstitutionInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,v])=>{validate(v);return v.rows.filter(r=>r.prior?.origin==='effective');})];}
export function recordSubstitution(d:Record<string,unknown>,b:SubstitutionBook,now=new Date().toISOString()):SubstitutionBook{
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identidade de substituição repetida.');
const all=uses(d,b),paymentsAB=abPaymentKeys(d);
const rows:SubstitutionResult[]=b.rows.map(row=>{
const fields:Record<string,FiscalFraction|null>=Object.fromEntries(['17','18','19','20','21','22','23','24','25','26','27'].map(k=>[k,null]));
const result:SubstitutionResult={id:row.id,status:'indeterminate',reason:'',inputKey:closureFingerprint(row),fields,pendingCredit:null,paid:null,outstanding:null,overpayment:null,totalPaid:null,totalOutstanding:null,source:substitutionSource};
const stop=(reason:string)=>({...result,reason});const prior=row.prior,source=row.source;
if(!prior||!source||!available(d,b,row).every(Boolean))return stop('Escolha declaração anterior e resultadoL2 congelados recuperáveis;sem fabricar ou recalcular origens.');
if(d.currency!=='MZN'||source.currency!=='MZN'||source.opportunity!==scope(d)||source.period!==prior.period||!/^2026-(0[1-9]|1[0-2])$/.test(prior.period)||!source.debit||!source.credit)return stop('A substituição exige mesmo âmbito e período2026/MZN,com17/18 determinados.');
if(!prior.entityRef.trim()||prior.origin==='effective'&&source.projected)return stop('Origem projectada não serve como declaração efectiva;conservar pressupostos identificados.');
const seen=new Set([row.id]);let ancestor:PriorDeclaration|null=prior;while(ancestor?.kind==='C'){if(seen.has(ancestor.id))break;seen.add(ancestor.id);ancestor=b.rows.find(r=>r.id===ancestor!.id)?.prior??null;}if(ancestor?.kind==='C'&&seen.has(ancestor.id))return stop('Sequência circular de substituições;ligue cada versão à anterior sem ciclos.');
if(all.filter(r=>r.prior&&norm(r.prior.entityRef)===norm(prior.entityRef)&&r.prior.origin===prior.origin&&r.prior.kind===prior.kind&&r.prior.id===prior.id).length>1)return stop('Declaração anterior já ligada a outra substituição;reconcilie a sequência sem duplicar o diferencial.');
if(!row.priorReference.trim()||row.priorDate!==prior.declarationDate||prior.declarationReference&&row.priorReference!==prior.declarationReference||!date(row.priorDate)||row.priorDate.slice(0,7)<prior.period||!date(row.date)||row.date<row.priorDate||!row.reference.trim()||!row.reason.trim())return stop('Reutilize a data/referência disponível da versão anterior;identifique referência/data da declaração anterior e da substituição,com motivo;preparação não equivale a submissão.');
if(BigInt(source.debit.numerator)<0n||BigInt(source.credit.numerator)<0n||BigInt(source.debit.numerator)>0n&&BigInt(source.credit.numerator)>0n)return stop('Sinais17/18 incompatíveis na origemL2.');
fields['17']=source.debit;fields['18']=source.credit;
fields['21']=subtractFractions(subtractFractions(source.debit,source.credit),subtractFractions(prior.debit,prior.credit));fields['22']=positiveFraction(fields['21']);result.pendingCredit=positiveFraction(subtractFractions(zero(),fields['21']));
if(row.interest==='none'&&row.interestRef.trim())fields['23']=zero();else if(row.interest==='documented'&&row.interestRef.trim())try{fields['23']=decimalFraction(row.interestAmount);}catch{/* unknown preserved */}
if(!fields['23'])return stop('Juros adicionais por identificar;sem copiar juros anteriores ou presumir taxa/prazo.');
fields['24']=addFractions([fields['22'],fields['23']]);
const paid:FiscalFraction[]=[];
for(const p of row.payments){const ident=JSON.stringify([norm(prior.entityRef),prior.origin,norm(p.reference)]);if(!p.reference.trim()||!date(p.date)||p.date<row.date||prior.paymentRefs.some(ref=>norm(ref)===norm(p.reference))||pastPaymentUses(d,b).some(v=>v.id!==row.id&&v.entity===norm(prior.entityRef)&&v.origin===prior.origin&&v.reference===norm(p.reference))||paymentsAB.includes(ident)||all.flatMap(r=>r.payments.filter(p2=>r.prior&&JSON.stringify([norm(r.prior.entityRef),r.prior.origin,norm(p2.reference)])===ident)).length>1)return stop('Pagamento já registado na anterior/outra declaração,sem referência/data aplicável;registe só pagamentos adicionais.');try{paid.push(decimalFraction(p.amount));}catch{return stop('Montante adicional não identificado;sem baixa presumida.');}}
result.paid=addFractions(paid);result.outstanding=positiveFraction(subtractFractions(fields['24'],result.paid));result.overpayment=positiveFraction(subtractFractions(result.paid,fields['24']));result.totalPaid=addFractions([prior.paid,result.paid]);result.totalOutstanding=addFractions([prior.outstanding,result.outstanding]);result.status='determined';result.reason='Diferencial21 separado da declaração anterior;19/20 não reutilizados,25/26/27 não atribuídos por esta substituição. Crédito credor aguarda comunicação20;sem compensar dívida anterior automaticamente. Pagamentos adicionais e saldo anterior separados;sem submissãoAT ou certificação de cobrança. '+(row.stage==='preparation'?'Preparação/simulação;referências e datas declaradas não comprovam entrega.':'Apresentação registada pelo utilizador fora do InvesteMoz.');return result;
});
const result={calculatedAt:now,key:key(d,b),inputs:structuredClone(b.rows),rows};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};
}
export function substitutionReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=substitutionBook(d);if(!b.rows.length)return[];const out:Array<[string,string,string,string]>=[['IVA mensal','L3-C — substituições',substitutionCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/origens',substitutionSource]];const fmt=(v:FiscalFraction|null|undefined)=>v?fractionPresentation(v).decimal:'Indeterminado';for(const r of b.result?.rows??[]){const input=b.result!.inputs.find(i=>i.id===r.id),p=input?.prior;out.push(['IVA mensal',`Substituição — ${p?.period??'por identificar'}`,`21:${fmt(r.fields['21'])};22 adicional:${fmt(r.fields['22'])};23 adicional:${fmt(r.fields['23'])};24 adicional:${fmt(r.fields['24'])};crédito aguardando comunicação:${fmt(r.pendingCredit)}`,`${p?.entityRef??''};${scope(d)};MZN;${p?.origin==='effective'?'Dados efectivos declarados':'Pressuposto de simulação'};anterior ${input?.priorReference??''},${input?.priorDate??''};substituição ${input?.reference??''},${input?.date??''};${input?.reason??''};${r.reason}`]);out.push(['IVA mensal',`Reconciliação da substituição — ${p?.period??''}`,`Anterior:pago${fmt(p?.paid)},por liquidar${fmt(p?.outstanding)},reporte histórico26:${fmt(p?.carry)};adicional:pago${fmt(r.paid)},por liquidar${fmt(r.outstanding)},excedente${fmt(r.overpayment)};total registado${fmt(r.totalPaid)},total por liquidar sem compensação${fmt(r.totalOutstanding)}`,`Origem anterior${p?.kind}/${p?.id},versão${p?.calculatedAt};origemL2 revisão${input?.source?.revision},${input?.source?.calculatedAt};pagamentos adicionais:${input?.payments.map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'nenhum'};juros adicionais:${input?.interestRef??''};histórico${b.result!.calculatedAt};${substitutionSource}`]);}return out;}catch{return[['IVA mensal','L3-C','Substituição não recuperável','Originais preservados;sem diferencial/crédito presumidos.']];}}
+2
View File
@@ -16,6 +16,7 @@ import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSet
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { acquisitionReportRows } from './iva-acquisitions.ts';
import {closureReportRows} from './iva-closure.ts';
import {substitutionReportRows} from './iva-substitution.ts';
import { importExemptionReportRows } from './iva-import-exemptions.ts';
import { publicBaseReportRows } from './iva-public-authority-base.ts';
import { commissionBaseReportRows } from './iva-commission-base.ts';
@@ -648,6 +649,7 @@ export function buildProjectDossierRows(
}
rows.push(...acquisitionReportRows(draft));
rows.push(...closureReportRows(draft));
rows.push(...substitutionReportRows(draft));
rows.push(...importExemptionReportRows(draft));
rows.push(...publicBaseReportRows(draft));
rows.push(...commissionBaseReportRows(draft));
@@ -0,0 +1,31 @@
<?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 IvaSubstitutionDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_substitution_recovers_frozen_predecessor_difference_payments_history_and_all_prior_maps(): 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-l3-c-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']);
$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['ivaSubstitutions']['opportunity:373'];$this->assertCount(1,$b['rows']);$this->assertCount(1,$b['history']);
$r=$b['result']['rows'][0];$this->assertSame('determined',$r['status']);foreach(['17'=>'220','18'=>'0','21'=>'60','22'=>'60','23'=>'1','24'=>'61']as $f=>$v)$this->assertSame(['numerator'=>$v,'denominator'=>'1'],$r['fields'][(string)$f]);
foreach(['19','20','25','26','27']as $f)$this->assertNull($r['fields'][(string)$f]);
foreach(['paid'=>'20','outstanding'=>'41','totalPaid'=>'50','totalOutstanding'=>'83']as $f=>$v)$this->assertSame(['numerator'=>$v,'denominator'=>'1'],$r[$f]);
$input=$b['result']['inputs'][0];$this->assertSame('SYNTHETIC QA declaraçãoMarço',$input['priorReference']);$this->assertSame('2026-04-15',$input['priorDate']);$this->assertSame('SYNTHETIC QA reciboP2',$input['payments'][0]['reference']);
$prior=$s['ivaClosures']['opportunity:373']['result']['rows'][2];$this->assertSame(['numerator'=>'72','denominator'=>'1'],$prior['fields']['24']);$this->assertSame(['numerator'=>'30','denominator'=>'1'],$prior['paid']);$this->assertSame(['numerator'=>'42','denominator'=>'1'],$prior['outstanding']);
$accepted=json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l3c-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$accepted=json_decode($accepted['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$this->assertSame($accepted['ivaClosures'],$s['ivaClosures']);
$frozen=json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l3ab-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,$s[$key][$context]);}
file_put_contents(base_path('outputs/iva-l3-c-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
+61
View File
@@ -0,0 +1,61 @@
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,saveAcquisitionBook,acquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {closureBook,closureCurrent,closureSources,closureReportRows,recordClosure} from '../../resources/js/lib/iva-closure.ts';
import {newSubstitution,previousDeclarations,substitutionBook,recordSubstitution,saveSubstitutionBook,substitutionCurrent,substitutionReportRows} from '../../resources/js/lib/iva-substitution.ts';
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3c-preserved.json',import.meta.url),'utf8'));
const old=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3ab-preserved.json',import.meta.url),'utf8'));
function fixture(){
const d=structuredClone(frozen),base=JSON.parse(old.modelo_a_snapshot),snap=JSON.parse(d.modelo_a_snapshot);for(const[k,v]of Object.entries(base))if(k!=='version')snap[k]={...v,...snap[k]};d.modelo_a_snapshot=JSON.stringify(snap);
const sales=snap.salesOperations['opportunity:373'];sales.rows[2].amount='1375';d.modelo_a_snapshot=saveSalesBook(d,recordSales(d,sales));d.modelo_a_snapshot=saveAcquisitionBook(d,recordAcquisitions(d,acquisitionBook(d),'2026-10-25T12:30:00.000Z'));
const prior=previousDeclarations(d).find(p=>p.period==='2026-03'),source=closureSources(d).filter(s=>s.period==='2026-03').at(-1);
const row={...newSubstitution('sub-Mar'),prior,source,priorReference:'SYNTHETIC QA declaraçãoMarço',priorDate:'2026-04-15',reference:'SYNTHETIC QA substituiçãoMarço',date:'2026-05-10',reason:'SYNTHETIC QA operação de Março corrigida',interest:'documented',interestAmount:'1',interestRef:'SYNTHETIC QA juros adicionais identificados',payments:[{id:'payment2',reference:'SYNTHETIC QA reciboP2',date:'2026-05-11',amount:'20'}]};
return{d,b:{version:1,rows:[row]}};
}
const value=(r,f)=>r.fields[f]&&`${r.fields[f].numerator}/${r.fields[f].denominator}`;
function alternative(d,debit,credit){const s=JSON.parse(d.modelo_a_snapshot),calc=structuredClone(s.books['opportunity:373'].result);calc.revision+=10;calc.calculatedAt+='-unit';const p=calc.periods.find(p=>p.period==='2026-03');p.fields['17']=String(debit);p.fields['18']=String(credit);p.exactFields['17']={numerator:String(debit),denominator:'1'};p.exactFields['18']={numerator:String(credit),denominator:'1'};s.books['opportunity:373'].history.push(calc);d.modelo_a_snapshot=JSON.stringify(s);return closureSources(d).find(p=>p.revision===calc.revision&&p.period==='2026-03');}
test('real corrected L2 version gives additional60 plus1;original72/30/42 and credits remain unchanged',()=>{
const{d,b}=fixture(),before=closureBook(d),r=recordSubstitution(d,b).result.rows[0];assert.equal(r.status,'determined',r.reason);assert.equal(value(r,'21'),'60/1');assert.equal(value(r,'22'),'60/1');assert.equal(value(r,'24'),'61/1');assert.deepEqual(r.paid,{numerator:'20',denominator:'1'});assert.deepEqual(r.outstanding,{numerator:'41',denominator:'1'});assert.deepEqual(r.totalOutstanding,{numerator:'83',denominator:'1'});assert.deepEqual(r.totalPaid,{numerator:'50',denominator:'1'});for(const f of ['19','20','25','26','27'])assert.equal(r.fields[f],null);
d.modelo_a_snapshot=saveSubstitutionBook(d,recordSubstitution(d,b));assert.deepEqual(closureBook(d),before);assert.ok(closureCurrent(d,before));assert.match(closureReportRows(d).map(r=>r.join(' ')).join(' '),/Calculado24:72\/1;pagamentos registados:30\/1;por liquidar:42\/1/);
});
test('quadro9 four sign cases use exact difference;credit waits for communication and never offsets original debt',()=>{
for(const [priorKind,actual17,actual18,expected]of [['debit',220,0,'60/1'],['credit',20,0,'120/1'],['credit',0,80,'20/1'],['debit',0,40,'-200/1']]){
const{d,b}=fixture();if(priorKind==='credit'){const jan=previousDeclarations(d).find(p=>p.period==='2026-01');b.rows[0].prior={...jan};b.rows[0].prior.period='2026-03';// use an actual frozen credit-origin version in March below
const snap=JSON.parse(d.modelo_a_snapshot),ab=snap.ivaClosures['opportunity:373'],calc=structuredClone(ab.result),r=calc.rows[2];r.fields['17']={numerator:'0',denominator:'1'};r.fields['18']={numerator:'100',denominator:'1'};r.id='unit-credit-Mar';calc.inputs[2].id=r.id;ab.history.push(calc);d.modelo_a_snapshot=JSON.stringify(snap);b.rows[0].prior=previousDeclarations(d).find(p=>p.id==='unit-credit-Mar');
}
b.rows[0].source=alternative(d,actual17,actual18);const r=recordSubstitution(d,b).result.rows[0];assert.equal(r.status,'determined',r.reason);assert.equal(value(r,'21'),expected);if(expected.startsWith('-')){assert.equal(value(r,'22'),'0/1');assert.deepEqual(r.pendingCredit,{numerator:'200',denominator:'1'});assert.equal(value(r,'26'),null);assert.equal(r.totalOutstanding.numerator,'42');}
}
});
test('missing facts,wrong period,currency,unknown source and interest never produce presumed zero',()=>{
const{d,b}=fixture();for(const patch of [{prior:null},{source:null},{priorReference:''},{priorDate:''},{date:'2026-04-01'},{reference:''},{reason:''},{interest:''}]){const bad=structuredClone(b);Object.assign(bad.rows[0],patch);assert.equal(recordSubstitution(d,bad).result.rows[0].status,'indeterminate',JSON.stringify(patch));}
const bad=structuredClone(b);bad.rows[0].source=closureSources(d).find(s=>s.period==='2026-02');assert.equal(recordSubstitution(d,bad).result.rows[0].fields['21'],null);assert.equal(recordSubstitution({...d,currency:'USD'},b).result.rows[0].status,'indeterminate');
});
test('old payment cannot be re-used and two substitutions cannot repeat one predecessor or payment',()=>{
const{d,b}=fixture();b.rows[0].payments[0].reference='SYNTHETIC QA reciboP1';assert.match(recordSubstitution(d,b).result.rows[0].reason,/Pagamento já/);
const f=fixture();f.b.rows.push({...structuredClone(f.b.rows[0]),id:'copy'});assert.ok(recordSubstitution(f.d,f.b).result.rows.every(r=>r.status==='indeterminate'));
const g=fixture();g.b.rows[0].payments.push({...g.b.rows[0].payments[0],id:'copy'});assert.equal(recordSubstitution(g.d,g.b).result.rows[0].paid,null);
});
test('successive substitution links frozen predecessor,charges only new difference and preserves all payments',()=>{
const{d,b}=fixture();let book=recordSubstitution(d,b,'2026-10-25T12:31:00Z');d.modelo_a_snapshot=saveSubstitutionBook(d,book);const next=structuredClone(book.rows[0]);next.id='sub-Mar-2';next.prior=previousDeclarations(d).find(p=>p.kind==='C');next.source=alternative(d,250,0);next.priorReference=next.reference;next.priorDate=next.date;next.reference='SYNTHETIC QA segunda substituição';next.date='2026-06-10';next.interest='none';next.interestRef='SYNTHETIC QA sem juros adicionais';next.payments=[];book=recordSubstitution(d,{...book,rows:[...book.rows,next]});const r=book.result.rows[1];assert.equal(r.status,'determined',r.reason);assert.equal(value(r,'21'),'30/1');assert.deepEqual(r.totalOutstanding,{numerator:'113',denominator:'1'});assert.deepEqual(r.totalPaid,{numerator:'50',denominator:'1'});assert.equal(book.history.length,1);
next.payments=[{id:'reuse',reference:'SYNTHETIC QA reciboP2',date:'2026-06-11',amount:'20'}];assert.equal(recordSubstitution(d,{...book,rows:[book.rows[0],next]}).result.rows[1].paid,null);
});
test('save/recover retains every L1/L2/AB map;report uses frozen inputs and recalculation appends history',()=>{
const{d,b}=fixture(),before=JSON.parse(d.modelo_a_snapshot);let book=recordSubstitution(d,b);d.modelo_a_snapshot=saveSubstitutionBook(d,book);assert.ok(substitutionCurrent(d,substitutionBook(d)));const stored=JSON.parse(d.modelo_a_snapshot);for(const[k,v]of Object.entries(before))assert.deepEqual(stored[k],v,k);
book.rows[0].reference='new deliberate edit';assert.equal(substitutionCurrent(d,book),false);d.modelo_a_snapshot=saveSubstitutionBook(d,book);const report=substitutionReportRows(d).map(r=>r.join(' ')).join(' ');assert.match(report,/SYNTHETIC QA substituiçãoMarço/);assert.doesNotMatch(report,/new deliberate edit/);
const oldResult=structuredClone(book.result);book=recordSubstitution(d,book);assert.deepEqual(book.history.at(-1),oldResult);d.modelo_a_snapshot=saveSubstitutionBook(d,book);assert.ok(substitutionCurrent(d,book));assert.match(substitutionReportRows(d).map(r=>r.join(' ')).join(' '),/new deliberate edit/);
});
test('fractional delta remains exact and overpayment does not generate communicated credit',()=>{
const{d,b}=fixture();const s=alternative(d,220,0),snap=JSON.parse(d.modelo_a_snapshot),calc=snap.books['opportunity:373'].history.at(-1);calc.periods[2].exactFields['17']={numerator:'661',denominator:'3'};d.modelo_a_snapshot=JSON.stringify(snap);b.rows[0].source=closureSources(d).find(x=>x.revision===s.revision&&x.period==='2026-03');b.rows[0].payments[0].amount='100';const r=recordSubstitution(d,b).result.rows[0];assert.equal(value(r,'21'),'181/3');assert.deepEqual(r.overpayment,{numerator:'116',denominator:'3'});assert.equal(r.pendingCredit.numerator,'0');assert.equal(r.fields['20'],null);
});
test('a payment already frozen in C cannot be added again in A/B;original valid A/B remains current',()=>{
const{d,b}=fixture();d.modelo_a_snapshot=saveSubstitutionBook(d,recordSubstitution(d,b));const ab=closureBook(d);assert.ok(closureCurrent(d,ab));ab.rows[2].payments.push({...b.rows[0].payments[0]});assert.equal(closureCurrent(d,ab),false);const r=recordClosure(d,ab).result.rows[2];assert.equal(r.paid,null);assert.match(r.reason,/Pagamento/);
});
test('a newly conflicting payment in recovered AB makes C stale and requires reconciliation',()=>{
const{d,b}=fixture();const book=recordSubstitution(d,b);d.modelo_a_snapshot=saveSubstitutionBook(d,book);assert.ok(substitutionCurrent(d,book));const s=JSON.parse(d.modelo_a_snapshot);s.ivaClosures['opportunity:373'].rows[2].payments.push({...b.rows[0].payments[0]});d.modelo_a_snapshot=JSON.stringify(s);assert.equal(substitutionCurrent(d,book),false);assert.equal(recordSubstitution(d,book).result.rows[0].paid,null);
});
test('a substitution cannot link back through a circular predecessor chain',()=>{
const{d,b}=fixture();let book=recordSubstitution(d,b,'2026-10-25T12:31:00Z');d.modelo_a_snapshot=saveSubstitutionBook(d,book);const child={...structuredClone(book.rows[0]),id:'child',prior:previousDeclarations(d).find(p=>p.kind==='C'),priorReference:book.rows[0].reference,priorDate:book.rows[0].date,date:'2026-06-10',reference:'child-ref',payments:[]};book=recordSubstitution(d,{...book,rows:[...book.rows,child]},'2026-10-25T12:32:00Z');d.modelo_a_snapshot=saveSubstitutionBook(d,book);book.rows[0].prior=previousDeclarations(d).find(p=>p.kind==='C'&&p.id==='child');assert.ok(recordSubstitution(d,book).result.rows.every(r=>/circular/.test(r.reason)));
});
test('export synthetic demonstrated correction for guarded private API and actual component proof',()=>{
const{d,b}=fixture();const result=recordSubstitution(d,b,'2026-10-25T12:31:00.000Z');const again=recordSubstitution(d,result,'2026-10-25T12:32:00.000Z');d.modelo_a_snapshot=saveSubstitutionBook(d,again);assert.ok(substitutionCurrent(d,again));if(process.env.IVA_L3C_CALCULATED)fs.writeFileSync(process.env.IVA_L3C_CALCULATED,JSON.stringify(d));
});