feat(iva): prepare beverage restitution QA and retain residual scope

This commit is contained in:
2026-10-25 15:08:10 +02:00
parent 46944402da
commit 4708662532
21 changed files with 418 additions and 21 deletions
@@ -27200,5 +27200,11 @@
100000
]
}
},
"l3_e_qa_20261025": {
"report": "docs\\investmoz\\simulations\\l3-e-beverages-qa-20261025.json",
"status": "QA_operacional_demonstrada_nao_instalada",
"installed_CD": "de36f028",
"receipt_CD": "46944402"
}
}
@@ -0,0 +1,165 @@
{
"status": "QA_operacional_demonstrada_nao_instalada",
"installed_CD": "de36f02880adf2337edf9f08c3eb424424adcca0",
"receipt_CD": "46944402",
"coverage": "CIVA13(2), condições próprias c/d/h, linha documental integral exclusiva, pedido/decisão/recebimento; sem isenção directa",
"source": {
"file": "source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
"pdf_page": 12,
"printed_page": "1426(270)",
"articles": "13(1)c/d/h e 13(2–3)"
},
"proof": {
"documentary_VAT": "16.00",
"deduction_this_route": "0.00",
"recognised": "12.00",
"received": "7.00",
"waiting_receipt": "5.00",
"not_granted": "4.00",
"currency": "MZN",
"frontend_distinct_tests": 27,
"API_tests": 1,
"API_assertions": 109,
"types": "passed",
"build": "passed",
"UI": "outputs/iva-l3-e-20261025/ui-recovered-beverages.png",
"database": "QA isolada 55441, transacções revertidas"
},
"preservation": {
"L1_delivered": 129,
"L1_functional_pending": [
"P058",
"P084",
"P085"
],
"P001": "05/06",
"all_prior_snapshot_maps_equal_after_UI": true,
"original_sources_unchanged": 17,
"schema_changes": 0,
"active_data_writes_for_E": 0
},
"runtime_delta": [
{
"path": "resources/js/components/acquisition-beverage-restitution.tsx",
"sha256_LF": "b91f2a0ecf2c939b3a52b7084465a79ccb31aa27a016676f5ccd63e02ab70587"
},
{
"path": "resources/js/components/acquisition-imports-exclusions.tsx",
"sha256_LF": "bb1324ec71722cb50bdf068ac23c3a871673f2cdf3feae8453496cde6a194043"
},
{
"path": "resources/js/components/acquisition-operation-link.tsx",
"sha256_LF": "f08de2c463b4c68c67a61545aece89c2fd6bda8eee6614b625b5ed5f4c86e4be"
},
{
"path": "resources/js/components/fiscal-claim-events.tsx",
"sha256_LF": "cf3a9c9edce64726e62369a3a89ff59ef66d9a17fe3c6a341b46b5699186207f"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "d3ab598e6798962ac9dc6f95419b40cfcd4de76529e8dbdb47e908c535497072"
},
{
"path": "resources/js/lib/fiscal-claim-events.ts",
"sha256_LF": "b1f8e9288989239871ead179fc0934935c72a675551c7d7831a8ad8ef7a89b80"
},
{
"path": "resources/js/lib/iva-acquisition-bc.ts",
"sha256_LF": "e805a6a9134e851d490d26ce8fc367c6014f4c025a14345352e8987889117861"
},
{
"path": "resources/js/lib/iva-acquisitions.ts",
"sha256_LF": "cce283b1d0a07fb383fcb527225f4dd5bec829010a662cbaae1d60e595221977"
},
{
"path": "resources/js/lib/iva-beverage-parcels.ts",
"sha256_LF": "3af5afa57faa0f014f59b00152ddd7a2fddd308c5c7a82d8e755022647d278ce"
},
{
"path": "resources/js/lib/iva-beverage-restitution.ts",
"sha256_LF": "3d34613146e19715aa0bae7a1472a2eecd5aac53b7552b2a3186225ae46351bc"
},
{
"path": "resources/js/lib/iva-refund.ts",
"sha256_LF": "37398e02e5710d18f8ab3c61b4c9de8f03640eedebda70eef995aa1be7328903"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "af4e387d1a98195d8e8d5d00d408f67b7dc09f04ebff1041edfd94d89fcc2b98"
}
],
"remaining": [
{
"ids": "L3-E entrega",
"tipo": "entrega",
"accao": "Integrar a candidata QA pelo processo Git, com comparação fresca, backup/restauro e conferência apenas do delta.",
"fecho": "Revisão instalada e recibo; C/D e históricos preservados."
},
{
"ids": "L3-E cobertura adicional",
"tipo": "código e parâmetros documentais específicos",
"accao": "Tratar repartição dentro da mesma linha e reconciliação de dedução efectiva anterior, pelas regularizações aplicáveis. A via demonstrada usa uma linha documental integral exclusiva.",
"fecho": "Parcelas conciliadas, sem benefício duplicado, gravação/recuperação/relatório e totais coerentes."
},
{
"ids": "L3-C/D/E formalidades",
"tipo": "dependência documental delimitada e implementação posterior",
"accao": "Mapear edição e campos do formulário de substituição; exemplares regulamentares do reembolso geral; procedimento/formulário específico de restituição de bebidas. Não transportar automaticamente limiares ou prazos de D para E.",
"fecho": "Correspondência demonstrada, preparação/exportação aplicável e referência à submissão externa; pedido, decisão e recebimento separados."
},
{
"ids": "Apuramento fiscal ainda indeterminado",
"tipo": "código e dados aplicáveis",
"accao": "Completar os cálculos ainda limitados dos fornecimentos, margem/viagens/eventos, quando aplicáveis. Na prova E, os 16 MZN são IVA documental da aquisição; o IVA da operação L1 continua indeterminado.",
"fecho": "Apuramento suportado pela regra e dados próprios, sem copiar IVA de aquisição como IVA devido nem substituir desconhecido por zero."
},
{
"ids": "L2 — 18.º(6) com afectação mista",
"tipo": "código",
"accao": "Combinar base reduzida e afectação parcial reutilizando as proporções existentes.",
"fecho": "Cálculo exacto, sem dupla afectação, totais afectados e histórico preservado."
},
{
"ids": "L2 — sucessivas/plurianuais",
"tipo": "código e parâmetros aplicáveis",
"accao": "Encadear eventos e anos por activo a partir da dedução congelada.",
"fecho": "Diferenciais por evento/ano, sem repetição nem reescrita de históricos."
},
{
"ids": "L2 — erros/incobráveis",
"tipo": "código e condições documentais específicas",
"accao": "Separar fundamentos, documentos, momento e recuperação posterior.",
"fecho": "Regularizações rastreáveis, sem duplicar rectificações da declaração."
},
{
"ids": "P058",
"tipo": "três questões documentais delimitadas; código subsequente",
"accao": "Resolver a remissão de 57.º(2)b do Decreto 37/2023, a fronteira exacta dos 180 dias de propriedade e 63.º(2), arma de caça/até 100 cartuchos, no âmbito dos beneficiários CIVA12.º(2)a. Preservar os percursos já entregues.",
"fecho": "Condições explícitas dos três casos, cálculo quando aplicável, gravação/recuperação/relatório."
},
{
"ids": "P084/P085",
"tipo": "correspondência documental de formulários e código de preenchimento/exportação",
"accao": "Identificar o formulário aplicável a cada finalidade, distinguindo aquisição interna e importação. O percurso Tempo pedido → decisão → formulário com despacho já está entregue; manter o dossier de apoio disponível.",
"fecho": "Exemplar e instruções ligados aos campos de cada cenário, reutilização de dados e despacho limitado aos bens/operações/períodos/condições autorizados."
},
{
"ids": "P001",
"tipo": "dependência documental externa da edição recebida",
"accao": "Resolver exclusivamente a correspondência dos campos 05/06, sem preenchimento presumido nem usar outra edição como equivalência automática.",
"fecho": "Instrução aplicável à edição recebida e mapeamento comprovado de 05/06."
},
{
"ids": "Formalidades digitais dos ramos entregues",
"tipo": "documentação e automatização ainda limitada",
"accao": "Conservar visíveis as formalidades não automatizadas e a submissão fora do portal.",
"fecho": "Cobertura demonstrada por procedimento, sem declarar cobertura digital integral."
}
],
"limits": [
"Sem submissão AT nem execução bancária automática.",
"Guarda contra duplicação nos livros/históricos recuperados; não equivale a reconciliação central de todos os dossiers ou da AT.",
"Via demonstrada 2026/MZN; sem extensão automática a outras condições ou períodos.",
"Estatísticas e IA adiadas pela Tempo."
]
}
@@ -0,0 +1,59 @@
{
"status": "prepared_QA_not_installed",
"baseline": "de36f028",
"report": "docs\\investmoz\\simulations\\l3-e-beverages-qa-20261025.json",
"runtime_delta": [
{
"path": "resources/js/components/acquisition-beverage-restitution.tsx",
"sha256_LF": "b91f2a0ecf2c939b3a52b7084465a79ccb31aa27a016676f5ccd63e02ab70587"
},
{
"path": "resources/js/components/acquisition-imports-exclusions.tsx",
"sha256_LF": "bb1324ec71722cb50bdf068ac23c3a871673f2cdf3feae8453496cde6a194043"
},
{
"path": "resources/js/components/acquisition-operation-link.tsx",
"sha256_LF": "f08de2c463b4c68c67a61545aece89c2fd6bda8eee6614b625b5ed5f4c86e4be"
},
{
"path": "resources/js/components/fiscal-claim-events.tsx",
"sha256_LF": "cf3a9c9edce64726e62369a3a89ff59ef66d9a17fe3c6a341b46b5699186207f"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "d3ab598e6798962ac9dc6f95419b40cfcd4de76529e8dbdb47e908c535497072"
},
{
"path": "resources/js/lib/fiscal-claim-events.ts",
"sha256_LF": "b1f8e9288989239871ead179fc0934935c72a675551c7d7831a8ad8ef7a89b80"
},
{
"path": "resources/js/lib/iva-acquisition-bc.ts",
"sha256_LF": "e805a6a9134e851d490d26ce8fc367c6014f4c025a14345352e8987889117861"
},
{
"path": "resources/js/lib/iva-acquisitions.ts",
"sha256_LF": "cce283b1d0a07fb383fcb527225f4dd5bec829010a662cbaae1d60e595221977"
},
{
"path": "resources/js/lib/iva-beverage-parcels.ts",
"sha256_LF": "3af5afa57faa0f014f59b00152ddd7a2fddd308c5c7a82d8e755022647d278ce"
},
{
"path": "resources/js/lib/iva-beverage-restitution.ts",
"sha256_LF": "3d34613146e19715aa0bae7a1472a2eecd5aac53b7552b2a3186225ae46351bc"
},
{
"path": "resources/js/lib/iva-refund.ts",
"sha256_LF": "37398e02e5710d18f8ab3c61b4c9de8f03640eedebda70eef995aa1be7328903"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "af4e387d1a98195d8e8d5d00d408f67b7dc09f04ebff1041edfd94d89fcc2b98"
}
],
"migrations": 0,
"imports": 0,
"accepted_CD_proofs": "reutilizadas; 13 testes D conferem apenas o auxiliar partilhado alterado",
"delivery": "Conferência fresca de main, backup/restauro e delta instalado pelo processo Git existente."
}
@@ -1,5 +1,31 @@
# Rastreabilidade, utilização e formação
## 25/10/2026 — L3 C/D entregues; E em QA; trabalho restante
C/D instalados em `de36f028`, recibo `46944402`. E permanece em QA, sem instalação. Reutilizar a aquisição e a classificação L1/L2; seleccionar a via exclusiva da linha documental e registar pedido, decisão e recebimentos. A prova guardada/recuperada/relatada conserva 16 MZN de IVA documental, zero de dedução nesta via, 12 reconhecidos, 7 recebidos e 5 por receber. Não representa apuramento do IVA do fornecimento, concessão automática ou pagamento bancário. As condições c/d/h permanecem distintas; a pesca costeira não herda o tratamento. Históricos conservam reservas e deduções anteriores; editar ou apagar a preparação actual não liberta uma parcela já utilizada.
27 testes frontend específicos, API 1/109 asserções em QA isolada com rollback, TypeScript e build passaram. Todos os mapas anteriores e os originais oficiais foram preservados. Relatório: [L3-E e pendências](../simulations/l3-e-beverages-qa-20261025.json).
Trabalho restante, na mesma matriz:
- **L3-E entrega — entrega**: Integrar a candidata QA pelo processo Git, com comparação fresca, backup/restauro e conferência apenas do delta. Critério de fecho: Revisão instalada e recibo; C/D e históricos preservados.
- **L3-E cobertura adicional — código e parâmetros documentais específicos**: Tratar repartição dentro da mesma linha e reconciliação de dedução efectiva anterior, pelas regularizações aplicáveis. A via demonstrada usa uma linha documental integral exclusiva. Critério de fecho: Parcelas conciliadas, sem benefício duplicado, gravação/recuperação/relatório e totais coerentes.
- **L3-C/D/E formalidades — dependência documental delimitada e implementação posterior**: Mapear edição e campos do formulário de substituição; exemplares regulamentares do reembolso geral; procedimento/formulário específico de restituição de bebidas. Não transportar automaticamente limiares ou prazos de D para E. Critério de fecho: Correspondência demonstrada, preparação/exportação aplicável e referência à submissão externa; pedido, decisão e recebimento separados.
- **Apuramento fiscal ainda indeterminado — código e dados aplicáveis**: Completar os cálculos ainda limitados dos fornecimentos, margem/viagens/eventos, quando aplicáveis. Na prova E, os 16 MZN são IVA documental da aquisição; o IVA da operação L1 continua indeterminado. Critério de fecho: Apuramento suportado pela regra e dados próprios, sem copiar IVA de aquisição como IVA devido nem substituir desconhecido por zero.
- **L2 — 18.º(6) com afectação mista — código**: Combinar base reduzida e afectação parcial reutilizando as proporções existentes. Critério de fecho: Cálculo exacto, sem dupla afectação, totais afectados e histórico preservado.
- **L2 — sucessivas/plurianuais — código e parâmetros aplicáveis**: Encadear eventos e anos por activo a partir da dedução congelada. Critério de fecho: Diferenciais por evento/ano, sem repetição nem reescrita de históricos.
- **L2 — erros/incobráveis — código e condições documentais específicas**: Separar fundamentos, documentos, momento e recuperação posterior. Critério de fecho: Regularizações rastreáveis, sem duplicar rectificações da declaração.
- **P058 — três questões documentais delimitadas; código subsequente**: Resolver a remissão de 57.º(2)b do Decreto 37/2023, a fronteira exacta dos 180 dias de propriedade e 63.º(2), arma de caça/até 100 cartuchos, no âmbito dos beneficiários CIVA12.º(2)a. Preservar os percursos já entregues. Critério de fecho: Condições explícitas dos três casos, cálculo quando aplicável, gravação/recuperação/relatório.
- **P084/P085 — correspondência documental de formulários e código de preenchimento/exportação**: Identificar o formulário aplicável a cada finalidade, distinguindo aquisição interna e importação. O percurso Tempo pedido → decisão → formulário com despacho já está entregue; manter o dossier de apoio disponível. Critério de fecho: Exemplar e instruções ligados aos campos de cada cenário, reutilização de dados e despacho limitado aos bens/operações/períodos/condições autorizados.
- **P001 — dependência documental externa da edição recebida**: Resolver exclusivamente a correspondência dos campos 05/06, sem preenchimento presumido nem usar outra edição como equivalência automática. Critério de fecho: Instrução aplicável à edição recebida e mapeamento comprovado de 05/06.
- **Formalidades digitais dos ramos entregues — documentação e automatização ainda limitada**: Conservar visíveis as formalidades não automatizadas e a submissão fora do portal. Critério de fecho: Cobertura demonstrada por procedimento, sem declarar cobertura digital integral.
L1 conserva 129 entregues + P058/P084/P085 + P001 = 133. L2 mantém as três famílias residuais; L3 permanece aberto. Não repetir pesquisas sem nova evidência nem provas aceites sem delta relevante. Estatísticas e IA continuam adiadas.
## 25/10/2026 — inventário anterior aL3-E
CD entregue de36f028/46944402;AB72/30/42,C/D eL1129fechos/quatroresiduais preservados,sem perdas confirmadas. E reutiliza documentos/classificação13(2) deL1/L2,parcela exclusivamente para dedução ou restituição. Cotejo delimitadoLei13/2016 PDF12/art13(2–3);formalidades,procedimento e formulário específicos não demonstrados nesta ronda. Não transportar automaticamente condições/prazos do reembolso geralD. L2três famílias e residuaisL1 mantidos.
## 25/10/2026 — entrega conjunta L3-C/D
Instalado de36f028,candidata9e2d90c8,incluiCb7a745e6;109cb0fe preservada na ancestralidade ebackup/restauro anterior. Uma integraçãoGit,backup novo e restauroDocker real isolado semrede antes de instalar. Delta:9ficheiros de execução,4originais fiscais inalterados,7marcadores compilados,manifesto/revisão e simuladorHTTP200. Comparação174tabelas e369ficheiros/configuração com mesma identidadepostgres;apenas cache/sessions podem variar. Sem migrações/importações ou recálculo de históricos;provasQA aceites reutilizadas.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
import {useState} from 'react';
import {beverageRestitutionBook,beverageRestitutionSources,newBeverageRestitution,saveBeverageRestitutionBook,recordBeverageRestitution,beverageRestitutionReportRows,type BeverageRestitutionBook,type BeverageRestitutionInput} from '@/lib/iva-beverage-restitution';
import {FiscalClaimEventsFields} from './fiscal-claim-events';
export function AcquisitionBeverageRestitution({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let b:BeverageRestitutionBook;try{b=beverageRestitutionBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
const save=(next:BeverageRestitutionBook)=>{try{onChange('modelo_a_snapshot',saveBeverageRestitutionBook(draft,next));setError('');}catch(e){setError(String(e));}},sources=beverageRestitutionSources(draft);
const change=(id:string,p:Partial<BeverageRestitutionInput>)=>save({...b,rows:b.rows.map(r=>r.id===id?{...r,...p}:r)});
return<details className="rounded border p-3"><summary>L3-E — restituição de bebidas13(2)</summary>
<p>Reutiliza bebidas de abastecimento de bordo13(1)(c/d/h),com documento e vínculoL1/L2. Escolha restituição na aquisição e recalculeL2:esta linha não gera dedução nem crédito para reembolso geral. Não se aplica a qualquer despesa com bebidas,tabaco ou recepção.</p>
<p>Âmbito:linha documental integral exclusivamente para restituição,2026/MZN. Parcelas mistas ou correcção de dedução efectiva anterior exigem reconciliação própria;históricos preservados. Pedido,decisão e recebimento são registos distintos. Procedimento/formulário específico e prazos não automatizados;submissão fora doInvesteMoz,sem condições importadas do reembolso geralD.</p>
{error&&<p role="alert">{error}</p>}
<button type="button" onClick={()=>save({...b,rows:[...b.rows,newBeverageRestitution(crypto.randomUUID(),sources.length===1?sources[0]:null,String(draft.fiscal_entity_ref??draft.organization_name??''))]})}>Preparar restituição de bebidas</button>
{b.rows.map(r=><fieldset key={r.id} className="rounded border p-3"><legend>Restituição — {r.source?.period||'origem por escolher'}</legend>
<label>Linha documental destinada à restituição<select value={sources.findIndex(s=>s.parcel===r.source?.parcel&&s.fingerprint===r.source?.fingerprint)} onChange={e=>{const s=sources[Number(e.target.value)];if(s)change(r.id,{source:structuredClone(s)});}}><option value={-1}>Escolha origem actualL1/L2</option>{sources.map((s,i)=><option key={s.parcel} value={i}>{s.documentRef} — linha{s.documentLine||'única'} — {s.period}</option>)}</select></label>
{r.source&&<p>Documento:{r.source.documentRef};{r.source.documentDate};fornecedor{r.source.supplier};operaçãoL1{r.source.operationId};{r.source.operationRef}. Montante pedido reutiliza o IVA identificado da linha,sem taxa presumida.</p>}
<label>Âmbito fiscal da entidade<input value={r.entityRef} onChange={e=>change(r.id,{entityRef:e.target.value})}/></label>
<label>Linha integral exclusivamente para restituição,sem dedução da mesma parcela<select value={r.exclusive} onChange={e=>change(r.id,{exclusive:e.target.value as BeverageRestitutionInput['exclusive']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não — reconciliar parcela</option></select></label>
<FiscalClaimEventsFields value={r} onChange={p=>change(r.id,p)}/>
<button type="button" onClick={()=>save({...b,rows:b.rows.filter(q=>q.id!==r.id)})}>Remover restituição desta preparação</button>
</fieldset>)}
{b.rows.length>0&&<button type="button" onClick={()=>{try{save(recordBeverageRestitution(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar restituição E</button>}
{beverageRestitutionReportRows(draft).map((r,i)=><p key={i}><strong>{r[1]}:</strong> {r[2]}<br/>{r[3]}</p>)}
</details>;
}
@@ -23,7 +23,7 @@ export function AcquisitionImportsExclusions({draft,row,onChange}:{draft:Record<
{b.expense==='fuel'&&<>{fact('resale','Combustível adquirido para revenda,com actividade e destino identificados20(3)(a)')}{b.facts.resale==='no'&&<><label>Combustível<select value={b.fuelKind} onChange={e=>update({fuelKind:e.target.value as AcquisitionBC['fuelKind']})}><option value="">Por identificar</option><option value="diesel">Gasóleo</option><option value="other">Outro combustível normalmente utilizável em viaturas</option></select></label>{b.fuelKind==='diesel'&&<><label>Utilização do gasóleo<select value={b.dieselUse} onChange={e=>update({dieselUse:e.target.value as AcquisitionBC['dieselUse']})}><option value="">Por identificar</option><option value="heavy_passengers">Veículo pesado de passageiros</option><option value="public_passengers">Veículo licenciado para transporte público de passageiros,excepto rent a car</option><option value="machine">Máquina consumidora de gasóleo que não é veículo matriculado</option><option value="agriculture">Tractor com emprego exclusivo/predominante em operações inerentes à actividade agrícola</option><option value="other">Outro uso — dedução50%</option></select></label>{b.dieselUse&&b.dieselUse!=='other'&&fact('dieselScope','O uso e as características correspondem integralmente à categoria20(1)(b)(i–iv) seleccionada')}</>}</>}</>}
{b.expense==='lodging_food'&&fact('commercialTraveller','Alojamento/alimentação de viajante comercial,agindo por conta própria,no quadro da actividade profissional20(3)(b)')}
{b.expense==='telephone'&&fact('ownLandline','Serviço de telefone fixo em nome do sujeito passivo20(1)(e)')}
{b.expense==='beverages'&&<>{fact('beverageSpecial','Bebidas de abastecimento de bordo nas alíneas13(1)(c/d/h),com origemL1 identificada;tabaco e recepção não estão abrangidos por esta excepção')}{b.facts.beverageSpecial==='yes'&&<><label>Via13(2) para esta parcela<select value={b.beveragePath} onChange={e=>update({beveragePath:e.target.value as AcquisitionBC['beveragePath']})}><option value="">Por escolher</option><option value="deduction">Dedução nesta aquisição — L2</option><option value="refund">Restituição — percursoL3,sem apuramento aqui</option></select></label>{fact('onlyDeduction','Parcela identificada exclusivamente para dedução;sem pedido/restituição da mesma parcela')}{fact('notCoastalFishing','Se origem13(1)(d):embarcação de salvamento/assistência marítima,sem ser pesca costeira cujas provisões ficam excluídas')}</>}</>}
{b.expense==='beverages'&&<>{fact('beverageSpecial','Bebidas de abastecimento de bordo nas alíneas13(1)(c/d/h),com origemL1 identificada;tabaco e recepção não estão abrangidos por esta excepção')}{b.facts.beverageSpecial==='yes'&&<><label>Via13(2) para esta parcela<select value={b.beveragePath} onChange={e=>update({beveragePath:e.target.value as AcquisitionBC['beveragePath']})}><option value="">Por escolher</option><option value="deduction">Dedução nesta aquisição — L2</option><option value="refund">RestituiçãoL3-E — sem dedução desta parcela</option></select></label>{b.beveragePath==='deduction'&&fact('onlyDeduction','Parcela identificada exclusivamente para dedução;sem pedido/restituição da mesma parcela')}{fact('notCoastalFishing','Se origem13(1)(d):embarcação de salvamento/assistência marítima,sem ser pesca costeira cujas provisões ficam excluídas')}</>}</>}
<label className="block">Factos e referência documental da classificação/excepção<input value={b.evidence} onChange={e=>update({evidence:e.target.value})}/></label>
</fieldset>;
}
@@ -5,12 +5,12 @@ export function AcquisitionOperationLink({draft,row,onChange}:{draft:Record<stri
const choices=acquisitionOperationChoices(draft),link=row.deductionLink;
const adopt=(id:string)=>onChange({deductionLink:createAcquisitionRightLink(draft,id)});
const patch=(value:Partial<AcquisitionRightLink>)=>{if(link)onChange({deductionLink:{...link,...value}});};
const result=link?(row.bc?.expense==='beverages'&&row.bc.facts.beverageSpecial==='yes'&&row.bc.beveragePath==='deduction'?linkedBeverageDeductionRight(draft,link,row.bc.facts.notCoastalFishing==='yes'):linkedOperationRight(draft,link)):null;
const result=link?(row.bc?.expense==='beverages'&&row.bc.facts.beverageSpecial==='yes'&&['deduction','refund'].includes(row.bc.beveragePath)?linkedBeverageDeductionRight(draft,link,row.bc.facts.notCoastalFishing==='yes'):linkedOperationRight(draft,link)):null;
return <fieldset className="space-y-2 rounded border p-3"><legend>Aquisição → direito à dedução — L2-A</legend>
<p>Reutilize a classificação existente. A ligação conserva montantes, documento e escolhas da aquisição; não cria outra compra. Importações, despesas especiais e afectação parcial conservam enquadramento próprio.</p>
{choices.length===1&&!link?<button type="button" onClick={()=>adopt(choices[0].id)}>Reutilizar operação existente: {choices[0].label}</button>:<label>Operação de destino<select value={link?.operationId??''} onChange={e=>e.target.value?adopt(e.target.value):onChange({deductionLink:undefined})}><option value="">Sem vínculo L2-A</option>{choices.map(c=><option key={c.id} value={c.id}>{c.label}</option>)}</select></label>}
{!choices.length&&<p>Não há operação classificada nesta oportunidade. A compra e o histórico permanecem disponíveis.</p>}
{link&&<><p>{result?.reason}</p><small>{result?.source}</small>
{link&&<><p>{row.bc?.beveragePath==='refund'&&result?.status==='eligible'?'Condições da operação13(2) identificadas;via de restituição escolhida,sem dedução desta parcela.':result?.reason}</p><small>{result?.source}</small>
<label className="block">Afectação exclusiva à operação ligada<select value={link.scopeConfirmed} onChange={e=>patch({scopeConfirmed:e.target.value as AcquisitionRightLink['scopeConfirmed']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / por rever</option></select></label>
<label className="block">Motivo ou referência da afectação<input value={link.allocationRef} onChange={e=>patch({allocationRef:e.target.value})}/></label>
{result?.status==='eligible'&&<label className="block">Aquisição fora dos pagamentos excluídos15(6)(c)/19(2)<select value={link.noExcludedPayments} onChange={e=>patch({noExcludedPayments:e.target.value as AcquisitionRightLink['noExcludedPayments']})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>}
@@ -0,0 +1,9 @@
import type {FiscalClaimEvents} from '@/lib/fiscal-claim-events';
export function FiscalClaimEventsFields({value:r,onChange}:{value:FiscalClaimEvents;onChange:(p:Partial<FiscalClaimEvents>)=>void}){return<>
<label>Referência do pedido ou preparação<input value={r.requestReference} onChange={e=>onChange({requestReference:e.target.value})}/></label><label>Data do pedido ou prevista<input type="date" value={r.requestDate} onChange={e=>onChange({requestDate:e.target.value})}/></label>
<label>Situação do pedido<select value={r.stage} onChange={e=>onChange({stage:e.target.value as FiscalClaimEvents['stage']})}><option value="preparation">Em preparação — sem submissão</option><option value="submitted">Apresentado fora doInvesteMoz — registo declarado</option></select></label>
<label>Decisão recebida<select value={r.decision} onChange={e=>onChange({decision:e.target.value as FiscalClaimEvents['decision']})}><option value="pending">Ainda não identificada</option><option value="approved">Deferimento integral</option><option value="partial">Deferimento parcial</option><option value="rejected">Indeferimento</option></select></label>
{r.decision!=='pending'&&<><label>Número ou referência da decisão<input value={r.decisionReference} onChange={e=>onChange({decisionReference:e.target.value})}/></label><label>Data da decisão<input type="date" value={r.decisionDate} onChange={e=>onChange({decisionDate:e.target.value})}/></label><label>Entidade emitente da decisão<input value={r.decisionIssuer} onChange={e=>onChange({decisionIssuer:e.target.value})}/></label><label>Montante reconhecido na decisão (MZN)<input value={r.approvedAmount} onChange={e=>onChange({approvedAmount:e.target.value})}/></label></>}
{r.receipts.map(p=><fieldset key={p.id}><legend>Recebimento identificado</legend>{(['reference','date','amount']as const).map(k=><label key={k}>{k==='reference'?'Referência do recebimento':k==='date'?'Data do recebimento':'Montante recebido (MZN)'}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>onChange({receipts:r.receipts.map(q=>q.id===p.id?{...q,[k]:e.target.value}:q)})}/></label>)}<button type="button" onClick={()=>onChange({receipts:r.receipts.filter(q=>q.id!==p.id)})}>Remover recebimento desta preparação</button></fieldset>)}
<button type="button" onClick={()=>onChange({receipts:[...r.receipts,{id:crypto.randomUUID(),reference:'',date:'',amount:''}]})}>Registar recebimento identificado</button>
</>;}
@@ -2,6 +2,7 @@ import { useState } from 'react';
import {AcquisitionClosure} from './acquisition-closure';
import {AcquisitionSubstitution} from './acquisition-substitution';
import {AcquisitionRefund} from './acquisition-refund';
import {AcquisitionBeverageRestitution} from './acquisition-beverage-restitution';
import {AcquisitionRegularizations} from './acquisition-regularizations';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { AcquisitionImportsExclusions } from './acquisition-imports-exclusions';
@@ -89,6 +90,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<AcquisitionClosure draft={draft} onChange={onChange}/>
<AcquisitionSubstitution draft={draft} onChange={onChange}/>
<AcquisitionRefund draft={draft} onChange={onChange}/>
<AcquisitionBeverageRestitution 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>;
}
+17
View File
@@ -0,0 +1,17 @@
import {addFractions,subtractFractions,positiveFraction,decimalFraction,type FiscalFraction} from './fiscal-rational.ts';
export type FiscalClaimEvents={stage:'preparation'|'submitted';requestReference:string;requestDate:string;decision:'pending'|'approved'|'partial'|'rejected';decisionReference:string;decisionDate:string;decisionIssuer:string;approvedAmount:string;receipts:Array<{id:string;reference:string;date:string;amount:string}>};
export type FiscalClaimState={status:'determined'|'indeterminate';reason:string;approved:FiscalFraction|null;received:FiscalFraction|null;awaitingReceipt:FiscalFraction|null;notGranted:FiscalFraction|null};
export function newFiscalClaimEvents():FiscalClaimEvents{return{stage:'preparation',requestReference:'',requestDate:'',decision:'pending',decisionReference:'',decisionDate:'',decisionIssuer:'',approvedAmount:'',receipts:[]};}
export const fiscalClaimDate=(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;
export function assessFiscalClaim(row:FiscalClaimEvents,requested:FiscalFraction,duplicate:(p:FiscalClaimEvents['receipts'][number])=>boolean):FiscalClaimState{
const r:FiscalClaimState={status:'indeterminate',reason:'',approved:null,received:null,awaitingReceipt:null,notGranted:null},stop=(reason:string)=>({...r,reason});
if(!row.requestReference.trim()||!fiscalClaimDate(row.requestDate))return stop('Identifique referência/data do pedido ou preparação.');
if(row.stage==='preparation'&&(row.decision!=='pending'||row.receipts.length))return stop('Preparação não é pedido apresentado;registe apresentação externa antes de decisão/recebimento.');
if(row.decision==='pending'){if(row.receipts.length)return stop('Recebimento sem decisão identificada;conservar registo sem o presumir reconhecido.');return{...r,status:'determined',reason:'Pedido conservado;decisão e recebimento ainda não identificados.'};}
if(!row.decisionReference.trim()||!row.decisionIssuer.trim()||!fiscalClaimDate(row.decisionDate)||row.decisionDate<row.requestDate)return stop('Identifique decisão recebida,número/data e entidade emitente;pedido não equivale a deferimento.');
let approved:FiscalFraction;try{approved=decimalFraction(row.approvedAmount);}catch{return stop('Montante reconhecido por identificar;indeferimento exigezero expresso,sem transformar desconhecido emzero.');}
if(BigInt(subtractFractions(approved,requested).numerator)>0n||row.decision==='approved'&&JSON.stringify(approved)!==JSON.stringify(requested)||row.decision==='rejected'&&BigInt(approved.numerator)!==0n||row.decision==='partial'&&(BigInt(approved.numerator)<=0n||BigInt(subtractFractions(requested,approved).numerator)<=0n))return stop('Montante reconhecido incompatível com decisão/pedido;conservar o âmbito exacto,sem generalizar o deferimento.');
r.approved=approved;r.notGranted=subtractFractions(requested,approved);const receipts:FiscalFraction[]=[];
for(const p of row.receipts){if(!p.reference.trim()||!fiscalClaimDate(p.date)||p.date<row.decisionDate||duplicate(p))return stop('Recebimento sem referência/data posterior à decisão ou já registado noutro pedido;sem duplicar caixa.');try{const value=decimalFraction(p.amount);if(BigInt(value.numerator)<=0n)return stop('Recebimento exige montante positivo identificado.');receipts.push(value);}catch{return stop('Montante recebido por identificar;decisão não cria recebimento.');}}
r.received=addFractions(receipts);if(BigInt(subtractFractions(r.received,approved).numerator)>0n)return stop('Recebimentos excedem o montante reconhecido;reconcilie sem criar crédito ou caixa adicional.');r.awaitingReceipt=positiveFraction(subtractFractions(approved,r.received));return{...r,status:'determined',reason:'Pedido,decisão e recebimento separados;sem execução bancária ou sincronização silenciosa de caixa.'};
}
+3 -2
View File
@@ -12,7 +12,7 @@ export type AcquisitionBC={version:1;expense:string;reducedSupply:Choice;evidenc
export function newAcquisitionBC():AcquisitionBC{return{version:1,expense:'',reducedSupply:'',evidence:'',facts:{},fuelKind:'',dieselUse:'',beveragePath:'',importer:'',belongs:'',ownImportVat:'',vatBasis:'',vatRef:'',goodsRef:''};}
export function acquisitionBCDetails(b:AcquisitionBC):string{
const labels:Record<string,string>={exigibleInPeriod:'IVA aduaneiro exigível no período de exercício confirmado pelo documento',tourism:'Bem abrangido20(1)(a)',saleOrExploit:'Venda/exploração como objecto da actividade',resale:'Combustível para revenda',dieselScope:'Âmbito específico do uso de gasóleo confirmado',commercialTraveller:'Viajante comercial por conta própria',ownLandline:'Telefone fixo em nome do sujeito passivo',beverageSpecial:'Bebidas no âmbito13(2)',onlyDeduction:'Parcela exclusivamente para dedução',notCoastalFishing:'Sem provisões de pesca costeira'};
const keys:Record<string,string[]>={vehicle:['tourism',...(b.facts.tourism==='yes'?['saleOrExploit']:[])],fuel:['resale',...(b.fuelKind==='diesel'&&b.dieselUse!=='other'?['dieselScope']:[])],lodging_food:['commercialTraveller'],telephone:['ownLandline'],beverages:['beverageSpecial',...(b.facts.beverageSpecial==='yes'?['onlyDeduction','notCoastalFishing']:[])]};
const keys:Record<string,string[]>={vehicle:['tourism',...(b.facts.tourism==='yes'?['saleOrExploit']:[])],fuel:['resale',...(b.fuelKind==='diesel'&&b.dieselUse!=='other'?['dieselScope']:[])],lodging_food:['commercialTraveller'],telephone:['ownLandline'],beverages:['beverageSpecial',...(b.facts.beverageSpecial==='yes'?[...(b.beveragePath==='deduction'?['onlyDeduction']:[]),'notCoastalFishing']:[])]};
return [...(b.facts.exigibleInPeriod!==undefined?['exigibleInPeriod']:[]),...(keys[b.expense]??[])].map(k=>`${labels[k]}:${b.facts[k]==='yes'?'Sim':b.facts[k]==='no'?'Não':'Por indicar'}`).concat(b.expense==='fuel'?[`Combustível:${b.fuelKind==='diesel'?'Gasóleo':b.fuelKind==='other'?'Outro':'Por indicar'}`,`Utilização:${({heavy_passengers:'Pesado de passageiros',public_passengers:'Transporte público licenciado,excepto rent a car',machine:'Máquina,não veículo matriculado',agriculture:'Tractor agrícola,emprego exclusivo/predominante',other:'Outro uso50%','':'Por indicar'})[b.dieselUse]}`]:[],b.expense==='beverages'&&b.facts.beverageSpecial==='yes'?[`Via:${b.beveragePath==='deduction'?'DeduçãoL2':b.beveragePath==='refund'?'RestituiçãoL3':'Por indicar'}`]:[]).join('; ');
}
export function validAcquisitionBC(v:unknown):v is AcquisitionBC{
@@ -51,7 +51,7 @@ export function evaluateAcquisitionBC(row:Acquisition,currency:string,registered
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'&&!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(!partial&&bc.expense==='beverages'&&bc.facts.beverageSpecial==='yes'&&['deduction','refund'].includes(bc.beveragePath)&&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);
const noExcludedPayments=partial?.noExcludedPayments??row.deductionLink?.noExcludedPayments;
@@ -93,6 +93,7 @@ export function evaluateAcquisitionBC(row:Acquisition,currency:string,registered
case 'luxury':return stop('Despesa de divertimento/luxo20(1)(f),segundo natureza/montante e exploração normal declarados.','no_right',0);
case 'beverages':
if(bc.facts.beverageSpecial==='no')return stop('Bebidas/tabaco/recepção excluídos20(1)(d),fora do fundamento especial13(2).','no_right',0);
if(bc.facts.beverageSpecial==='yes'&&bc.beveragePath==='refund'&&right.reason.startsWith('Via de dedução13(2)'))return stop('Via restituição13(2):linha documental integral escolhida para acompanhamentoL3-E,sem dedução desta parcela. Não representa pedido apresentado,concessão ou recebimento.','no_right',0);
if(bc.facts.beverageSpecial==='yes'&&bc.beveragePath==='deduction'&&bc.facts.onlyDeduction==='yes'&&right.reason.startsWith('Via de dedução13(2)'))return stop(right.reason+' IVA suportado identificado neste documento;mesma parcela não segue restituiçãoL3.','eligible',100);
return stop('Via especial13(2) por delimitar:dedução ou restituiçãoL3,sem dupla contagem;tabaco/recepção não herdam a excepção de bebidas.');
}
+2
View File
@@ -9,6 +9,7 @@ import { acquisitionTreatmentIssue, acquisitionContinuityVersion, formerExplorat
import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.ts';
import {regularizationFields,regularizationSourceKey,regularizationReportRows,frozenAssetRows,validateRegularizationStore} from './iva-regularizations.ts';
import {beverageParcelReserved} from './iva-beverage-parcels.ts';
export const acquisitionVersion = 'modelo-a-internal-acquisitions/1';
export const acquisitionBasis = 'CIVA arts.18(1)(a),18(2) (Lei10/2025),18(3–5),19(1)(a),19(2),20 (Lei22/2022),21(1–3); âmbito interno, afectação exclusiva, mesmo período, sem exclusões ou dedução parcial.';
export const acquisitionSources = 'Modelo A DOCX 62c198e9 / v1 63d45fa8; VAT Return.pdf SHA256 9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741, p.1; CIVA republicado Lei13/2016, PDF14–16; Lei22/2022, art.20; Lei10/2025, arts.18/19, PDF13.';
@@ -172,6 +173,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(result.deductible!==null&&(result.deductibleExact?BigInt(result.deductibleExact.numerator)>0n:Number(result.deductible)>0)&&beverageParcelReserved(draft,row))return {...result,deductible:null,deductibleExact:undefined,nonDeductibleExact:undefined,reason:'Parcela ligada a pedido/decisão/recebimento de restituição13(2);sem a voltar a deduzir. Reconciliar o procedimento conservando o histórico.',eligibility:{status:'indeterminate' as const,reason:'Parcela reservada para restituição13(2).',source:acquisitionBCSource}};
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;
});
+4
View File
@@ -0,0 +1,4 @@
import type {Acquisition} from './iva-acquisitions.ts';
export function beverageParcel(row:Acquisition):string{return JSON.stringify([row.field==='11'?'customs':row.supplier.trim().toLocaleLowerCase(),row.documentRef.trim().toLocaleLowerCase(),row.documentDate,row.documentLine.trim()]);}
type Claim={source:{parcel:string;origin:string}|null;stage:string;decision:string;receipts:unknown[]};
export function beverageParcelReserved(d:Record<string,unknown>,row:Acquisition):boolean{const store=JSON.parse(String(d.modelo_a_snapshot||'{}')),map=store.ivaBeverageRestitutions as Record<string,{rows:Claim[];result?:{inputs:Claim[]};history?:Array<{inputs:Claim[]}>}>|undefined;return Object.entries(map??{}).some(([scope,b])=>[...b.rows,...[...(b.history??[]),...(b.result?[b.result]:[])].flatMap(c=>c.inputs)].some(r=>r.source&&(scope===String(d.context_opportunity||'general')||r.source.origin==='effective')&&r.source.origin===row.origin&&r.source.parcel===beverageParcel(row)&&(r.stage==='submitted'||r.decision!=='pending'||r.receipts.length>0)));}
@@ -0,0 +1,39 @@
import {acquisitionBook,acquisitionCurrent,type Acquisition} from './iva-acquisitions.ts';
import {beverageParcel} from './iva-beverage-parcels.ts';
import {closureFingerprint} from './iva-closure.ts';
import {assessFiscalClaim,newFiscalClaimEvents,fiscalClaimDate,type FiscalClaimEvents,type FiscalClaimState} from './fiscal-claim-events.ts';
import {decimalFraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const beverageRestitutionSource='L3-E:CIVA13(1)(c/d/h),13(2–3),Lei13/2016 PDF12/p1426(270). Bebidas:dedução ou restituição,sem aplicar20(1)(d);não isenção directa. Provisões de pesca costeira excluídas. Linha documental integral afectada exclusivamente à restituição;sem dedução da mesma parcela. Procedimento/formulário específico não automatizado;sem importar limites/prazos do reembolso geral21.';
export type BeverageRestitutionSource={parcel:string;acquisitionId:string;scope:string;period:string;origin:'projected'|'effective';documentRef:string;documentDate:string;documentLine:string;supplier:string;vat:FiscalFraction;fingerprint:string;operationId:string;operationRef:string;calculatedAt:string};
export type BeverageRestitutionInput=FiscalClaimEvents&{id:string;source:BeverageRestitutionSource|null;entityRef:string;exclusive:''|'yes'|'no'};
export type BeverageRestitutionResult=FiscalClaimState&{id:string;inputKey:string;requested:FiscalFraction|null;source:string};
type Calc={calculatedAt:string;key:string;inputs:BeverageRestitutionInput[];rows:BeverageRestitutionResult[]};
export type BeverageRestitutionBook={version:1;rows:BeverageRestitutionInput[];result?:Calc;history?:Calc[]};
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase();
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 não recuperáveis;originais preservados.');return s;}
function maps(d:Record<string,unknown>):Record<string,BeverageRestitutionBook>{const m=store(d).ivaBeverageRestitutions;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa de restituições não recuperável;originais preservados.');return m as Record<string,BeverageRestitutionBook>;}
function validate(b:BeverageRestitutionBook):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('LivroE não recuperável;originais preservados.');for(const r of b.rows)if(!r||!['id','entityRef','requestReference','requestDate','decisionReference','decisionDate','decisionIssuer','approvedAmount'].every(k=>typeof r[k as keyof BeverageRestitutionInput]==='string')||!['','yes','no'].includes(r.exclusive)||!['preparation','submitted'].includes(r.stage)||!['pending','approved','partial','rejected'].includes(r.decision)||!Array.isArray(r.receipts)||r.receipts.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof typeof p]==='string')))throw Error('EntradasE não recuperáveis;sem apagar documentos.');}
export function beverageRestitutionBook(d:Record<string,unknown>):BeverageRestitutionBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
export function saveBeverageRestitutionBook(d:Record<string,unknown>,b:BeverageRestitutionBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaBeverageRestitutions:{...maps(d),[scope(d)]:b}});}
export function beverageRestitutionSources(d:Record<string,unknown>):BeverageRestitutionSource[]{const b=acquisitionBook(d);if(!acquisitionCurrent(d,b))return[];return b.rows.flatMap(row=>{const result=b.result!.rows.find(r=>r.id===row.id);if(row.bc?.expense!=='beverages'||row.bc.beveragePath!=='refund'||!result?.reason.startsWith('Via restituição13(2)')||result.deductible!=='0.00'||!result.vat||!row.deductionLink)return[];const vat=decimalFraction(result.vat);if(BigInt(vat.numerator)<=0n)return[];return[{parcel:beverageParcel(row),acquisitionId:row.id,scope:scope(d),period:row.period,origin:row.origin,documentRef:row.documentRef,documentDate:row.documentDate,documentLine:row.documentLine,supplier:row.supplier,vat,fingerprint:closureFingerprint([row,result]),operationId:row.deductionLink.operationId,operationRef:row.deductionLink.allocationRef,calculatedAt:b.result!.calculatedAt}];});}
function deducted(d:Record<string,unknown>,s:BeverageRestitutionSource):boolean{const books=store(d).books as Record<string,ReturnType<typeof acquisitionBook>>;return Object.entries(books).some(([context,b])=>[...(b.history??[]),...(b.result?[b.result]:[])].some(calc=>calc.inputs.some(row=>beverageParcel(row)===s.parcel&&(row.origin==='effective'&&s.origin==='effective'||context===scope(d)&&calc===b.result)&&calc.rows.some(r=>r.id===row.id&&r.deductible!==null&&(r.deductibleExact?BigInt(r.deductibleExact.numerator)>0n:BigInt(decimalFraction(r.deductible).numerator)>0n)))));}
export function newBeverageRestitution(id:string,source:BeverageRestitutionSource|null,entityRef=''):BeverageRestitutionInput{return{...newFiscalClaimEvents(),id,source:source?structuredClone(source):null,entityRef,exclusive:''};}
function sameSource(d:Record<string,unknown>,r:BeverageRestitutionInput):boolean{return!!r.source&&beverageRestitutionSources(d).some(s=>s.scope===r.source!.scope&&s.parcel===r.source!.parcel&&s.fingerprint===r.source!.fingerprint)&&!deducted(d,r.source);}
function allUses(d:Record<string,unknown>,b:BeverageRestitutionBook):BeverageRestitutionInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,v])=>{validate(v);return v.rows.filter(r=>r.source?.origin==='effective');})];}
function past(d:Record<string,unknown>,b:BeverageRestitutionBook):BeverageRestitutionInput[]{return Object.entries({...maps(d),[scope(d)]:b}).flatMap(([context,v])=>[...(v.history??[]),...(v.result?[v.result]:[])].flatMap(c=>c.inputs.filter(r=>context===scope(d)||r.source?.origin==='effective')));}
const receiptKey=(r:BeverageRestitutionInput,p:FiscalClaimEvents['receipts'][number])=>JSON.stringify([norm(r.entityRef),r.source?.origin,norm(p.reference)]);
function key(d:Record<string,unknown>,b:BeverageRestitutionBook):string{return closureFingerprint([b.rows,b.rows.map(r=>sameSource(d,r)),allUses(d,b).map(r=>[r.id,r.source?.parcel,r.receipts.map(p=>receiptKey(r,p))]),past(d,b).filter(r=>r.stage==='submitted'||r.decision!=='pending'||r.receipts.length).map(r=>[r.id,r.source?.parcel,r.receipts.map(p=>receiptKey(r,p))])]);}
export function beverageRestitutionCurrent(d:Record<string,unknown>,b:BeverageRestitutionBook):boolean{return!!b.result&&b.result.key===key(d,b)&&b.rows.length===b.result.rows.length&&b.rows.every(r=>b.result!.rows.some(v=>v.id===r.id&&v.inputKey===closureFingerprint(r)));}
export function recordBeverageRestitution(d:Record<string,unknown>,b:BeverageRestitutionBook,now=new Date().toISOString()):BeverageRestitutionBook{
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('IdentidadeE repetida.');const uses=allUses(d,b),history=past(d,b);
const rows=b.rows.map(row=>{const r:BeverageRestitutionResult={id:row.id,inputKey:closureFingerprint(row),status:'indeterminate',reason:'',requested:null,approved:null,received:null,awaitingReceipt:null,notGranted:null,source:beverageRestitutionSource},stop=(reason:string)=>({...r,reason}),s=row.source;
if(!s||!sameSource(d,row)||d.currency!=='MZN'||s.scope!==scope(d))return stop('OrigemL1/L2 alterada,ausente ou parcela já deduzida em registo efectivo;sem restituição presumida.');
if(!row.entityRef.trim()||row.exclusive!=='yes')return stop('Identifique entidade e confirme a linha integral exclusivamente para restituição;sem dupla utilização.');
if(uses.filter(v=>v.source?.origin===s.origin&&v.source?.parcel===s.parcel).length>1||history.some(v=>v.id!==row.id&&v.source?.origin===s.origin&&v.source?.parcel===s.parcel&&(v.stage==='submitted'||v.decision!=='pending'||v.receipts.length)))return stop('Linha documental já ligada a outro pedido de restituição no âmbito recuperado;sem duplicar a parcela.');
r.requested=s.vat;if(!fiscalClaimDate(row.requestDate)||row.requestDate<s.documentDate)return stop('Data do pedido/preparação anterior ao documento ou por identificar.');
const events=assessFiscalClaim(row,s.vat,p=>uses.flatMap(v=>v.receipts.filter(q=>receiptKey(v,q)===receiptKey(row,p))).length>1||history.some(v=>v.id!==row.id&&v.receipts.some(q=>receiptKey(v,q)===receiptKey(row,p))));Object.assign(r,events);
if(r.status==='determined')r.reason+=' Linha13(2) fora da deduçãoL2 e do crédito geralD;pedido não é isenção directa nem concessão automática. Decisão e recebimento registados fora doInvesteMoz;procedimento/formulário específico e prazos não automatizados,sem os importar deD.';return r;
});
const result={calculatedAt:now,key:'',inputs:structuredClone(b.rows),rows};const next={...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};result.key=key(d,next);return next;
}
export function beverageRestitutionReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=beverageRestitutionBook(d);if(!b.rows.length)return[];const out:Array<[string,string,string,string]>=[['IVA mensal','L3-E — restituição de bebidas',beverageRestitutionCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever origem/parcela',beverageRestitutionSource]],fmt=(v:FiscalFraction|null)=>v?fractionPresentation(v).decimal:'Indeterminado';for(const r of b.result?.rows??[]){const i=b.result!.inputs.find(v=>v.id===r.id),s=i?.source;out.push(['IVA mensal',`Restituição de bebidas — ${s?.period??''}`,`ParcelaIVA:${fmt(r.requested)};dedução desta parcela:${r.requested?'0pela via exclusivaE':'Indeterminada;ver origem'};reconhecido:${fmt(r.approved)};recebido:${fmt(r.received)};aguarda recebimento:${fmt(r.awaitingReceipt)}`,`${i?.entityRef??''};${scope(d)};${s?.origin==='effective'?'Dados efectivos declarados':'Pressuposto de simulação'};${s?.documentRef??''};${s?.documentDate??''};linha${s?.documentLine??''};operaçãoL1${s?.operationId??''};${s?.operationRef??''};pedido${i?.requestReference??''},${i?.requestDate??''};decisão${i?.decisionReference??''},${i?.decisionDate??''},${i?.decisionIssuer??''};recibos:${i?.receipts.map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'nenhum registado'};${r.reason}`]);}return out;}catch{return[['IVA mensal','L3-E','Restituição não recuperável','Originais preservados;sem dedução/restituição presumidas.']];}}
+7 -14
View File
@@ -1,5 +1,6 @@
import {assessFiscalClaim} from './fiscal-claim-events.ts';
import {closureBook,closureCurrent,closureFingerprint} from './iva-closure.ts';
import {addFractions,subtractFractions,positiveFraction,decimalFraction,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
import {subtractFractions,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const refundSource='L3-D:CIVA21(5/6/7),Lei13/2016 PDF15–16,p1426(273–274);Lei22/2022 PDF3–4;Lei10/2025 PDF13/p2180(13),21(7a/8revogado/10/13/15). ModeloA/VAT Return9c0b99a8,p2/quadro11:26+27=25. Pedido,decisão e recebimento separados. Formalidades doRegulamento78/2017/30/2022/52/2025 não automatizadas;sem concessão automática.';
type Choice=''|'yes'|'no';
@@ -9,7 +10,7 @@ export type RefundInput={id:string;source:RefundSource|null;ground:''|'four_mont
export type RefundResult={id:string;status:'determined'|'indeterminate';reason:string;inputKey:string;eligibility:'compatible'|'not_demonstrated'|'incompatible';requested:FiscalFraction|null;reported:FiscalFraction|null;approved:FiscalFraction|null;received:FiscalFraction|null;awaitingReceipt:FiscalFraction|null;notGranted:FiscalFraction|null;source:string};
export type RefundCalculation={calculatedAt:string;key:string;inputs:RefundInput[];rows:RefundResult[]};
export type RefundBook={version:1;rows:RefundInput[];result?:RefundCalculation;history?:RefundCalculation[]};
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase(),zero=()=>fraction(0n);
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase();
const date=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(s)&&!Number.isNaN(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
const period=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])$/.test(s);
const months=(a:string,b:string)=>(Number(b.slice(0,4))-Number(a.slice(0,4)))*12+Number(b.slice(5))-Number(a.slice(5));
@@ -45,18 +46,10 @@ export function recordRefund(d:Record<string,unknown>,b:RefundBook,now=new Date(
else if(row.ground==='four_months'&&(months(row.originPeriod,s.period)<4||!above(100000n))||row.ground==='high_credit'&&!above(500000n)||row.ground==='cessation'&&(!date(row.cessationDate)||row.cessationDate>row.requestDate)){r.eligibility='incompatible';eligibilityReason='Factos declarados não demonstram21(6),quatro meses/crédito superior100000;21(7a),superior500000;ou21(7b),cessação identificada.';}
else r.eligibility='compatible';
}
if(row.stage==='preparation'&&(row.decision!=='pending'||row.receipts.length))return stop('Preparação não é pedido apresentado;registe apresentação externa antes de decisão/recebimento.');
if(row.decision==='pending'){
if(row.receipts.length)return stop('Recebimento sem decisão identificada;conservar registo sem o presumir reconhecido.');
if(eligibilityReason)return stop(eligibilityReason+' Pedido/estado conservados;formalidades regulamentares não automatizadas.');
r.status='determined';r.reason=row.stage==='preparation'?'Pedido em preparação;parcela separada do reporte,sem submissão,decisão ou recebimento.':'Pedido apresentado fora doInvesteMoz;decisão e recebimento ainda não identificados.';return r;
}
if(!row.decisionReference.trim()||!row.decisionIssuer.trim()||!date(row.decisionDate)||row.decisionDate<row.requestDate)return stop('Identifique decisão recebida,número/data e entidade emitente;pedido não equivale a deferimento.');
let approved:FiscalFraction;try{approved=decimalFraction(row.approvedAmount);}catch{return stop('Montante reconhecido por identificar;indeferimento exigezero expresso,sem transformar desconhecido emzero.');}
if(BigInt(subtractFractions(approved,s.requested).numerator)>0n||row.decision==='approved'&&JSON.stringify(approved)!==JSON.stringify(s.requested)||row.decision==='rejected'&&BigInt(approved.numerator)!==0n||row.decision==='partial'&&(BigInt(approved.numerator)<=0n||BigInt(subtractFractions(s.requested,approved).numerator)<=0n))return stop('Montante reconhecido incompatível com decisão/pedido;conservar o âmbito exacto,sem generalizar o deferimento.');
r.approved=approved;r.notGranted=subtractFractions(s.requested,approved);const receipts:FiscalFraction[]=[];
for(const p of row.receipts){const id=receiptIdentity(row,p);if(!p.reference.trim()||!date(p.date)||p.date<row.decisionDate||all.flatMap(v=>v.receipts.filter(q=>receiptIdentity(v,q)===id)).length>1||old.some(v=>v.id!==row.id&&v.receipts.some(q=>receiptIdentity(v,q)===id)))return stop('Recebimento sem referência/data posterior à decisão ou já registado noutro pedido;sem duplicar caixa.');try{const v=decimalFraction(p.amount);if(BigInt(v.numerator)<=0n)return stop('Recebimento exige montante positivo identificado.');receipts.push(v);}catch{return stop('Montante recebido por identificar;decisão não cria recebimento.');}}
r.received=addFractions(receipts);if(BigInt(subtractFractions(r.received,approved).numerator)>0n)return stop('Recebimentos excedem o montante reconhecido;reconcilie sem criar crédito ou caixa adicional.');r.awaitingReceipt=positiveFraction(subtractFractions(approved,r.received));r.status='determined';r.reason=(eligibilityReason?'Decisão externa documentada registada independentemente da avaliação automática do fundamento: '+eligibilityReason+' ':'')+'Pedido,decisão e recebimento separados;parcela27 não é reutilizada em26. Parte não deferida/indeferimento não libertam crédito automaticamente;eventual retorno exige procedimento documentado e reconciliação,sem duplicar20. Recebimento é registo declarado,sem execução bancária ou alteração silenciosa de caixa. Formalidades e suspensões não automatizadas;150dias não são promessa de recebimento.';return r;
const events=assessFiscalClaim(row,s.requested,p=>{const id=receiptIdentity(row,p);return all.flatMap(v=>v.receipts.filter(q=>receiptIdentity(v,q)===id)).length>1||old.some(v=>v.id!==row.id&&v.receipts.some(q=>receiptIdentity(v,q)===id));});Object.assign(r,events);
if(events.status==='indeterminate')return r;
if(row.decision==='pending'){if(eligibilityReason){r.status='indeterminate';return stop(eligibilityReason+' Pedido/estado conservados;formalidades regulamentares não automatizadas.');}r.reason=row.stage==='preparation'?'Pedido em preparação;parcela separada do reporte,sem submissão,decisão ou recebimento.':'Pedido apresentado fora doInvesteMoz;decisão e recebimento ainda não identificados.';return r;}
r.reason=(eligibilityReason?'Decisão externa documentada registada independentemente da avaliação automática do fundamento: '+eligibilityReason+' ':'')+'Pedido,decisão e recebimento separados;parcela27 não é reutilizada em26. Parte não deferida/indeferimento não libertam crédito automaticamente;eventual retorno exige procedimento documentado e reconciliação,sem duplicar20. Recebimento é registo declarado,sem execução bancária ou alteração silenciosa de caixa. Formalidades e suspensões não automatizadas;150dias não são promessa de recebimento.';return r;
});
const result={calculatedAt:now,key:key(d,b),inputs:structuredClone(b.rows),rows};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};
}
+2
View File
@@ -18,6 +18,7 @@ import { acquisitionReportRows } from './iva-acquisitions.ts';
import {closureReportRows} from './iva-closure.ts';
import {substitutionReportRows} from './iva-substitution.ts';
import {refundReportRows} from './iva-refund.ts';
import {beverageRestitutionReportRows} from './iva-beverage-restitution.ts';
import { importExemptionReportRows } from './iva-import-exemptions.ts';
import { publicBaseReportRows } from './iva-public-authority-base.ts';
import { commissionBaseReportRows } from './iva-commission-base.ts';
@@ -652,6 +653,7 @@ export function buildProjectDossierRows(
rows.push(...closureReportRows(draft));
rows.push(...substitutionReportRows(draft));
rows.push(...refundReportRows(draft));
rows.push(...beverageRestitutionReportRows(draft));
rows.push(...importExemptionReportRows(draft));
rows.push(...publicBaseReportRows(draft));
rows.push(...commissionBaseReportRows(draft));
@@ -0,0 +1,26 @@
<?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 IvaBeverageRestitutionDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_beverage_restitution_recovers_invoice_classification_exclusive_route_decision_receipt_and_all_previous_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-e-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['ivaBeverageRestitutions']['opportunity:375'];$this->assertCount(1,$b['rows']);$this->assertCount(1,$b['history']);$r=$b['result']['rows'][0];$this->assertSame('determined',$r['status']);
foreach(['requested'=>'16','approved'=>'12','received'=>'7','awaitingReceipt'=>'5','notGranted'=>'4']as $f=>$v)$this->assertSame(['numerator'=>$v,'denominator'=>'1'],$r[$f]);
$i=$b['result']['inputs'][0];$this->assertSame('SYNTHETIC QA facturaBE1',$i['source']['documentRef']);$this->assertSame('1-bebidas',$i['source']['documentLine']);$this->assertSame('beverage-supply',$i['source']['operationId']);$this->assertSame('yes',$i['exclusive']);$this->assertSame('SYNTHETIC QA decisãoBE1',$i['decisionReference']);
$this->assertSame('0.00',$s['books']['opportunity:375']['result']['rows'][0]['deductible']);$this->assertSame('refund',$s['books']['opportunity:375']['rows'][0]['bc']['beveragePath']);
foreach(['iva-l3e-preserved','iva-l3ab-preserved']as $fixture){$accepted=json_decode(file_get_contents(base_path('tests/frontend/fixtures/'.$fixture.'.json')),true,flags:JSON_THROW_ON_ERROR);foreach(json_decode($accepted['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]);}}
$this->assertSame(['numerator'=>'72','denominator'=>'1'],$s['ivaClosures']['opportunity:373']['result']['rows'][2]['fields']['24']);$this->assertSame(['numerator'=>'450000','denominator'=>'1'],$s['ivaRefunds']['opportunity:374']['result']['rows'][0]['received']);
file_put_contents(base_path('outputs/iva-l3-e-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -61,7 +61,7 @@ test('beverages13(2) uses current c/d/h source and deduction-only scope without
const{draft,row}=bcFixture();const ops=salesBook(draft);const rule=operationTreatments.find(t=>t.id===treatment);ops.rows.push({...newSalesOperation('beverages','2026-12','MZN'),description:'SYNTHETIC QA bebidas de bordo',establishment:'Manica',amount:'100',basis:'net',treatment,supplyCategory:'beverages',confirmed:rule.conditions.map((_,i)=>i),evidence:'SYNTHETIC QA meio,destino,utilização e embarque'});draft.modelo_a_snapshot=saveSalesBook(draft,recordSales(draft,ops));
const r=row('beverages',{}, {expense:'beverages',beveragePath:'deduction',facts:{beverageSpecial:'yes',onlyDeduction:'yes',notCoastalFishing:'yes'}});r.deductionLink={...createAcquisitionRightLink(draft,'beverages'),scopeConfirmed:'yes',allocationRef:'SYNTHETIC QA parcela e operação',noExcludedPayments:'yes'};
assert.equal(evaluateAcquisition(r,'MZN','yes',draft).deductible,treatment==='onboard_departing_warship'?null:'16.00',treatment);
assert.equal(evaluateAcquisition({...r,bc:{...r.bc,beveragePath:'refund'}},'MZN','yes',draft).deductible,null);
assert.equal(evaluateAcquisition({...r,bc:{...r.bc,beveragePath:'refund'}},'MZN','yes',draft).deductible,treatment==='onboard_departing_warship'?null:'0.00');
assert.equal(evaluateAcquisition({...r,bc:{...r.bc,facts:{...r.bc.facts,onlyDeduction:''}}},'MZN','yes',draft).deductible,null);
if(treatment==='onboard_rescue_coastal_ship')assert.equal(evaluateAcquisition({...r,bc:{...r.bc,facts:{...r.bc.facts,notCoastalFishing:'no'}}},'MZN','yes',draft).deductible,null);
}
@@ -0,0 +1,21 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {newSalesOperation,recordSales,saveSalesBook,operationTreatments,salesBook} from '../../resources/js/lib/iva-operations.ts';
import {newAcquisition,recordAcquisitions,saveAcquisitionBook,acquisitionBook,acquisitionCurrent} from '../../resources/js/lib/iva-acquisitions.ts';
import {createAcquisitionRightLink} from '../../resources/js/lib/iva-acquisition-right.ts';
import {newAcquisitionBC} from '../../resources/js/lib/iva-acquisition-bc.ts';
import {newBeverageRestitution,beverageRestitutionSources,recordBeverageRestitution,saveBeverageRestitutionBook,beverageRestitutionBook,beverageRestitutionCurrent,beverageRestitutionReportRows} from '../../resources/js/lib/iva-beverage-restitution.ts';
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3e-preserved.json',import.meta.url),'utf8')),older=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3ab-preserved.json',import.meta.url),'utf8'));
function fixture(treatment='onboard_seagoing_ship'){const d=structuredClone(seed),s=JSON.parse(d.modelo_a_snapshot),old=JSON.parse(older.modelo_a_snapshot);for(const[k,v]of Object.entries(old))if(k!=='version')s[k]={...v,...s[k]};d.modelo_a_snapshot=JSON.stringify(s);d.context_opportunity='opportunity:375';const rule=operationTreatments.find(t=>t.id===treatment);const sale={...newSalesOperation('beverage-supply','2026-12','MZN'),description:'SYNTHETIC QA bebidas efectivamente postas a bordo',establishment:'Manica',amount:'100',basis:'net',treatment,supplyCategory:'beverages',confirmed:rule.conditions.map((_,i)=>i),evidence:'SYNTHETIC QA embarcação/avião,actividade,consumo e embarque'};d.modelo_a_snapshot=saveSalesBook(d,recordSales(d,{currency:'MZN',rows:[sale],periods:[{period:'2026-12',complete:true}]}));
const row={...newAcquisition('beverage-purchase','2026-12','MZN'),net:'100',vat:'16',gross:'116',description:'SYNTHETIC QA parcela bebidas',documentType:'invoice',documentRef:'SYNTHETIC QA facturaBE1',documentDate:'2026-12-10',documentLine:'1-bebidas',supplier:'SYNTHETIC QA fornecedor',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes',deductionLink:{...createAcquisitionRightLink(d,'beverage-supply'),scopeConfirmed:'yes',allocationRef:'SYNTHETIC QA linha1 exclusivamente para abastecimento identificado',noExcludedPayments:'yes'},bc:{...newAcquisitionBC(),expense:'beverages',beveragePath:'refund',reducedSupply:'no',evidence:'SYNTHETIC QA natureza bebidas,não tabaco/recepção',facts:{beverageSpecial:'yes',notCoastalFishing:'yes'}}};
const acq=recordAcquisitions(d,{currency:'MZN',rightVersion:'acquisition-operation-right/1',taxpayerRegistered:'yes',rows:[row],periods:[{period:'2026-12',complete:true,importsComplete:true,absent:['12','13']}]});d.modelo_a_snapshot=saveAcquisitionBook(d,acq);
const claim={...newBeverageRestitution('beverage-claim',beverageRestitutionSources(d)[0]??null,'SYNTHETIC QA entidade375'),exclusive:'yes',stage:'submitted',requestReference:'SYNTHETIC QA pedidoBE1',requestDate:'2026-12-15',decision:'partial',decisionReference:'SYNTHETIC QA decisãoBE1',decisionDate:'2026-12-20',decisionIssuer:'SYNTHETIC QA entidade emitente declarada',approvedAmount:'12',receipts:[{id:'rec1',reference:'SYNTHETIC QA recebimentoBE1',date:'2026-12-23',amount:'7'}]};return{d,b:{version:1,rows:[claim]},acq};}
const n=v=>v&&`${v.numerator}/${v.denominator}`;
test('E reuses classified supply and invoice;VAT16 is not deducted,12recognised/7received/5waiting remain distinct',()=>{const{d,b,acq}=fixture();assert.equal(acq.result.rows[0].deductible,'0.00');assert.ok(b.rows[0].source);const r=recordBeverageRestitution(d,b).result.rows[0];assert.equal(r.status,'determined',r.reason);assert.equal(n(r.requested),'16/1');assert.equal(n(r.approved),'12/1');assert.equal(n(r.received),'7/1');assert.equal(n(r.awaitingReceipt),'5/1');assert.equal(n(r.notGranted),'4/1');assert.equal(b.rows[0].source.operationId,'beverage-supply');assert.equal(b.rows[0].source.documentLine,'1-bebidas');});
test('c/d/h retain their distinct conditions;warships,coastal fishing,ordinary beverages and changedL1 are not inherited',()=>{for(const t of ['onboard_seagoing_ship','onboard_rescue_coastal_ship','onboard_international_aircraft'])assert.ok(fixture(t).b.rows[0].source,t);assert.equal(fixture('onboard_departing_warship').b.rows[0].source,null);const{d}=fixture('onboard_rescue_coastal_ship'),a=acquisitionBook(d);a.rows[0].bc.facts.notCoastalFishing='no';d.modelo_a_snapshot=saveAcquisitionBook(d,recordAcquisitions(d,a));assert.deepEqual(beverageRestitutionSources(d),[]);const f=fixture();const ops=salesBook(f.d);ops.rows[0].evidence='changed';f.d.modelo_a_snapshot=saveSalesBook(f.d,recordSales(f.d,ops));assert.equal(recordBeverageRestitution(f.d,f.b).result.rows[0].requested,null);});
test('a projected alternative may change its route explicitly;effective past deduction cannot be erased to obtain restitution',()=>{const{d,b}=fixture(),a=acquisitionBook(d);a.rows[0].origin='effective';a.rows[0].bc.beveragePath='deduction';a.rows[0].bc.facts.onlyDeduction='yes';let actual=recordAcquisitions(d,a);assert.equal(actual.result.rows[0].deductible,'16.00');d.modelo_a_snapshot=saveAcquisitionBook(d,actual);actual.rows[0].bc.beveragePath='refund';actual=recordAcquisitions(d,actual);d.modelo_a_snapshot=saveAcquisitionBook(d,actual);b.rows[0].source=beverageRestitutionSources(d)[0];assert.equal(recordBeverageRestitution(d,b).result.rows[0].requested,null);assert.equal(actual.history.at(-1).rows[0].deductible,'16.00');});
test('an exact historical deduction is not zero merely because its presentation rounds tozero',()=>{const{d,b}=fixture(),a=acquisitionBook(d);a.rows[0].origin='effective';const actual=recordAcquisitions(d,a),prior=structuredClone(actual.result);prior.rows[0].deductible='0.00';prior.rows[0].deductibleExact={numerator:'1',denominator:'100000000'};actual.history=[prior];d.modelo_a_snapshot=saveAcquisitionBook(d,actual);b.rows[0].source=beverageRestitutionSources(d)[0];assert.equal(recordBeverageRestitution(d,b).result.rows[0].requested,null);});
test('submitted/decided restitution blocks deduction of the same parcel even after removing current preparation',()=>{const{d,b}=fixture();let book=recordBeverageRestitution(d,b);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);assert.ok(acquisitionCurrent(d,acquisitionBook(d)));let a=acquisitionBook(d);a.rows[0].bc.beveragePath='deduction';a.rows[0].bc.facts.onlyDeduction='yes';let result=recordAcquisitions(d,a);assert.equal(result.result.rows[0].deductible,null);assert.match(result.result.rows[0].reason,/Parcela ligada/);book=recordBeverageRestitution(d,{...book,rows:[]});d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);result=recordAcquisitions(d,a);assert.equal(result.result.rows[0].deductible,null);});
test('duplicate requests/receipts and receipts beyond decision do not create another benefit or cash',()=>{const{d,b}=fixture();const dup=structuredClone(b);dup.rows.push({...structuredClone(dup.rows[0]),id:'copy'});assert.ok(recordBeverageRestitution(d,dup).result.rows.every(r=>r.requested===null));const rec=structuredClone(b);rec.rows[0].receipts.push({...rec.rows[0].receipts[0],id:'copy'});assert.equal(recordBeverageRestitution(d,rec).result.rows[0].received,null);rec.rows[0].receipts=[{...rec.rows[0].receipts[0],amount:'13'}];assert.equal(recordBeverageRestitution(d,rec).result.rows[0].status,'indeterminate');});
test('preparation,pending decision and missing facts never imply exemption,approval or receipt',()=>{const{d,b}=fixture();for(const patch of [{entityRef:''},{exclusive:''},{requestDate:''},{decisionReference:''},{approvedAmount:''},{stage:'preparation'}]){const v=structuredClone(b);Object.assign(v.rows[0],patch);assert.equal(recordBeverageRestitution(d,v).result.rows[0].status,'indeterminate');}Object.assign(b.rows[0],{stage:'preparation',decision:'pending',receipts:[]});const r=recordBeverageRestitution(d,b).result.rows[0];assert.equal(r.status,'determined');assert.equal(r.approved,null);assert.equal(r.received,null);});
test('all prior maps and original documents survive save/recovery;report uses frozen inputs and recalculation appends history',()=>{const{d,b}=fixture(),before=JSON.parse(d.modelo_a_snapshot);let book=recordBeverageRestitution(d,b);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);for(const[k,v]of Object.entries(before))assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v,k);assert.ok(beverageRestitutionCurrent(d,book));book.rows[0].requestReference='deliberate edit';assert.equal(beverageRestitutionCurrent(d,book),false);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);assert.doesNotMatch(beverageRestitutionReportRows(d).map(r=>r.join(' ')).join(' '),/deliberate edit/);const old=structuredClone(book.result);book=recordBeverageRestitution(d,book);assert.deepEqual(book.history.at(-1),old);});
test('export synthetic source-linkedE for guardedAPI and real component proof',()=>{const{d,b}=fixture();let book=recordBeverageRestitution(d,b,'2026-10-25T14:00:00Z');book=recordBeverageRestitution(d,book,'2026-10-25T14:01:00Z');d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);assert.ok(beverageRestitutionCurrent(d,book));if(process.env.IVA_L3E_CALCULATED)fs.writeFileSync(process.env.IVA_L3E_CALCULATED,JSON.stringify(d));});