feat(irpc): demonstrate documented international credit in QA
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-27 14:57:18 +02:00
parent 814fe0a9b7
commit 838ad941a2
17 changed files with 734 additions and 16 deletions
@@ -1522,3 +1522,42 @@ O primeiro recorte para implementaçãoQA será o crédito por dupla tributaçã
| Regulamentação2026para obrigação/calendário | Anúncio sem articulado publicado:retomar com diploma,artigos e vigência dos parâmetros afectados;sem bloquear oPECcorrente ou crédito independente cujo fundamento já está disponível. |
L8 permanece parcial. A avaliação real do percurso proactivo continua a aguardar retorno da equipa;dificuldades observadas serão correcções concretas. Songo eGurué permanecem privados;L3/ISPC/câmbio/L6/L7 eSGFTEMPO preservados. Sem nova estimativa global ou declaração de IRPC completo.
## 27/10/2026 — Crédito internacional corrente: percurso implementado em QA
**Estado actual: implementado e demonstrado em QA, sem instalação.** Base `814fe0a9`, descendente do PEC instalado `e182d1d7` e da correcção dos sectores `669cb897`. Os registos anteriores de delimitação são históricos: o crédito corrente abaixo já tem código executável; os restantes grupos continuam separados. Ramo `codex/irpc-l8-international-credit-qa-20261027`. Recibo: `docs/investmoz/simulations/irpc-l8-international-credit-qa-20261027.json`.
**Âmbito e origem das restrições:** pedido expresso da Tempo para implementar o crédito internacional em QA, preservando o PEC e os históricos. Reutilizado CIRPC65(1–2), Lei34/2007, PDF18/p768-(72), e ordem antes do PEC em66/RIRPC21, do acervo aceite. Escolha incremental de implementação: residente geral, ano civil2026 e valores comprovados emMZN; inclusão do rendimento e fracção nacional documentadas, sem determinação automática dessa fracção, de vigência/aplicabilidade deCDT ou de conversão cambial. A selecção do país não concede crédito. A mudança de país/colecta exige reconfirmar a correspondência dos limites, conservando os valores.
**Percurso:** a interface permite preparar explicitamente o detalhe, reutilizando entidade/exercício/colecta e o resumo anterior quando este declara ausência ou zero; mantém esse resumo e os resultados anteriores. Uma posição por país/exercício agrega rendimentos e provas sob um só limite nacional. Regista natureza do imposto, pagamento efectivo, titular, suporte e utilização anterior real; data fora do exercício exige correspondência documental expressa, sem inferir transporte. O crédito elegível é o menor entre imposto seleccionado comprovado, fracção nacional e limiteCDT quando aplicável; desconta utilização anterior e limita a dedução à colecta. Seguem PEC, retenções ePPC existentes. Não transforma simulações históricas em utilização fiscal. Limites admitem fracção exacta; apresentação decimal não é regra de arredondamento fiscal.
| Caso sintético recuperado da API | Novo efeito demonstrado em MZN |
| --- | --- |
| Imposto estrangeiro10, limite nacional8, não aplicaçãoCDT documentada | Crédito8; 1 408−8=1 400; PEC200→1 200; PPC500→**700 a pagar, simulado**. Os2 acima do limite não são transporte. |
| LimiteCDT documentado6 | Crédito6; saldo simulado702. Os dados de país/CDT são artificiais, sem afirmar uma convenção real. |
| Utilização anterior comprovada3 | Elegível8, disponível5; saldo simulado703. Não somar utilizações de simulações. |
| Crédito disponível2 000 e colecta1 408 | Deduz1 408; insuficiência592 identificada, sem transporte; PEC200 não deduzido. Os500 a recuperar provêm doPPC, sem reembolso do crédito/PEC. |
| Ausência internacional expressa e documentada | Crédito0, saldo708; não confundir ausência com informação em falta. |
| AplicabilidadeCDT em falta ou mesma prova usada comoPPC e imposto estrangeiro | Saldo e dedução dependentes indeterminados, com motivo; não preencher por presunção. |
**Provas do delta:**18testes novos e31anuais/PEC afectados,49aprovados; API privada1teste/171asserções com transacção revertida na baseQA guardada, porta55441. Sete casos porPOST/PUT/GET, recalculados após recuperação e exportados pelos relatóriosHTML e dossier existentes; histórico anterior congelado. Wayfinder `routes/actions`, TypeScript e compilação isolada aprovados. Esta passagem não inclui uma nova prova manual de utilização no browser nem uma avaliação pela equipa. Não se repetiram as suites independentes aceites. As7tabelas de registos anteriores ficaram intactas;30PDFs originais e11consumidores protegidos iguais ao instalado. Sem migrations, novas permissões ou configuração activa; nada enviado para a base do portal. Os testes usam fixtures sintéticas portáteis, sem dados comerciais ou pilotos reais.
**Prevenção de dupla dedução:** identidade física da prova por emitente/documento, independentemente da data; confronto entre imposto estrangeiro,RF,PPC ePEC e entre mapas do mesmo snapshot; país/entidade/exercício e montantes disponíveis coerentes. Posições repetidas do mesmo país/ano exigem consolidação, evitando repetir a quota nacional. Não existe consumo de saldos oficiais nem reconciliação global entre dossiers independentes. Valores conhecidos, ausência declarada e informação em falta permanecem distintos. Créditos económicos e benefícios positivos ou desconhecidos deixam apenas o saldo dependente indeterminado, preservando a qualificação internacional conhecida.
### Saldo concreto de L8 após este incremento
| Código por implementar | Condição de retoma |
| --- | --- |
| Determinação automática da fracção nacional e limites específicosCDT | Corresponder componentes da base/imposto e rendimento estrangeiro e regras específicas do caso; substituir entradas documentadas apenas quando essa correspondência estiver demonstrada. A regra corrente do menor/ordem já está implementada. |
| Crédito económico e créditos de investimento/benefício | Aplicar art64/CBF próprio, qualificaçãoL7, base elegível, limite, cumulação e uso real; não os converter em crédito internacional. |
| Transporte/caducidade e utilização interanual | Proveniência por exercício, utilização real e prazo próprio de65(3), distinto doPEC66; sem transformar imposto acima do limite em crédito transportável. PEC excedente/transporte continuam fora do âmbito entregue. |
| Obrigação/calendárioPPC/PEC, autónomos, períodos especiais, outras moedas eM20/M22 | Retomar cada grupo com factos e regra de período/compensação/câmbio/campo correspondidos; reutilizar motores existentes sem alterar o cálculo independente aceite. |
| Informação documental externa em falta | Exemplo e condição de retoma |
| --- | --- |
| Inclusão e fracção nacional do caso | Imposto10 sem prova da inclusão/base: identificar o rendimento incluído e o suporte da quota; apenas o saldo desse caso fica indeterminado. Não exige nova autenticação da fonte oficial. |
| Convenção, período e imposto elegível | País conhecido sem artigo/limite aplicável: corresponder artigo, vigência e imposto pago nos termos daCDT. O limite6 da QA não é uma correspondência oficial. |
| Pagamento/titular/uso anterior e correspondência temporal | Pagamento em2027 afecto a2026 sem suporte: anexar/identificar correspondência expressa; sem presumir transporte ou utilização zero. |
**L8 permanece parcial.** Crédito internacional apenas emQA; PEC instalado em`e182d1d7` mantém o âmbito demonstrado. Songo eGurué privados; L3,ISPC,câmbio,L5–L7 eSGFTEMPO preservados. Avaliação de utilização do percurso proactivo continua a aguardar retorno da equipa. Sem instalação, publicação ou nova estimativa global nesta passagem.
@@ -0,0 +1,220 @@
{
"date": "2026-10-27",
"status": "QA demonstrated; not installed",
"branch": "codex/irpc-l8-international-credit-qa-20261027",
"base": "814fe0a9",
"installed_reference": "e182d1d79dc945f35d2079c6b8ca2ac93048cec3",
"sector_fix_preserved": "669cb8979b0c372659fc2ad3876aebc919d5df19",
"scope": {
"year": "2026",
"resident": "general",
"period": "calendar year",
"currency": "documented MZN",
"foreign_tax": "actual income tax paid; inclusion in taxable base documented",
"national_fraction": "documented exact input, not automatically derived",
"treaty": "case-specific applicability and ceiling documented, not inferred from country",
"order": [
"international credit limited to eligible available amount and collection",
"PEC limited to remaining collection",
"withholdings and PPC"
],
"known_absent_missing_distinct": true,
"proof_deduplication": "issuer/document across tax natures, dates and snapshot contexts; consolidate country-year quota",
"officialBalancesConsumed": false,
"carryCreated": false
},
"sources": {
"reuse": "accepted CIRPC65(1–2), Lei34/2007 BR52 PDF18/printed768-(72); CIRPC66/RIRPC21 order",
"originals_preserved": 30,
"source_preservation_artifact": "outputs/irpc-l8-international-20261027/source-preservation.json",
"automatic_treaty_entitlement": false
},
"verification": {
"frontend_new": 18,
"frontend_annual_pec_affected": 31,
"frontend_pass": 49,
"frontend_fail": 0,
"api_tests": 1,
"api_assertions": 171,
"api_cases": 7,
"api_path": "POST/PUT/GET private simulation drafts; DatabaseTransactions rollback in isolated guarded PostgreSQL55441",
"recovered_reports": "fresh recalculation of all7 API recoveries; existing HTML and dossier finance report; frozen previous result",
"wayfinder_routes_actions": "passed --with-form",
"typescript": "passed",
"isolated_build": "passed",
"new_browser_manual_proof": false,
"team_usability_evaluation": "awaiting team return; separate"
},
"cases": [
{
"name": "payable",
"status": "payable",
"balance": {
"numerator": "700",
"denominator": "1"
},
"current": true,
"history_records": 0,
"apiRoundTrip": true,
"dossier": true
},
{
"name": "treaty",
"status": "payable",
"balance": {
"numerator": "702",
"denominator": "1"
},
"current": true,
"history_records": 1,
"apiRoundTrip": true,
"dossier": true
},
{
"name": "missing",
"status": "indeterminate",
"balance": null,
"current": true,
"history_records": 1,
"apiRoundTrip": true,
"dossier": true
},
{
"name": "limited-collecta",
"status": "recoverable",
"balance": {
"numerator": "-500",
"denominator": "1"
},
"current": true,
"history_records": 1,
"apiRoundTrip": true,
"dossier": true
},
{
"name": "duplicate",
"status": "indeterminate",
"balance": null,
"current": true,
"history_records": 1,
"apiRoundTrip": true,
"dossier": true
},
{
"name": "absent",
"status": "payable",
"balance": {
"numerator": "708",
"denominator": "1"
},
"current": true,
"history_records": 1,
"apiRoundTrip": true,
"dossier": true
},
{
"name": "prior-use",
"status": "payable",
"balance": {
"numerator": "703",
"denominator": "1"
},
"current": true,
"history_records": 1,
"apiRoundTrip": true,
"dossier": true
}
],
"example": {
"foreign_tax_paid": "10 MZN",
"documented_national_limit": "8 MZN",
"documented_treaty_nonapplication": "synthetic only, not actual country entitlement",
"credit": "8 MZN",
"calculation": "1408 - 8 = 1400; - PEC200 = 1200; - PPC500 = 700 MZN simulated payable",
"above_eligibility_limit": "2 MZN, not a carry",
"insufficient_collection_example": "available2000, deducted1408, insufficient592; PEC200 not deducted; recoverable500 derives from PPC only"
},
"preservation": {
"existing_records": {
"database": "proactive_investment_20261027_test",
"tables": {
"opportunities": {
"existing_rows": 55,
"unchanged": true
},
"documents": {
"existing_rows": 64,
"unchanged": true
},
"opportunity_documents": {
"existing_rows": 1,
"unchanged": true
},
"simulation_form_drafts": {
"existing_rows": 19,
"unchanged": true
},
"users": {
"existing_rows": 40,
"unchanged": true
},
"organizations": {
"existing_rows": 3,
"unchanged": true
},
"audit_events": {
"existing_rows": 409,
"unchanged": true
}
},
"transaction_rollback": true
},
"originals": 30,
"protected_consumers_count": 11,
"no_migrations_or_permissions_changes": true,
"legacy_summary_and_frozen_history_preserved": true,
"private_pilots": "Songo/Gurue unchanged and private",
"production_written": false,
"SGF_TEMPO": "unchanged"
},
"remaining_code": [
"automatic national fraction and case-specific treaty matching",
"economic double-tax and CBF benefit credits with own rules",
"international carry/expiry65(3), separate from PEC66 carry/expiry",
"PPC/PEC obligation/calendar; autonomous tax, special periods, other currencies and M20/M22 transport/submission"
],
"document_dependencies": [
{
"missing": "income inclusion and national fraction",
"example": "tax10 but no included income/base correspondence",
"resume_when": "income/base and documented fraction matched"
},
{
"missing": "treaty article/period/ceiling",
"example": "country selected but no applicable article or eligible tax",
"resume_when": "case-specific article, effective period and eligible ceiling identified"
},
{
"missing": "proof/titular/previous actual use/temporal link",
"example": "payment2027 selected for2026 without correspondence",
"resume_when": "actual payment and explicit2026 link supported, prior use reconciled"
}
],
"limits": [
"L8 partial",
"no new PEC excess/carry scope",
"no automatic foreign exchange or official rounding rule",
"no official tax balance consumption, submission or refund execution",
"same-snapshot deduplication is not a global inter-dossier official ledger",
"synthetic QA examples do not assert any actual foreign treaty",
"no production deployment or publication of pilots"
],
"local_evidence": {
"outputs\\irpc-l8-international-20261027\\frontend-tests.log": "7f981dc819ffe0947565994c9a29a72c408eb94a9cd687dba1d1b93903949b87",
"outputs\\irpc-l8-international-20261027\\types.log": "82962651e47f564de653ad2883f8fd34e7dae4b4468e4ac459cb3331962a3879",
"outputs\\irpc-l8-international-20261027\\build.log": "d9a173ef26392275e42f6ef8927c2c9a79039a16c98a371e8bbcccbeea17776b",
"outputs\\irpc-l8-international-20261027\\existing-records-preservation.json": "b45b3306d3c9dee63fbbaf9a04bba31008019e65892bd16322460b5ef53eda7d",
"outputs\\irpc-l8-international-20261027\\source-preservation.json": "d82c68f709793103373f2110b3b55ef226fd5e6a51a6835c62425c7407bcf0bd",
"outputs\\irpc-l8-international-20261027\\recovered-report-receipt.json": "080d61ad1e3e4a849ea8b71642fb256881aa05a9fef81144cb7cf56848383cb2"
}
}
@@ -199,3 +199,8 @@ Sem novos originais:31exemplares preservados por hash. Conferidos visualmenteCIR
Sem novo original legal importado. Conferidos visualmente Lei34/2007PDF18/p768-(72),art66(1–2),eDecreto9/2008PDF6/p121,21(2/5),PDF7/p122,27/29;Lei34PDF19/art71 lido para delimitar obrigação separada. Lei4/2012,Lei19/2013 e acervo de alteraçõesLei12/2025 reutilizados;nenhuma alteração doart66 identificada nesses exemplares. Dedução corrente limitada à colecta,sem reembolso PEC;transporte/caducidade e obrigação separados.30PDFs presentes no arquivo source-archive iguais byte a byte a dc14f01e;recibo source-preservation.json.
Consulta primária dirigida identificou anúncio do Governo de alterações regulamentares2026: https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/ . O anúncio não fornece o articulado promulgado e contém referência editorial divergente à lei; não é tratado como nova regra. Retoma específica para obrigação/calendário: diploma publicado,artigos e vigência. Sem nova pesquisa genérica ou substituição das fontes aceites;art66 e cálculo simulado corrente permanecem no recorte explícito. Model39 oficialAT identificaPEC,mas não foi inferido transporte automáticoM20/M22: https://edeclaracao.at.gov.mz/formularios/irpc/view/formulario_irpc_m39.aspx .
### 27/10/2026 — Reutilização executável: crédito internacional corrente em QA
CIRPC65(1–2), Lei34/2007, exemplarBR52, PDF18/p768-(72), e ordem face aoPEC em66/RIRPC21 já aceites: usados no menor entre imposto estrangeiro efectivamente pago, fracção nacional documentada e limiteCDT aplicável, antes doPEC e até à colecta. Conferência da página existente para aplicação da fonte, sem nova exigência de autenticação. Original preservado, sem activação por importação/publicação. Afracção nacional eCDT do caso continuam documentadas pelo utilizador; aQA não estabelece correspondências oficiais de convenções.65(3), transportes, créditos económicos/CBF e obrigaçãoPEC continuam separados. Ver recibo `irpc-l8-international-credit-qa-20261027.json`; não instalado.
@@ -7064,3 +7064,42 @@ O primeiro recorte para implementaçãoQA será o crédito por dupla tributaçã
| Regulamentação2026para obrigação/calendário | Anúncio sem articulado publicado:retomar com diploma,artigos e vigência dos parâmetros afectados;sem bloquear oPECcorrente ou crédito independente cujo fundamento já está disponível. |
L8 permanece parcial. A avaliação real do percurso proactivo continua a aguardar retorno da equipa;dificuldades observadas serão correcções concretas. Songo eGurué permanecem privados;L3/ISPC/câmbio/L6/L7 eSGFTEMPO preservados. Sem nova estimativa global ou declaração de IRPC completo.
## 27/10/2026 — Crédito internacional corrente: percurso implementado em QA
**Estado actual: implementado e demonstrado em QA, sem instalação.** Base `814fe0a9`, descendente do PEC instalado `e182d1d7` e da correcção dos sectores `669cb897`. Os registos anteriores de delimitação são históricos: o crédito corrente abaixo já tem código executável; os restantes grupos continuam separados. Ramo `codex/irpc-l8-international-credit-qa-20261027`. Recibo: `docs/investmoz/simulations/irpc-l8-international-credit-qa-20261027.json`.
**Âmbito e origem das restrições:** pedido expresso da Tempo para implementar o crédito internacional em QA, preservando o PEC e os históricos. Reutilizado CIRPC65(1–2), Lei34/2007, PDF18/p768-(72), e ordem antes do PEC em66/RIRPC21, do acervo aceite. Escolha incremental de implementação: residente geral, ano civil2026 e valores comprovados emMZN; inclusão do rendimento e fracção nacional documentadas, sem determinação automática dessa fracção, de vigência/aplicabilidade deCDT ou de conversão cambial. A selecção do país não concede crédito. A mudança de país/colecta exige reconfirmar a correspondência dos limites, conservando os valores.
**Percurso:** a interface permite preparar explicitamente o detalhe, reutilizando entidade/exercício/colecta e o resumo anterior quando este declara ausência ou zero; mantém esse resumo e os resultados anteriores. Uma posição por país/exercício agrega rendimentos e provas sob um só limite nacional. Regista natureza do imposto, pagamento efectivo, titular, suporte e utilização anterior real; data fora do exercício exige correspondência documental expressa, sem inferir transporte. O crédito elegível é o menor entre imposto seleccionado comprovado, fracção nacional e limiteCDT quando aplicável; desconta utilização anterior e limita a dedução à colecta. Seguem PEC, retenções ePPC existentes. Não transforma simulações históricas em utilização fiscal. Limites admitem fracção exacta; apresentação decimal não é regra de arredondamento fiscal.
| Caso sintético recuperado da API | Novo efeito demonstrado em MZN |
| --- | --- |
| Imposto estrangeiro10, limite nacional8, não aplicaçãoCDT documentada | Crédito8; 1 408−8=1 400; PEC200→1 200; PPC500→**700 a pagar, simulado**. Os2 acima do limite não são transporte. |
| LimiteCDT documentado6 | Crédito6; saldo simulado702. Os dados de país/CDT são artificiais, sem afirmar uma convenção real. |
| Utilização anterior comprovada3 | Elegível8, disponível5; saldo simulado703. Não somar utilizações de simulações. |
| Crédito disponível2 000 e colecta1 408 | Deduz1 408; insuficiência592 identificada, sem transporte; PEC200 não deduzido. Os500 a recuperar provêm doPPC, sem reembolso do crédito/PEC. |
| Ausência internacional expressa e documentada | Crédito0, saldo708; não confundir ausência com informação em falta. |
| AplicabilidadeCDT em falta ou mesma prova usada comoPPC e imposto estrangeiro | Saldo e dedução dependentes indeterminados, com motivo; não preencher por presunção. |
**Provas do delta:**18testes novos e31anuais/PEC afectados,49aprovados; API privada1teste/171asserções com transacção revertida na baseQA guardada, porta55441. Sete casos porPOST/PUT/GET, recalculados após recuperação e exportados pelos relatóriosHTML e dossier existentes; histórico anterior congelado. Wayfinder `routes/actions`, TypeScript e compilação isolada aprovados. Esta passagem não inclui uma nova prova manual de utilização no browser nem uma avaliação pela equipa. Não se repetiram as suites independentes aceites. As7tabelas de registos anteriores ficaram intactas;30PDFs originais e11consumidores protegidos iguais ao instalado. Sem migrations, novas permissões ou configuração activa; nada enviado para a base do portal. Os testes usam fixtures sintéticas portáteis, sem dados comerciais ou pilotos reais.
**Prevenção de dupla dedução:** identidade física da prova por emitente/documento, independentemente da data; confronto entre imposto estrangeiro,RF,PPC ePEC e entre mapas do mesmo snapshot; país/entidade/exercício e montantes disponíveis coerentes. Posições repetidas do mesmo país/ano exigem consolidação, evitando repetir a quota nacional. Não existe consumo de saldos oficiais nem reconciliação global entre dossiers independentes. Valores conhecidos, ausência declarada e informação em falta permanecem distintos. Créditos económicos e benefícios positivos ou desconhecidos deixam apenas o saldo dependente indeterminado, preservando a qualificação internacional conhecida.
### Saldo concreto de L8 após este incremento
| Código por implementar | Condição de retoma |
| --- | --- |
| Determinação automática da fracção nacional e limites específicosCDT | Corresponder componentes da base/imposto e rendimento estrangeiro e regras específicas do caso; substituir entradas documentadas apenas quando essa correspondência estiver demonstrada. A regra corrente do menor/ordem já está implementada. |
| Crédito económico e créditos de investimento/benefício | Aplicar art64/CBF próprio, qualificaçãoL7, base elegível, limite, cumulação e uso real; não os converter em crédito internacional. |
| Transporte/caducidade e utilização interanual | Proveniência por exercício, utilização real e prazo próprio de65(3), distinto doPEC66; sem transformar imposto acima do limite em crédito transportável. PEC excedente/transporte continuam fora do âmbito entregue. |
| Obrigação/calendárioPPC/PEC, autónomos, períodos especiais, outras moedas eM20/M22 | Retomar cada grupo com factos e regra de período/compensação/câmbio/campo correspondidos; reutilizar motores existentes sem alterar o cálculo independente aceite. |
| Informação documental externa em falta | Exemplo e condição de retoma |
| --- | --- |
| Inclusão e fracção nacional do caso | Imposto10 sem prova da inclusão/base: identificar o rendimento incluído e o suporte da quota; apenas o saldo desse caso fica indeterminado. Não exige nova autenticação da fonte oficial. |
| Convenção, período e imposto elegível | País conhecido sem artigo/limite aplicável: corresponder artigo, vigência e imposto pago nos termos daCDT. O limite6 da QA não é uma correspondência oficial. |
| Pagamento/titular/uso anterior e correspondência temporal | Pagamento em2027 afecto a2026 sem suporte: anexar/identificar correspondência expressa; sem presumir transporte ou utilização zero. |
**L8 permanece parcial.** Crédito internacional apenas emQA; PEC instalado em`e182d1d7` mantém o âmbito demonstrado. Songo eGurué privados; L3,ISPC,câmbio,L5–L7 eSGFTEMPO preservados. Avaliação de utilização do percurso proactivo continua a aguardar retorno da equipa. Sem instalação, publicação ou nova estimativa global nesta passagem.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import {SimulationIrpcInternational} from './simulation-irpc-international';
import {SimulationIrpcPec} from './simulation-irpc-pec';
import {useState} from 'react';
import {annualBook,newAnnualBook,saveAnnualBook,reuseAnnualAssessment,annualAssessment,annualWithholdingOptions,reuseAnnualWithholding,newAnnualPpc,recordAnnual,annualReportRows,annualHtml,annualSource,type AnnualBook,type AnnualPpc} from '@/lib/irpc-annual-settlement';
@@ -19,7 +20,7 @@ export function SimulationIrpcAnnual({draft,onChange}:Props){
{b.withholdings.rows.map((r,index)=>{const change=(p:Partial<typeof r>)=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>Retenção seleccionada {index+1}</legend><p>Rendimento:{r.rowId};prova:{r.paymentId};NUIT:{r.subjectNuit};exercício:{r.year}. Fonte conservada. Se mudou, retire esta selecção e seleccione a prova actual. O histórico mantém-se.</p><label className="block">Parcela a abater<input aria-label={'Parcela da retenção L8 '+(index+1)} value={r.amount} onChange={e=>change({amount:e.target.value})}/></label><label className="block">O próprio sujeito é beneficiário e o rendimento está incluído na base deste exercício<select aria-label={'Rendimento incluído L8 '+(index+1)} value={r.incomeIncluded} onChange={e=>change({incomeIncluded:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — crédito não aplicável neste fecho</option></select></label><label className="block">Suporte da titularidade e inclusão<input aria-label={'Suporte da retenção L8 '+(index+1)} value={r.reference} onChange={e=>change({reference:e.target.value})}/></label><button type="button" onClick={()=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.filter(x=>x.id!==r.id)}})}>Retirar selecção L8 da retenção {index+1}</button></fieldset>;})}</fieldset>
<fieldset className="space-y-2 rounded border p-3"><legend>PPC efectivamente pagos</legend>{group('ppc','PPC efectuados')}<p>Regista comprovativos de pagamentos por conta; não calcula automaticamente a obrigação PPC nem usa pagamentos previstos no plano.</p><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:[...b.ppc.rows,{...newAnnualPpc(crypto.randomUUID()),subjectNuit:b.subjectNuit,year:b.year}]}})}>Registar comprovativo PPC</button>
{b.ppc.rows.map((r,index)=>{const change=(p:Partial<AnnualPpc>)=>patch({ppc:{...b.ppc,rows:b.ppc.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>PPC {index+1}</legend>{([['document','Comprovativo'],['issuer','Emitente'],['date','Data (AAAA-MM-DD)'],['subjectNuit','NUIT beneficiário'],['year','Exercício do comprovativo'],['currency','Moeda do comprovativo'],['proofAmount','Montante comprovado'],['amount','Parcela a abater'],['reference','Suporte do pagamento e afectação']]as const).map(([key,label])=><label key={key} className="block">{label}<input aria-label={'PPC L8 '+(index+1)+' — '+label} value={r[key]} onChange={e=>change({[key]:e.target.value})}/></label>)}<label className="block">Pagamento efectivamente comprovado<select aria-label={'PPC L8 '+(index+1)+' — Efectivo'} value={r.effective} onChange={e=>change({effective:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — comprovativo reconciliado</option><option value="no">Não — apenas previsão/outro estado</option></select></label><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:b.ppc.rows.filter(x=>x.id!==r.id)}})}>Retirar PPC {index+1}</button></fieldset>;})}</fieldset>
<SimulationIrpcPec book={b} save={save}/>{([...(b.pecPayments?[]:[['pec','PEC']as const]),['credits','Créditos com regras próprias']as const]).map(([key,label])=><fieldset key={key} className="rounded border p-3"><legend>{label} — tratamento separado</legend><label className="block">Estado — {label}<select aria-label={'Estado L8 — '+label} value={b[key].state} onChange={e=>patch({[key]:{...b[key],state:e.target.value as ''|'known'|'absent'}})}><option value="">Informação em falta</option><option value="absent">Ausência declarada com suporte</option><option value="known">Valor conhecido — positivo exige regra própria</option></select></label>{(b[key].state==='known'||b[key].amount)&&<label className="block">Montante declarado<input aria-label={'Montante L8 — '+label} value={b[key].amount} onChange={e=>patch({[key]:{...b[key],amount:e.target.value}})}/></label>}<label className="block">Suporte<input aria-label={'Suporte L8 — '+label} value={b[key].reference} onChange={e=>patch({[key]:{...b[key],reference:e.target.value}})}/></label></fieldset>)}
<SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/>{([...(b.pecPayments?[]:[['pec','PEC']as const]),...(b.internationalCredits?[]:[['credits','Créditos com regras próprias']as const])]).map(([key,label])=><fieldset key={key} className="rounded border p-3"><legend>{label} — tratamento separado</legend><label className="block">Estado — {label}<select aria-label={'Estado L8 — '+label} value={b[key].state} onChange={e=>patch({[key]:{...b[key],state:e.target.value as ''|'known'|'absent'}})}><option value="">Informação em falta</option><option value="absent">Ausência declarada com suporte</option><option value="known">Valor conhecido — positivo exige regra própria</option></select></label>{(b[key].state==='known'||b[key].amount)&&<label className="block">Montante declarado<input aria-label={'Montante L8 — '+label} value={b[key].amount} onChange={e=>patch({[key]:{...b[key],amount:e.target.value}})}/></label>}<label className="block">Suporte<input aria-label={'Suporte L8 — '+label} value={b[key].reference} onChange={e=>patch({[key]:{...b[key],reference:e.target.value}})}/></label></fieldset>)}
<button type="button" onClick={()=>{try{save(recordAnnual(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar saldo anual L8</button><button type="button" onClick={()=>{try{const html=annualHtml(draft),url=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),link=document.createElement('a');link.href=url;link.download='IRPC-L8-saldo-simulado.html';link.click();URL.revokeObjectURL(url);setError('');}catch(e){setError(String(e));}}}>Exportar saldo anual L8</button>
{annualReportRows(draft).map((r,n)=><p key={n}>{r[1]} — {r[2]} — {r[3]}</p>)}</section>;
}catch(e){return<p role="alert">PreparaçãoL8 não recuperável:{String(e)}. Originais e históricos conservados.</p>;}
@@ -0,0 +1,57 @@
import CountrySelect, {type Country} from '@/components/country-select';
import {countryIsoCodes} from '@/lib/country-iso-codes';
import {newInternationalProof, prepareInternationalBook, type AnnualBook} from '@/lib/irpc-annual-settlement';
import {newInternationalPosition, internationalCreditSource, type CreditEvidence, type InternationalCredits, type InternationalPosition, type InternationalTaxProof} from '@/lib/irpc-international-credit';
const pt = new Intl.DisplayNames(['pt'], {type: 'region'}), en = new Intl.DisplayNames(['en'], {type: 'region'});
const countries: Country[] = countryIsoCodes.filter(code => code !== 'MZ').map(code => ({code, name: pt.of(code) ?? code, name_en: en.of(code) ?? code})).sort((a, b) => a.name.localeCompare(b.name, 'pt'));
function Evidence({value, label, change}: {value: CreditEvidence; label: string; change: (value: CreditEvidence) => void}) {
return <fieldset className="rounded border p-2"><legend>{label}</legend>
<label className="block">Estado<select aria-label={label + ' — Estado'} value={value.state} onChange={e => change({...value, state: e.target.value as CreditEvidence['state']})}><option value="">Informação em falta</option><option value="known">Valor conhecido</option><option value="absent">Ausência declarada</option></select></label>
{(value.state === 'known' || value.amount) && <label className="block">Montante MZN<input aria-label={label + ' — Montante'} value={value.amount} onChange={e => change({...value, amount: e.target.value})}/></label>}
<label className="block">Suporte<input aria-label={label + ' — Suporte'} value={value.reference} onChange={e => change({...value, reference: e.target.value})}/></label>
</fieldset>;
}
export function SimulationIrpcInternational({book, save}: {book: AnnualBook; save: (book: AnnualBook) => void}) {
const c = book.internationalCredits;
if (!c) return <fieldset className="rounded border p-3"><legend>Crédito internacional — próprio exercício</legend><p>Prepare a inclusão do imposto estrangeiro comprovado e dos limites documentados, antes do PEC. O resumo anterior de créditos e os históricos ficam conservados. A escolha do país não confirma uma convenção ou benefício.</p><button type="button" onClick={() => save(prepareInternationalBook(book))}>Preparar crédito internacional</button></fieldset>;
const update = (next: InternationalCredits) => save({...book, internationalCredits: next});
const positionChange = (p: InternationalPosition) => update({...c, positions: c.positions.map(x => x.id === p.id ? p : x)});
return <fieldset className="space-y-3 rounded border p-3"><legend>Crédito por dupla tributação internacional</legend>
<p>{internationalCreditSource}</p>
<p>Reúna os comprovativos de cada país numa posição por exercício. Indique a fracção nacional e a convenção conforme o suporte do caso. Limites admitem decimal ou fracção exacta n/d; a apresentação não constitui arredondamento fiscal. Valores MZN exigem correspondência documental; uma previsão de câmbio ou de pagamento não comprova o crédito.</p>
<label className="block">Estado internacional<select aria-label="Estado do crédito internacional L8" value={c.state} onChange={e => update({...c, state: e.target.value as InternationalCredits['state']})}><option value="">Informação em falta</option><option value="known">Posições conhecidas</option><option value="absent">Ausência declarada</option></select></label>
<label className="block">Suporte do inventário<input aria-label="Suporte do crédito internacional L8" value={c.reference} onChange={e => update({...c, reference: e.target.value})}/></label>
<Evidence label="Crédito económico — regra própria" value={c.economic} change={economic => update({...c, economic})}/>
<Evidence label="Benefícios/créditos próprios — regra própria" value={c.benefits} change={benefits => update({...c, benefits})}/>
<p>Créditos económicos e benefícios positivos exigem regras próprias; informação em falta mantém o saldo dependente indeterminado.</p>
<button type="button" onClick={() => update({...c, state: 'known', positions: [...c.positions, {...newInternationalPosition(crypto.randomUUID()), subjectNuit: book.subjectNuit, year: book.year, assessmentKey: book.assessmentKey}]})}>Registar posição internacional</button>
{c.positions.map((p, index) => {
const patch = (v: Partial<InternationalPosition>) => positionChange({...p, ...v});
const paymentChange = (r: InternationalTaxProof) => patch({payments: p.payments.map(x => x.id === r.id ? r : x)});
return <fieldset key={p.id} className="space-y-2 rounded border p-3"><legend>Posição internacional {index + 1}</legend>
<CountrySelect countries={countries} label={'País do imposto estrangeiro ' + (index + 1)} value={p.country} onChange={country => patch({country})} allowLegacyValue/>
{([['subjectNuit', 'NUIT titular'], ['year', 'Exercício'], ['incomeAmount', 'Rendimento incluído — MZN'], ['incomeReference', 'Suporte da inclusão na matéria colectável'], ['nationalLimit', 'Fracção nacional correspondente — MZN'], ['nationalLimitReference', 'Suporte da fracção nacional e exercício']] as const).map(([key, label]) => <label key={key} className="block">{label}<input aria-label={'Internacional ' + (index + 1) + ' — ' + label} value={p[key]} onChange={e => patch({[key]: e.target.value})}/></label>)}
<label className="block">Rendimento incluído na matéria colectável<select aria-label={'Internacional ' + (index + 1) + ' — Inclusão'} value={p.incomeIncluded} onChange={e => patch({incomeIncluded: e.target.value as InternationalPosition['incomeIncluded']})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — crédito não aplicável nesta base</option></select></label>
<button type="button" onClick={() => patch({assessmentKey: book.assessmentKey, qualificationCountry: p.country})}>Vincular inclusão e limites ao país e à colecta seleccionados</button>
<label className="block">Convenção aplicável<select aria-label={'Internacional ' + (index + 1) + ' — Convenção'} value={p.treaty} onChange={e => patch({treaty: e.target.value as InternationalPosition['treaty']})}><option value="">Informação em falta</option><option value="yes">Sim — artigo, período e limite documentados</option><option value="no">Não — ausência de aplicação documentada</option></select></label>
{(p.treaty === 'yes' || p.treatyLimit) && <label className="block">Imposto pago elegível nos termos da convenção — limite MZN<input aria-label={'Internacional ' + (index + 1) + ' — Limite CDT'} value={p.treatyLimit} onChange={e => patch({treatyLimit: e.target.value})}/></label>}
<label className="block">Suporte da convenção ou da não aplicação<input aria-label={'Internacional ' + (index + 1) + ' — Suporte CDT'} value={p.treatyReference} onChange={e => patch({treatyReference: e.target.value})}/></label>
<Evidence label={'Utilização anterior do crédito — país ' + (index + 1)} value={p.previousUse} change={previousUse => patch({previousUse})}/>
<p>Utilização anterior é crédito já deduzido e documentado. Não é a soma de simulações históricas nem o imposto pago acima dos limites.</p>
<button type="button" onClick={() => patch({payments: [...p.payments, {...newInternationalProof(crypto.randomUUID()), subjectNuit: p.subjectNuit, year: p.year}]})}>Registar imposto estrangeiro na posição {index + 1}</button>
{p.payments.map((r, ri) => {
const change = (v: Partial<InternationalTaxProof>) => paymentChange({...r, ...v});
return <fieldset key={r.id} className="rounded border p-2"><legend>Imposto estrangeiro {index + 1}.{ri + 1}</legend>
{([['document', 'Comprovativo'], ['issuer', 'Emitente'], ['date', 'Data (AAAA-MM-DD)'], ['subjectNuit', 'NUIT titular'], ['year', 'Exercício do pagamento'], ['currency', 'Moeda'], ['proofAmount', 'Total comprovado — MZN'], ['amount', 'Parcela de imposto seleccionado — MZN'], ['reference', 'Suporte do pagamento e afectação'], ['taxReference', 'Suporte da natureza de imposto sobre o rendimento']] as const).map(([key, label]) => <label key={key} className="block">{label}<input aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — ' + label} value={r[key]} onChange={e => change({[key]: e.target.value})}/></label>)}
{(r.exerciseReference || r.date && !r.date.startsWith(p.year + '-')) && <label className="block">Suporte da correspondência da data ao exercício<input aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — Correspondência temporal'} value={r.exerciseReference} onChange={e => change({exerciseReference: e.target.value})}/></label>}
<label className="block">Pagamento efectivo<select aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — Efectivo'} value={r.effective} onChange={e => change({effective: e.target.value as InternationalTaxProof['effective']})}><option value="">Informação em falta</option><option value="yes">Sim — comprovado</option><option value="no">Não — previsão/outra situação</option></select></label>
<label className="block">Imposto sobre o rendimento elegível<select aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — Natureza'} value={r.incomeTax} onChange={e => change({incomeTax: e.target.value as InternationalTaxProof['incomeTax']})}><option value="">Informação em falta</option><option value="yes">Sim — natureza documentada</option><option value="no">Não — outra natureza, sem crédito presumido</option></select></label>
<button type="button" onClick={() => patch({payments: p.payments.filter(x => x.id !== r.id)})}>Retirar imposto estrangeiro {index + 1}.{ri + 1}</button>
</fieldset>;
})}
<button type="button" onClick={() => update({...c, positions: c.positions.filter(x => x.id !== p.id)})}>Retirar posição internacional {index + 1}</button>
</fieldset>;
})}
</fieldset>;
}
+3
View File
@@ -0,0 +1,3 @@
// Derived code projection of resources/data/iso3166.tab; keep the existing PHP catalogue authoritative.
// Source SHA256 (LF): 837c80785080c8433fd9d4ea87e78f161ac7a40389301c5153d4f90198baeb2a; regenerate with node tools/country-iso-projection.mjs
export const countryIsoCodes = 'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW'.split(' ');
+44 -14
View File
@@ -1,3 +1,4 @@
import {validateInternational,internationalProofValue,internationalGroupPending,internationalPosition,internationalDeduction,internationalCreditSource,newInternationalCredits,type InternationalCredits,type InternationalTaxProof} from './irpc-international-credit.ts';
import {validatePec,pecPaymentValue,pecDeduction,pecSource,type PecPayments} from './irpc-pec.ts';
import {normalBook,normalCurrent} from './irpc-normal.ts';
import {irpcBook,irpcCurrent} from './irpc-withholdings.ts';
@@ -13,11 +14,25 @@ type Group={state:State;reference:string};
export type AnnualWithholding={id:string;rowId:string;paymentId:string;sourceKey:string;subjectNuit:string;year:string;amount:string;incomeIncluded:''|'yes'|'no';reference:string};
export type AnnualPpc={id:string;document:string;issuer:string;date:string;subjectNuit:string;year:string;currency:string;proofAmount:string;amount:string;effective:''|'yes'|'no';reference:string};
type AnnualRecord={version:typeof annualSettlementVersion;key:string;createdAt:string;model:AnnualModel};
export type AnnualBook={version:1;subjectNuit:string;year:string;reference:string;assessment:''|'normal'|'special';assessmentKey:string;assessmentReference:string;withholdings:Group&{rows:AnnualWithholding[]};ppc:Group&{rows:AnnualPpc[]};pec:Evidence;pecPayments?:PecPayments;credits:Evidence;result?:AnnualRecord;history:AnnualRecord[]};
export const annualSource=(b:AnnualBook)=>b.pecPayments?annualSettlementSource.replace('PEC/créditos positivos','Créditos próprios positivos')+' '+pecSource:annualSettlementSource;
export type AnnualBook={version:1;subjectNuit:string;year:string;reference:string;assessment:''|'normal'|'special';assessmentKey:string;assessmentReference:string;withholdings:Group&{rows:AnnualWithholding[]};ppc:Group&{rows:AnnualPpc[]};pec:Evidence;pecPayments?:PecPayments;credits:Evidence;internationalCredits?:InternationalCredits;result?:AnnualRecord;history:AnnualRecord[]};
export const annualSource=(b:AnnualBook)=>{
const excluded=b.internationalCredits?(b.pecPayments?'Créditos económicos/benefícios positivos':'PEC/créditos económicos/benefícios positivos'):(b.pecPayments?'Créditos próprios positivos':'PEC/créditos positivos');
return annualSettlementSource.replace('PEC/créditos positivos',excluded)+(b.pecPayments?' '+pecSource:'')+(b.internationalCredits?' '+internationalCreditSource:'');
};
export function prepareInternationalBook(b:AnnualBook):AnnualBook{
if(b.internationalCredits)return b;
const c=newInternationalCredits(),old=b.credits;
if(old.state==='absent'||old.state==='known'&&old.amount==='0'){
c.economic=structuredClone(old);c.benefits=structuredClone(old);
if(old.state==='absent'){c.state='absent';c.reference=old.reference;}
}
return{...b,internationalCredits:c};
}
const evidence=():Evidence=>({state:'',amount:'',reference:''});
export const newAnnualBook=():AnnualBook=>({version:1,subjectNuit:'',year:'',reference:'',assessment:'',assessmentKey:'',assessmentReference:'',withholdings:{state:'',reference:'',rows:[]},ppc:{state:'',reference:'',rows:[]},pec:evidence(),credits:evidence(),history:[]});
export const newAnnualPpc=(id:string):AnnualPpc=>({id,document:'',issuer:'',date:'',subjectNuit:'',year:'',currency:'MZN',proofAmount:'',amount:'',effective:'',reference:''});
export const newInternationalProof=(id:string):InternationalTaxProof=>({...newAnnualPpc(id),incomeTax:'',taxReference:'',exerciseReference:''});
const zero=()=>fraction(0n),cmp=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator);
const iso=(s:string)=>/^20\d{2}-\d{2}-\d{2}$/.test(s)&&Number.isFinite(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
const moneyInput=(s:string)=>{if(!/^\d{1,18}(\.\d{1,2})?$/.test(s))throw Error('MontanteMZN em falta/inválido;máximo dois decimais,sem zero presumido.');return decimalFraction(s);};
@@ -27,6 +42,7 @@ function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_sn
function books(d:Record<string,unknown>):Record<string,AnnualBook>{const m=store(d).irpcAnnual;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('MapaL8 não recuperável;originais conservados.');return m;}
export function validateAnnual(b:AnnualBook){
if(b?.pecPayments!==undefined)validatePec(b.pecPayments);
if(b?.internationalCredits!==undefined)validateInternational(b.internationalCredits);
if(!b||b.version!==1||['subjectNuit','year','reference','assessmentKey','assessmentReference'].some(k=>typeof b[k as 'year']!=='string')||!['','normal','special'].includes(b.assessment)||!Array.isArray(b.history))throw Error('PreparaçãoL8 não recuperável.');
for(const g of[b.withholdings,b.ppc,b.pec,b.credits])if(!g||!['','known','absent'].includes(g.state)||typeof g.reference!=='string')throw Error('EstadoL8 não recuperável.');
for(const g of[b.pec,b.credits])if(typeof g.amount!=='string')throw Error('CréditoL8 não recuperável.');
@@ -56,7 +72,7 @@ export function reuseAnnualWithholding(d:Record<string,unknown>,b:AnnualBook,row
if(b.withholdings.rows.some(r=>r.rowId===rowId&&r.paymentId===paymentId))throw Error('Retenção/prova já seleccionada;altere a parcela existente sem duplicar.');
return{...b,withholdings:{...b.withholdings,rows:[...b.withholdings.rows,{id,rowId,paymentId,sourceKey:o.key,subjectNuit:b.subjectNuit,year:o.year,amount:o.amount,incomeIncluded:'',reference:''}]}};
}
function dependencies(d:Record<string,unknown>){let normal:unknown,L5:unknown;try{const a=annualAssessment(d);normal=[a.record,a.current];}catch(e){normal=String(e);}try{const b=irpcBook(d);L5=[b.rows,b.payments,b.result,irpcCurrent(d,b)];}catch(e){L5=String(e);}return[normal,L5,Object.entries(books(d)).filter(([k])=>k!==acquisitionScope(d)).map(([k,b])=>[k,b.subjectNuit,b.year,b.withholdings,b.ppc,...(b.pecPayments?[b.pecPayments]:[])])];}
function dependencies(d:Record<string,unknown>){let normal:unknown,L5:unknown;try{const a=annualAssessment(d);normal=[a.record,a.current];}catch(e){normal=String(e);}try{const b=irpcBook(d);L5=[b.rows,b.payments,b.result,irpcCurrent(d,b)];}catch(e){L5=String(e);}return[normal,L5,Object.entries(books(d)).filter(([k])=>k!==acquisitionScope(d)).map(([k,b])=>[k,b.subjectNuit,b.year,b.withholdings,b.ppc,...(b.pecPayments?[b.pecPayments]:[]),...(b.internationalCredits?[b.internationalCredits]:[])])];}
function key(d:Record<string,unknown>,b:AnnualBook){const{result:_,history:__,...input}=b;return closureFingerprint([annualSettlementVersion,acquisitionScope(d),input,dependencies(d)]);}
export function annualCurrent(d:Record<string,unknown>,b:AnnualBook){return !!b.result&&b.result.key===key(d,b);}
export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
@@ -72,8 +88,8 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
const group=(g:Group,rows:unknown[],label:string)=>{if(!g.state||!g.reference.trim())pending.push(label+':identifique valor conhecido,ausência declarada ou informação em falta com suporte.');if(g.state==='absent'&&rows.length)pending.push(label+':ausência declarada contradiz parcelas presentes;conserve e reconcilie.');if(g.state==='known'&&!rows.length)pending.push(label+':não há comprovativos seleccionados;ausência exige declaração própria.');};
group(b.withholdings,b.withholdings.rows,'Retenções sofridas por conta');group(b.ppc,b.ppc.rows,'PPC efectuados');
try{if(b.withholdings.state==='absent'&&options.some(o=>o.ready&&o.year===b.year&&cmp(moneyInput(o.amount),zero())>0n))pending.push('Ausência de retenções sofridas contradiz provaL5 efectiva por conta no exercício;reconcilie sem a ignorar.');}catch(e){pending.push('Reconciliação da ausência de retenções: '+String(e));}
for(const[label,e]of[...(b.pecPayments?[]:[['PEC',b.pec]as const]),['Créditos com regras próprias',b.credits]as const]){try{if(!e.state||!e.reference.trim())throw Error('presença/ausência e suporte em falta');if(e.state==='absent'){if(e.amount.trim())throw Error('ausência contradiz montante preenchido');}else if(cmp(moneyInput(e.amount),zero())!==0n)throw Error('montante positivo fora deste recorte:corresponder ordem,limite e transporte próprios;sem abatimento ou reembolso automático');}catch(err){pending.push(label+': '+String(err));}}
const claims:Array<{id:string;kind:'withholding'|'ppc'|'pec';previouslyUsed?:FiscalFraction;identity:string;proofAmount:FiscalFraction|null;value:FiscalFraction|null;sourceKey:string;allocationAmount:FiscalFraction|null;document:string;issuer:string;date:string;subjectNuit:string;year:string;reason:string}>=[];
for(const[label,e]of[...(b.pecPayments?[]:[['PEC',b.pec]as const]),...(b.internationalCredits?[]:[['Créditos com regras próprias',b.credits]as const])]){try{if(!e.state||!e.reference.trim())throw Error('presença/ausência e suporte em falta');if(e.state==='absent'){if(e.amount.trim())throw Error('ausência contradiz montante preenchido');}else if(cmp(moneyInput(e.amount),zero())!==0n)throw Error('montante positivo fora deste recorte:corresponder ordem,limite e transporte próprios;sem abatimento ou reembolso automático');}catch(err){pending.push(label+': '+String(err));}}
const claims:Array<{id:string;kind:'withholding'|'ppc'|'pec'|'foreign_credit';country?:string;positionId?:string;previouslyUsed?:FiscalFraction;identity:string;proofAmount:FiscalFraction|null;value:FiscalFraction|null;sourceKey:string;allocationAmount:FiscalFraction|null;document:string;issuer:string;date:string;subjectNuit:string;year:string;reason:string}>=[];
for(const r of b.withholdings.rows){const o=options.find(o=>o.rowId===r.rowId&&o.paymentId===r.paymentId),issues:string[]=[];let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null;
try{if(!o||!o.ready||r.sourceKey!==o.key)throw Error(o?.reason||'provaL5 ausente ou alterada;reutilize sem duplicar');if(r.subjectNuit!==b.subjectNuit||r.year!==b.year||o.year!==b.year||!o.date.startsWith(b.year+'-'))throw Error('entidade/exercício da retenção não correspondem ao fecho');if(r.incomeIncluded!=='yes'||!r.reference.trim())throw Error('confirme o próprio beneficiário/NUIT e inclusão do rendimento na base do exercício com suporte');value=moneyInput(r.amount);proofAmount=moneyInput(o.proofAmount);if(cmp(value,moneyInput(o.amount))>0n)throw Error('parcela ultrapassa a retenção efectivamente comprovada e afectada emL5');if(b.withholdings.rows.filter(x=>x.rowId===r.rowId&&x.paymentId===r.paymentId).length!==1)throw Error('retenção/prova repetida;uma afectação por linhaL5');if(assessment?.record?.model.special?.cooperative.status==='calculated'&&cmp(value,zero())>0n)throw Error('cooperativa integralmente elegível declara ausência de rendimentos retidos;segregue o casoL7 antes do crédito anual');}catch(e){issues.push(String(e));value=null;}
claims.push({id:r.id,kind:'withholding',identity:o?.identity??'',proofAmount,value,sourceKey:r.sourceKey,allocationAmount:o&&value?moneyInput(o.amount):null,document:o?.document??r.paymentId,issuer:o?.issuer??'',date:o?.date??'',subjectNuit:r.subjectNuit,year:r.year,reason:issues.join(' ')});
@@ -83,23 +99,33 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
claims.push({id:r.id,kind:'ppc',identity:proofIdentity(r.issuer,r.document,r.date),proofAmount,value,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason:issues.join(' ')});
}
if(b.pecPayments){group(b.pecPayments,b.pecPayments.rows,'PEC pagos no próprio exercício');for(const r of b.pecPayments.rows){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,previouslyUsed:FiscalFraction|undefined;let reason='';try{({value,proofAmount,previouslyUsed}=pecPaymentValue(r,b.subjectNuit,b.year));}catch(e){reason=String(e);}claims.push({id:r.id,kind:'pec',identity:proofIdentity(r.issuer,r.document,r.date),value,proofAmount,previouslyUsed,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason});}}
const elsewhere:Array<{identity:string;subjectNuit:string;year:string;value:FiscalFraction;proofAmount:FiscalFraction;sourceKey:string;kind:'ppc'|'pec'|'withholding';previouslyUsed?:FiscalFraction}>=[];
for(const[scope,other]of Object.entries(books(d))){if(scope===acquisitionScope(d))continue;try{validateAnnual(other);for(const r of other.pecPayments?.rows??[]){const p=pecPaymentValue(r,other.subjectNuit,other.year);elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...p,sourceKey:'',kind:'pec'});}for(const r of other.ppc.rows)elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(r.proofAmount),sourceKey:'',kind:'ppc'});for(const r of other.withholdings.rows){const source=(store(d).irpcWithholdings??{})[scope];const row=source?.rows?.find((x:{id:string})=>x.id===r.rowId),p=source?.payments?.find((x:{id:string})=>x.id===r.paymentId);if(row&&p)elsewhere.push({identity:proofIdentity(row.counterparty,p.reference,p.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(p.amount),sourceKey:r.sourceKey,kind:'withholding'});}}catch(e){if(other?.subjectNuit===b.subjectNuit)pending.push('Outro mapa da mesma entidade não recuperável para conferir duplicações: '+scope+';'+String(e));}}
if(b.internationalCredits){
pending.push(...internationalGroupPending(b.internationalCredits));
const countries=b.internationalCredits.positions.map(p=>p.country);
if(new Set(countries).size!==countries.length)pending.push('Um país/exercício tem várias posições;consolide rendimentos e comprovativos sob uma única fracção nacional,sem repetir o limite.');
for(const p of b.internationalCredits.positions)for(const r of p.payments){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,reason='';try{({value,proofAmount}=internationalProofValue(r,b.subjectNuit,b.year));}catch(e){reason=String(e);}claims.push({id:r.id,kind:'foreign_credit',country:p.country,positionId:p.id,identity:proofIdentity(r.issuer,r.document,r.date),proofAmount,value,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason});}
}
const elsewhere:Array<{identity:string;subjectNuit:string;year:string;value:FiscalFraction;proofAmount:FiscalFraction;sourceKey:string;kind:'ppc'|'pec'|'withholding'|'foreign_credit';country?:string;previouslyUsed?:FiscalFraction}>=[];
for(const[scope,other]of Object.entries(books(d))){if(scope===acquisitionScope(d))continue;try{validateAnnual(other);for(const p of other.internationalCredits?.positions??[]){if(other.subjectNuit===b.subjectNuit&&other.year===b.year&&b.internationalCredits?.positions.some(x=>x.country===p.country))pending.push('País '+p.country+' já preparado noutro mapa da entidade/exercício;consolide a fracção nacional e usos sem duplicar quotas.');for(const r of p.payments){const v=internationalProofValue(r,other.subjectNuit,other.year);elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...v,sourceKey:'',kind:'foreign_credit',country:p.country});}}for(const r of other.pecPayments?.rows??[]){const p=pecPaymentValue(r,other.subjectNuit,other.year);elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...p,sourceKey:'',kind:'pec'});}for(const r of other.ppc.rows)elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(r.proofAmount),sourceKey:'',kind:'ppc'});for(const r of other.withholdings.rows){const source=(store(d).irpcWithholdings??{})[scope];const row=source?.rows?.find((x:{id:string})=>x.id===r.rowId),p=source?.payments?.find((x:{id:string})=>x.id===r.paymentId);if(row&&p)elsewhere.push({identity:proofIdentity(row.counterparty,p.reference,p.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(p.amount),sourceKey:r.sourceKey,kind:'withholding'});}}catch(e){if(other?.subjectNuit===b.subjectNuit)pending.push('Outro mapa da mesma entidade não recuperável para conferir duplicações: '+scope+';'+String(e));}}
for(const c of claims){if(!c.value||!c.proofAmount)continue;const same=claims.filter(x=>x.identity===c.identity&&x.value),other=elsewhere.filter(x=>x.identity===c.identity);let issue='';
if(same.filter(x=>x.kind!=='withholding').length>1||new Set(same.map(x=>x.kind)).size>1)issue='Mesmo comprovativo repetido ou usado com naturezas diferentes (retenção/PPC/PEC);reconcilie sem dedução dupla.';
if(same.filter(x=>x.kind!=='withholding').length>1||new Set(same.map(x=>x.kind)).size>1)issue='Mesmo comprovativo repetido ou usado com naturezas diferentes (retenção/PPC/PEC/crédito internacional);reconcilie sem dedução dupla.';
if(other.some(x=>x.subjectNuit!==b.subjectNuit||x.year!==b.year))issue='Mesmo comprovativo já associado a outra entidade/exercício;reconcilie a titularidade e a afectação.';
if([...same,...other].some(x=>x.proofAmount&&cmp(x.proofAmount,c.proofAmount!)!==0n)||cmp(addFractions([...same.flatMap(x=>x.value?[x.value]:[]),...other.map(x=>x.value)]),c.proofAmount)>0n)issue='Comprovativo utilizado além do montante disponível ou com montantes divergentes neste/outro mapa.';
if(c.kind==='pec'&&c.previouslyUsed){if(other.some(x=>x.kind!=='pec')||other.some(x=>!x.previouslyUsed||cmp(x.previouslyUsed,c.previouslyUsed!)!==0n)||cmp(addFractions([c.previouslyUsed,...same.flatMap(x=>x.value?[x.value]:[]),...other.map(x=>x.value)]),c.proofAmount)>0n)issue='PEC com utilização anterior divergente, natureza incompatível ou parcelas acima do saldo disponível.';}
if(other.some(x=>x.kind!==c.kind))issue='Comprovativo já afectado a outra natureza (retenção/PPC/PEC);sem dupla dedução.';
if(other.some(x=>x.kind!==c.kind))issue='Comprovativo já afectado a outra natureza (retenção/PPC/PEC/crédito internacional);sem dupla dedução.';
if(c.kind==='foreign_credit'&&other.some(x=>x.country!==c.country))issue='Comprovativo estrangeiro associado a outro país/natureza;reconcilie sem duplicação.';
if(c.kind==='withholding'&&c.sourceKey&&c.allocationAmount&&cmp(addFractions([...same.filter(x=>x.sourceKey===c.sourceKey).flatMap(x=>x.value?[x.value]:[]),...other.filter(x=>x.sourceKey===c.sourceKey).map(x=>x.value)]),c.allocationAmount)>0n)issue='Mesma afectação L5 reutilizada além da parcela disponível, ainda que o comprovativo global seja maior.';
if(issue)c.reason=[c.reason,issue].filter(Boolean).join(' ');
}
for(const c of claims)if(c.reason)pending.push((c.document||c.id)+': '+c.reason);
const knownWithholdings=addFractions(claims.filter(c=>c.kind==='withholding'&&!c.reason&&c.value).map(c=>c.value!)),knownPpc=addFractions(claims.filter(c=>c.kind==='ppc'&&!c.reason&&c.value).map(c=>c.value!));
const ready=pending.length===0&&colecta!==null,pec=b.pecPayments?pecDeduction(addFractions(claims.filter(c=>c.kind==='pec'&&!c.reason&&c.value).map(c=>c.value!)),colecta,ready):null,arithmeticBalance=ready?subtractFractions(subtractFractions(pec?.afterPec??colecta!,knownWithholdings),knownPpc):null;
const positions=b.internationalCredits?.positions.map(p=>{const own=claims.filter(c=>c.kind==='foreign_credit'&&c.positionId===p.id);const m=internationalPosition(p,addFractions(own.filter(c=>!c.reason&&c.value).map(c=>c.value!)),own.filter(c=>c.reason).map(c=>c.reason),b.subjectNuit,b.year,b.assessmentKey);pending.push(...m.pending.map(e=>'País '+p.country+': '+e));return m;})??[];
const ready=pending.length===0&&colecta!==null,international=b.internationalCredits?internationalDeduction(positions,colecta,ready):null,creditCollecta=international?.afterCredit??colecta;
const pec=b.pecPayments?pecDeduction(addFractions(claims.filter(c=>c.kind==='pec'&&!c.reason&&c.value).map(c=>c.value!)),creditCollecta,ready):null,arithmeticBalance=ready?subtractFractions(subtractFractions(pec?.afterPec??creditCollecta!,knownWithholdings),knownPpc):null;
if(international&&international.insufficientCollecta&&cmp(international.insufficientCollecta,zero())>0n)warn.push('Crédito elegível sem colecta suficiente:sem reembolso,transporte ou escolha automática da afectação por país.');
let payable:FiscalFraction|null=null,recoverable:FiscalFraction|null=null,belowMinimum:FiscalFraction|null=null;let status:'payable'|'recoverable'|'balanced'|'below_minimum'|'indeterminate'='indeterminate';
if(arithmeticBalance){const n=BigInt(arithmeticBalance.numerator),abs=fraction(n<0n?-n:n,BigInt(arithmeticBalance.denominator));payable=zero();recoverable=zero();belowMinimum=zero();if(!n)status='balanced';else if(cmp(abs,fraction(100n))<0n){status='below_minimum';belowMinimum=abs;warn.push('Art27(6):saldo inferior a100MZN,sem cobrança/reembolso neste recorte;aritmética conservada,não é transporte de crédito.');}else if(n>0n){status='payable';payable=abs;}else{status='recoverable';recoverable=abs;}}
return{version:annualSettlementVersion,input:structuredClone(input),subjectNuit:b.subjectNuit,year:b.year,assessment:assessment?.record?{key:b.assessmentKey,kind:b.assessment,version:assessment.record.model.version,createdAt:assessment.record.createdAt,colecta,period:assessment.period,origin:assessment.record.model.origin}:null,claims,pecDeduction:pec,knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[b.pecPayments?'L8 parcial:transporte/caducidade de PEC, cálculo da obrigação, créditos próprios positivos, tributação autónoma, períodos especiais e outras moedas separados. Excedente PEC não é reembolso nem saldo transportado oficialmente.':'L8 parcial:PEC e créditos positivos,tributação autónoma,períodos especiais e pagamentos noutra moeda exigem regras próprias.','PPC aqui é pagamento comprovado;não cálculo automático da obrigação ou calendário.','Retenção sofrida não é retenção entregue pela empresa por conta de terceiros.','Valores racionais exactos;camposM20/M22 e representação não presumidos.','Saldo simulado por colecta projectada;sem pagamento,submissão ou reembolso automático.']};
return{version:annualSettlementVersion,input:structuredClone(input),subjectNuit:b.subjectNuit,year:b.year,assessment:assessment?.record?{key:b.assessmentKey,kind:b.assessment,version:assessment.record.model.version,createdAt:assessment.record.createdAt,colecta,period:assessment.period,origin:assessment.record.model.origin}:null,claims,internationalCredit:international,pecDeduction:pec,knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.internationalCredits?['Crédito internacional do próprio exercício:fracção nacional,convenção e montantes MZN são documentados;sem cálculo presumido,transporte ou consumo oficial. Créditos económicos/benefícios positivos ficam separados.']:[]),b.internationalCredits?'L8 parcial:créditos económicos/benefícios positivos,transporte/caducidade,obrigações,tributação autónoma,períodos especiais e outras moedas separados. PEC sem reembolso ou transporte automático.':(b.pecPayments?'L8 parcial:transporte/caducidade de PEC, cálculo da obrigação, créditos próprios positivos, tributação autónoma, períodos especiais e outras moedas separados. Excedente PEC não é reembolso nem saldo transportado oficialmente.':'L8 parcial:PEC e créditos positivos,tributação autónoma,períodos especiais e pagamentos noutra moeda exigem regras próprias.'),'PPC aqui é pagamento comprovado;não cálculo automático da obrigação ou calendário.','Retenção sofrida não é retenção entregue pela empresa por conta de terceiros.','Valores racionais exactos;camposM20/M22 e representação não presumidos.','Saldo simulado por colecta projectada;sem pagamento,submissão ou reembolso automático.']};
}
export type AnnualModel=ReturnType<typeof calculateAnnual>;
export function recordAnnual(d:Record<string,unknown>,b:AnnualBook,now=new Date().toISOString()):AnnualBook{return{...b,history:b.result?[...b.history,structuredClone(b.result)]:b.history,result:{version:annualSettlementVersion,key:key(d,b),createdAt:now,model:calculateAnnual(d,b)}};}
@@ -114,10 +140,14 @@ export function annualReportRows(d:Record<string,unknown>):Array<[string,string,
'NUIT: '+m.subjectNuit+'; exercício: '+m.year+'; colecta: '+show(m.assessment?.colecta??null)+'; origem: '+(m.assessment?.kind==='special'?'L7 qualificado':'L6 normal')+'. '+(annualCurrent(d,b)?'Entradas correspondentes.':'Histórico; origem/entradas alteradas: recalcule o saldo corrente.')+' Saldo simulado, não liquidação submetida.',
m.input.reference+'; '+m.input.assessmentReference+'; '+m.source],
...m.claims.map(c=>[
'IRPC — L8',(c.kind==='withholding'?'Retenção sofrida':c.kind==='pec'?'PEC':'PPC')+' — '+(c.document||c.id),
'IRPC — L8',(c.kind==='withholding'?'Retenção sofrida':c.kind==='pec'?'PEC':c.kind==='foreign_credit'?'Imposto estrangeiro':'PPC')+' — '+(c.document||c.id),
'Entidade: '+c.subjectNuit+'; exercício: '+c.year+'; emitente: '+c.issuer+'; data: '+c.date+'; comprovado: '+show(c.proofAmount)+'; parcela: '+(c.reason?'Indeterminado':show(c.value))+'. '+(c.reason||'Reconciliada sem utilização duplicada.'),
c.kind==='withholding'?m.input.withholdings.reference+'; '+(m.input.withholdings.rows.find(r=>r.id===c.id)?.reference??''):c.kind==='pec'?(m.input.pecPayments?.reference??'')+'; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.reference??'')+'; utilizações anteriores: '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.previouslyUsed??'')+' MZN; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.priorUseReference??''):m.input.ppc.reference+'; '+(m.input.ppc.rows.find(r=>r.id===c.id)?.reference??'')
c.kind==='withholding'?m.input.withholdings.reference+'; '+(m.input.withholdings.rows.find(r=>r.id===c.id)?.reference??''):c.kind==='foreign_credit'?(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.positions.flatMap(p=>p.payments).find(r=>r.id===c.id)?.reference??'')+'; '+(m.input.internationalCredits?.positions.flatMap(p=>p.payments).find(r=>r.id===c.id)?.exerciseReference??''):c.kind==='pec'?(m.input.pecPayments?.reference??'')+'; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.reference??'')+'; utilizações anteriores: '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.previouslyUsed??'')+' MZN; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.priorUseReference??''):m.input.ppc.reference+'; '+(m.input.ppc.rows.find(r=>r.id===c.id)?.reference??'')
]as[string,string,string,string]),
...(m.internationalCredit?[
...m.internationalCredit.positions.map(p=>['IRPC — L8','Crédito internacional — '+p.country,'Imposto pago seleccionado: '+show(p.paid)+'; fracção nacional: '+show(p.nationalLimit)+'; limite CDT: '+(p.input.treaty==='no'?'Não aplicável documentado':show(p.treatyLimit))+'; elegível: '+show(p.eligible)+'; utilização anterior: '+show(p.previousUse)+'; disponível: '+show(p.available)+'; imposto acima dos limites: '+show(p.aboveLimit)+'. '+p.pending.join(' | '),p.input.incomeReference+'; '+p.input.nationalLimitReference+'; '+p.input.treatyReference+'; '+p.input.previousUse.reference]as[string,string,string,string]),
['IRPC — L8','Crédito internacional — antes do PEC','Disponível conhecido: '+show(m.internationalCredit.available)+'; dedução: '+show(m.internationalCredit.deducted)+'; colecta após crédito: '+show(m.internationalCredit.afterCredit)+'; elegível não deduzido por insuficiência de colecta: '+show(m.internationalCredit.insufficientCollecta)+'. Sem reembolso,transporte ou afectação oficial por país. Estado: '+stateLabel(m.input.internationalCredits?.state??'')+'. Créditos económicos/benefícios: '+stateLabel(m.input.internationalCredits?.economic.state??'')+' / '+stateLabel(m.input.internationalCredits?.benefits.state??'')+'.',m.internationalCredit.source+'; '+(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.economic.reference??'')+'; '+(m.input.internationalCredits?.benefits.reference??'')]as[string,string,string,string]
]:[]),
...(m.pecDeduction?[[
'IRPC — L8','PEC — dedução limitada à colecta',
'Estado: '+stateLabel(m.input.pecPayments?.state??'')+'; parcelas disponíveis: '+show(m.pecDeduction.available)+'; dedução: '+show(m.pecDeduction.deducted)+'; colecta após PEC, antes de retenções/PPC: '+show(m.pecDeduction.afterPec)+'; parcela seleccionada não deduzida: '+show(m.pecDeduction.remaining)+'. Excedente sem reembolso PEC; transporte/caducidade não automáticos. Subtotais não resolvem informação em falta.',
@@ -128,7 +158,7 @@ export function annualReportRows(d:Record<string,unknown>):Array<[string,string,
['IRPC — L8','Estados dos comprovativos',
'Retenções sofridas: '+stateLabel(m.input.withholdings.state)+'; PPC: '+stateLabel(m.input.ppc.state)+'. Subtotais conhecidos não substituem informação em falta.',m.input.withholdings.reference+'; '+m.input.ppc.reference],
['IRPC — L8','PEC e créditos separados',
(m.input.pecPayments?'Declaração PEC anterior conservada (substituída nesta execução pelo detalhe comprovado): ':'PEC: ')+stateLabel(m.input.pec.state)+'; montante declarado: '+(m.input.pec.amount||'Não preenchido')+'; créditos: '+stateLabel(m.input.credits.state)+'; montante declarado: '+(m.input.credits.amount||'Não preenchido')+'. Sem dedução, transporte ou reembolso presumidos. '+m.limitations.join(' '),m.input.pec.reference+'; '+m.input.credits.reference]
(m.input.pecPayments?'Declaração PEC anterior conservada (substituída nesta execução pelo detalhe comprovado): ':'PEC: ')+stateLabel(m.input.pec.state)+'; montante declarado: '+(m.input.pec.amount||'Não preenchido')+(m.input.internationalCredits?'; resumo anterior de créditos conservado,substituído nesta execução pelo detalhe: ':'; créditos: ')+stateLabel(m.input.credits.state)+'; montante declarado: '+(m.input.credits.amount||'Não preenchido')+'. Sem dedução, transporte ou reembolso presumidos. '+m.limitations.join(' '),m.input.pec.reference+'; '+m.input.credits.reference]
];
}catch(e){return[['IRPC — L8','Não recuperável',String(e),'Originais e históricos conservados.']];}
}
@@ -0,0 +1,108 @@
import type {AnnualPpc} from './irpc-annual-settlement.ts';
import {countryIsoCodes} from './country-iso-codes.ts';
import {addFractions, subtractFractions, decimalFraction, fraction, type FiscalFraction} from './fiscal-rational.ts';
export const internationalCreditVersion = 'IRPC-international-credit/1';
export const internationalCreditSource = 'CIRPC art. 65(1–2), Lei 34/2007, PDF18/p.768-(72); ordem face ao PEC: art.66 e RIRPC21. Recorte: próprio exercício 2026, residente geral, ano civil, valores comprovados em MZN. Menor entre imposto sobre o rendimento pago no estrangeiro, fracção nacional documentada e limite da convenção quando aplicável; depois utilizações anteriores comprovadas e limite da colecta. Não determina a fracção nacional, a aplicabilidade da convenção ou a conversão cambial por presunção. Sem transporte, reembolso do excedente ou consumo de saldos oficiais.';
export type CreditState = '' | 'known' | 'absent';
export type CreditEvidence = {state: CreditState; amount: string; reference: string};
export type InternationalTaxProof = AnnualPpc & {incomeTax: '' | 'yes' | 'no'; taxReference: string; exerciseReference: string};
export type InternationalPosition = {
id: string; country: string; subjectNuit: string; year: string; assessmentKey: string; qualificationCountry: string;
incomeAmount: string; incomeIncluded: '' | 'yes' | 'no'; incomeReference: string;
nationalLimit: string; nationalLimitReference: string;
treaty: '' | 'yes' | 'no'; treatyLimit: string; treatyReference: string;
previousUse: CreditEvidence; payments: InternationalTaxProof[];
};
export type InternationalCredits = {
version: 1; state: CreditState; reference: string; positions: InternationalPosition[];
economic: CreditEvidence; benefits: CreditEvidence;
};
const emptyEvidence = (): CreditEvidence => ({state: '', amount: '', reference: ''});
export const newInternationalCredits = (): InternationalCredits => ({version: 1, state: '', reference: '', positions: [], economic: emptyEvidence(), benefits: emptyEvidence()});
export const newInternationalPosition = (id: string): InternationalPosition => ({id, country: '', subjectNuit: '', year: '', assessmentKey: '', qualificationCountry: '', incomeAmount: '', incomeIncluded: '', incomeReference: '', nationalLimit: '', nationalLimitReference: '', treaty: '', treatyLimit: '', treatyReference: '', previousUse: emptyEvidence(), payments: []});
const states = ['', 'known', 'absent'];
const yesNo = ['', 'yes', 'no'];
const evidenceValid = (e: CreditEvidence) => e && states.includes(e.state) && typeof e.amount === 'string' && typeof e.reference === 'string';
const proofKeys = ['id','document','issuer','date','subjectNuit','year','currency','proofAmount','amount','effective','reference','incomeTax','taxReference','exerciseReference'];
const positionKeys = ['id','country','subjectNuit','year','assessmentKey','qualificationCountry','incomeAmount','incomeIncluded','incomeReference','nationalLimit','nationalLimitReference','treaty','treatyLimit','treatyReference'];
export function validateInternational(c: InternationalCredits) {
if (!c || c.version !== 1 || !states.includes(c.state) || typeof c.reference !== 'string' || !evidenceValid(c.economic) || !evidenceValid(c.benefits) || !Array.isArray(c.positions) || new Set(c.positions.map(p => p?.id)).size !== c.positions.length) throw Error('Créditos internacionais não recuperáveis; originais conservados.');
const proofIds: string[] = [];
for (const p of c.positions) {
if (!p || positionKeys.some(k => typeof (p as unknown as Record<string, unknown>)[k] !== 'string') || !yesNo.includes(p.incomeIncluded) || !yesNo.includes(p.treaty) || !evidenceValid(p.previousUse) || !Array.isArray(p.payments)) throw Error('Posição internacional não recuperável; originais conservados.');
for (const r of p.payments) {
if (!r || proofKeys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !yesNo.includes(r.effective) || !yesNo.includes(r.incomeTax)) throw Error('Comprovativo internacional não recuperável.');
proofIds.push(r.id);
}
}
if (new Set(proofIds).size !== proofIds.length) throw Error('Identificador de comprovativo internacional repetido.');
}
export const creditMoney = (raw: string) => {
if (!/^\d{1,18}(\.\d{1,2})?$/.test(raw)) throw Error('Montante MZN em falta/inválido; zero exige declaração própria.');
return decimalFraction(raw);
};
export const creditCompare = (a: FiscalFraction, b: FiscalFraction) => BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator);
const creditLimit = (raw: string) => {
const rational = /^(\d{1,18})\/(\d{1,18})$/.exec(raw);
if (rational) return fraction(BigInt(rational[1]), BigInt(rational[2]));
if (!/^\d{1,18}(\.\d{1,6})?$/.test(raw)) throw Error('Limite documentado em falta/inválido; use decimal ou fracção exacta n/d, sem arredondamento presumido.');
return decimalFraction(raw);
};
const zero = () => fraction(0n);
const minimum = (...values: FiscalFraction[]) => values.reduce((a, b) => creditCompare(a, b) < 0n ? a : b);
const isDate = (s: string) => /^20\d{2}-\d{2}-\d{2}$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s;
export function internationalProofValue(r: InternationalTaxProof, subjectNuit: string, year: string) {
if (!r.id.trim() || !r.document.trim() || !r.issuer.trim() || !r.reference.trim() || !r.taxReference.trim() || r.incomeTax !== 'yes' || r.effective !== 'yes' || r.currency !== 'MZN' || !isDate(r.date) || r.subjectNuit !== subjectNuit || r.year !== year || year !== '2026') throw Error('Crédito internacional: exige imposto sobre o rendimento efectivamente pago, prova/titular/exercício 2026 e MZN; previsões, outras naturezas e conversões não correspondidas ficam separados.');
if (!r.date.startsWith(year + '-') && !r.exerciseReference.trim()) throw Error('Data de pagamento fora do exercício: falta correspondência documental com o rendimento e imposto deste exercício; não é transporte presumido.');
const value = creditMoney(r.amount), proofAmount = creditMoney(r.proofAmount);
if (creditCompare(value, proofAmount) > 0n) throw Error('Parcela estrangeira ultrapassa o montante comprovado.');
return {value, proofAmount};
}
export function internationalGroupPending(c: InternationalCredits) {
const pending: string[] = [];
if (!c.state || !c.reference.trim()) pending.push('Crédito internacional: declare pagamentos conhecidos, ausência ou informação em falta, com suporte.');
if (c.state === 'absent' && c.positions.length) pending.push('Ausência de crédito internacional contradiz posições presentes; conserve e reconcilie.');
if (c.state === 'known' && !c.positions.length) pending.push('Crédito internacional conhecido sem posição/comprovativo; zero não é ausência presumida.');
for (const [label, e] of [['Dupla tributação económica', c.economic], ['Benefícios/créditos próprios', c.benefits]] as const) {
try {
if (!e.state || !e.reference.trim()) throw Error('estado e suporte em falta');
if (e.state === 'absent') {if (e.amount.trim()) throw Error('ausência contradiz montante');}
else if (creditCompare(creditMoney(e.amount), zero()) !== 0n) throw Error('valor positivo exige regra própria; não é deduzido nem ignorado');
} catch (e) {pending.push(label + ': ' + String(e));}
}
return pending;
}
export function internationalPosition(p: InternationalPosition, paid: FiscalFraction, proofIssues: string[], subjectNuit: string, year: string, assessmentKey: string) {
const pending = [...proofIssues];
let nationalLimit: FiscalFraction | null = null, treatyLimit: FiscalFraction | null = null, previousUse: FiscalFraction | null = null, eligible: FiscalFraction | null = null, available: FiscalFraction | null = null, aboveLimit: FiscalFraction | null = null;
try {
if (!p.id.trim() || !countryIsoCodes.includes(p.country) || p.country === 'MZ' || p.subjectNuit !== subjectNuit || p.year !== year || year !== '2026') throw Error('País estrangeiro do catálogo, titular e exercício não correspondem.');
if (p.qualificationCountry !== p.country) throw Error('O país mudou; confirme a inclusão e os limites documentados para o país seleccionado.');
if (p.assessmentKey !== assessmentKey || !p.assessmentKey) throw Error('Confirme a inclusão e a fracção nacional na colecta corrente; a origem mudou.');
if (p.incomeIncluded !== 'yes' || !p.incomeReference.trim()) throw Error('Falta inclusão documentada do rendimento estrangeiro na matéria colectável.');
const income = creditMoney(p.incomeAmount);
nationalLimit = creditLimit(p.nationalLimit);
if (!p.nationalLimitReference.trim()) throw Error('Fracção nacional exige suporte próprio; não é calculada nem presumida pelo portal.');
if (creditCompare(income, zero()) === 0n && creditCompare(nationalLimit, zero()) !== 0n) throw Error('Rendimento incluído zero contradiz fracção nacional positiva.');
if (!p.treaty || !p.treatyReference.trim()) throw Error('Aplicabilidade da convenção e suporte em falta.');
if (p.treaty === 'yes') treatyLimit = creditLimit(p.treatyLimit);
else if (p.treatyLimit.trim()) throw Error('Ausência de convenção contradiz limite preenchido.');
if (!p.previousUse.state || !p.previousUse.reference.trim()) throw Error('Utilizações anteriores do crédito em falta; histórico de simulação não é consumo oficial.');
if (p.previousUse.state === 'absent') {if (p.previousUse.amount.trim()) throw Error('Ausência de uso anterior contradiz montante');previousUse = zero();}
else previousUse = creditMoney(p.previousUse.amount);
if (!p.payments.length) throw Error('Sem comprovativos de imposto estrangeiro nesta posição.');
if (!pending.length) {
eligible = minimum(paid, nationalLimit, ...(treatyLimit ? [treatyLimit] : []));
aboveLimit = subtractFractions(paid, eligible);
if (creditCompare(previousUse, eligible) > 0n) throw Error('Utilização anterior excede o crédito elegível; reconcilie o saldo.');
available = subtractFractions(eligible, previousUse);
}
} catch (e) {pending.push(String(e));available = null;}
return {id: p.id, country: p.country, paid, nationalLimit, treatyLimit, previousUse, eligible, available, aboveLimit, pending, input: structuredClone(p)};
}
export function internationalDeduction(positions: ReturnType<typeof internationalPosition>[], colecta: FiscalFraction | null, ready: boolean) {
const available = addFractions(positions.flatMap(p => p.available && !p.pending.length ? [p.available] : []));
const deducted = ready && colecta ? minimum(available, colecta) : null;
return {version: internationalCreditVersion, positions, available, deducted, afterCredit: deducted && colecta ? subtractFractions(colecta, deducted) : null, insufficientCollecta: deducted ? subtractFractions(available, deducted) : null, source: internationalCreditSource, countryAllocationCreated: false, carryCreated: false, officialBalancesConsumed: false};
}
@@ -0,0 +1,62 @@
<?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 IrpcInternationalCreditDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_international_credit_delta_round_trips_with_frozen_history_and_unchanged_existing_records(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test',$target->name);
$this->assertSame(55441,(int)$target->port);
$snapshot=static fn(string $table):array=>collect(DB::select('select id::text as id,md5(row_to_json(t)::text) as hash from public."'.$table.'" t order by id'))->pluck('hash','id')->all();
$protected=[];
foreach(['opportunities','documents','opportunity_documents','simulation_form_drafts','users','organizations','audit_events']as$table)$protected[$table]=$snapshot($table);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::findOrFail(1021)); // Existing synthetic QA actor, no new grants.
$base=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-assessment-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$fixture=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-international-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$this->assertTrue($fixture['synthetic']);$this->assertFalse($fixture['actualConventionData']);
$personal=['person_type'=>$base['person_type']];unset($base['person_type']);
$id=null;$etag=null;$previous=null;
foreach($fixture['cases']as$name=>$book){
$draft=$base;$s=json_decode($draft['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
if($previous!==null)$book['history']=[$previous];
$s['irpcAnnual'][$draft['context_opportunity']]=$book;
$draft['modelo_a_snapshot']=json_encode($s,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE);
$payload=['personal_snapshot'=>$personal,'economic_snapshot'=>$draft];
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',$payload,['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,$payload,['If-Match'=>$etag])->assertOk();
$response=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$response->headers->get('ETag');$back=$response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($draft['business_plan_snapshot'],$back['business_plan_snapshot']);
$recovered=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
foreach(['irpcNormal','irpcWithholdings','books','currencyEvents','ispcQuarters']as$map)$this->assertSame($s[$map],$recovered[$map]);
$saved=$recovered['irpcAnnual'][$draft['context_opportunity']];$m=$saved['result']['model'];$credit=$m['internationalCredit'];
$this->assertSame($book,$saved);$this->assertSame('IRPC-international-credit/1',$credit['version']);$this->assertFalse($credit['carryCreated']);$this->assertFalse($credit['officialBalancesConsumed']);$this->assertFalse($credit['countryAllocationCreated']);$this->assertFalse($m['submitted']);
$this->assertSame(['numerator'=>'1408','denominator'=>'1'],$m['assessment']['colecta']);
if(in_array($name,['missing','duplicate'],true)){
$this->assertSame('indeterminate',$m['status']);$this->assertNull($m['arithmeticBalance']);$this->assertNull($credit['deducted']);$this->assertNull($m['pecDeduction']['deducted']);$this->assertNotEmpty($m['pending']);
}else{
$expected=['payable'=>['payable','700','8','1400','200'],'treaty'=>['payable','702','6','1402','200'],'limited-collecta'=>['recoverable','-500','1408','0','0'],'absent'=>['payable','708','0','1408','200'],'prior-use'=>['payable','703','5','1403','200']][$name];
$this->assertSame($expected[0],$m['status']);$this->assertEmpty($m['pending']);
$this->assertSame(['numerator'=>$expected[1],'denominator'=>'1'],$m['arithmeticBalance']);
$this->assertSame(['numerator'=>$expected[2],'denominator'=>'1'],$credit['deducted']);
$this->assertSame(['numerator'=>$expected[3],'denominator'=>'1'],$credit['afterCredit']);
$this->assertSame(['numerator'=>$expected[4],'denominator'=>'1'],$m['pecDeduction']['deducted']);
if($name==='limited-collecta'){$this->assertSame(['numerator'=>'592','denominator'=>'1'],$credit['insufficientCollecta']);$this->assertSame(['numerator'=>'200','denominator'=>'1'],$m['pecDeduction']['remaining']);}
}
if($previous!==null)$this->assertSame($previous,$saved['history'][0]);$previous=$saved['result'];
if(getenv('INVESTMOZ_INTERNATIONAL_QA_OUTPUT')==='1')file_put_contents(base_path('outputs/irpc-l8-international-20261027/recovered-'.$name.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
$preserved=[];
foreach($protected as$table=>$before){$this->assertSame($before,array_intersect_key($snapshot($table),$before),'Existing records changed: '.$table);$preserved[$table]=['existing_rows'=>count($before),'unchanged'=>true];}
if(getenv('INVESTMOZ_INTERNATIONAL_QA_OUTPUT')==='1')file_put_contents(base_path('outputs/irpc-l8-international-20261027/existing-records-preservation.json'),json_encode(['database'=>$target->name,'tables'=>$preserved,'transaction_rollback'=>true],JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT));
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
import {pecCase} from './irpc-pec-case.mjs';
import {prepareInternationalBook,newInternationalProof} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newInternationalPosition} from '../../../resources/js/lib/irpc-international-credit.ts';
export function internationalCase(tax='10',national='8',pec='200',ppc='500',rf='0') {
const c=pecCase(pec,ppc,rf);c.b=prepareInternationalBook(c.b);
const p={...newInternationalPosition('country-za'),country:'ZA',qualificationCountry:'ZA',subjectNuit:c.b.subjectNuit,year:c.b.year,assessmentKey:c.b.assessmentKey,incomeAmount:'100',incomeIncluded:'yes',incomeReference:'SYNTHETIC QA: foreign income included in valid 2026 base',nationalLimit:national,nationalLimitReference:'SYNTHETIC QA: documented national fraction, no automatic derivation',treaty:'no',treatyReference:'SYNTHETIC QA assumption only; not an assertion about actual ZA convention applicability',previousUse:{state:'known',amount:'0',reference:'SYNTHETIC QA: actual previous credit use known zero'},payments:[{...newInternationalProof('foreign-tax'),document:'SYNTHETIC-FOREIGN-TAX-2026',issuer:'SYNTHETIC QA foreign authority',date:'2026-09-30',subjectNuit:c.b.subjectNuit,year:c.b.year,proofAmount:tax,amount:tax,effective:'yes',incomeTax:'yes',reference:'SYNTHETIC QA paid MZN tax amount, no real receipt',taxReference:'SYNTHETIC QA income tax nature and entitlement'}]};
Object.assign(c.b.internationalCredits,{state:'known',reference:'SYNTHETIC QA complete inventory for entity/year',positions:[p]});
return c;
}
@@ -0,0 +1,91 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import crypto from 'node:crypto';
import {countryIsoCodes} from '../../resources/js/lib/country-iso-codes.ts';
import {internationalCase} from './helpers/irpc-international-case.mjs';
import {calculateAnnual,annualBook,recordAnnual,saveAnnualBook,annualCurrent,annualHtml,annualReportRows,prepareInternationalBook} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const value=v=>v?Number(v.numerator)/Number(v.denominator):null;
const now='2026-10-27T17:00:00+02:00';
const calc=({d,b})=>calculateAnnual(d,b);
const pending=c=>{const m=calc(c);assert.equal(m.status,'indeterminate',JSON.stringify(m.pending));assert.equal(m.arithmeticBalance,null);assert.equal(m.internationalCredit.deducted,null);assert.equal(m.pecDeduction.deducted,null);return m;};
const position=c=>c.b.internationalCredits.positions[0];
const proof=c=>position(c).payments[0];
test('country code projection exactly reuses the existing master and its source hash',()=>{
const raw=Buffer.from(fs.readFileSync(new URL('../../resources/data/iso3166.tab',import.meta.url),'utf8').replace(/\r\n/g,'\n'));
const codes=raw.toString('utf8').split(/\r?\n/).filter(l=>l&&!l.startsWith('#')).map(l=>l.split('\t')[0]);
assert.deepEqual(countryIsoCodes,codes);const source=fs.readFileSync(new URL('../../resources/js/lib/country-iso-codes.ts',import.meta.url),'utf8');assert.ok(source.includes(crypto.createHash('sha256').update(raw).digest('hex')));
});
test('10 paid capped at documented national8 precedes PEC200/PPC500:1408-8-200-500=700',()=>{
const m=calc(internationalCase());assert.deepEqual(m.pending,[]);const p=m.internationalCredit.positions[0];
assert.equal(value(p.paid),10);assert.equal(value(p.eligible),8);assert.equal(value(p.aboveLimit),2);assert.equal(value(m.internationalCredit.deducted),8);assert.equal(value(m.internationalCredit.afterCredit),1400);assert.equal(value(m.pecDeduction.deducted),200);assert.equal(value(m.payable),700);assert.equal(m.internationalCredit.carryCreated,false);assert.equal(m.officialBalancesConsumed,false);
});
test('documented treaty limit6 caps credit without country-based automatic entitlement',()=>{
const c=internationalCase();Object.assign(position(c),{treaty:'yes',treatyLimit:'6',treatyReference:'SYNTHETIC QA convention/article/exercise/limit, not real treaty data'});
const m=calc(c);assert.equal(value(m.internationalCredit.deducted),6);assert.equal(value(m.payable),702);assert.equal(value(m.internationalCredit.positions[0].aboveLimit),4);
position(c).treatyLimit='';pending(c);position(c).treatyLimit='6';position(c).treatyReference='';pending(c);
});
test('multiple actual receipts share one country limit;two countries keep separate limits',()=>{
const c=internationalCase();proof(c).amount='6';proof(c).proofAmount='6';position(c).payments.push({...proof(c),id:'foreign-second',document:'OTHER-RECEIPT',amount:'4',proofAmount:'4'});assert.equal(value(calc(c).internationalCredit.deducted),8);
const other=structuredClone(position(c));other.id='country-pt';other.country='PT';other.qualificationCountry='PT';other.nationalLimit='4';other.payments=[{...other.payments[0],id:'pt-tax',document:'PT-OTHER-RECEIPT',proofAmount:'5',amount:'5'}];c.b.internationalCredits.positions.push(other);assert.equal(value(calc(c).internationalCredit.deducted),12);assert.equal(value(calc(c).payable),696);
});
test('same country cannot multiply the national fraction across positions or other context maps',()=>{
const a=internationalCase(),copy=structuredClone(position(a));copy.id='duplicate-country';copy.payments[0].id='other-proof';copy.payments[0].document='OTHER';a.b.internationalCredits.positions.push(copy);assert.match(pending(a).pending.join(' '),/país\/exercício/);
const b=internationalCase(),other=structuredClone(b.b);other.internationalCredits.positions[0].payments[0].id='elsewhere';other.internationalCredits.positions[0].payments[0].document='ELSEWHERE-RECEIPT';const s=JSON.parse(b.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};b.d.modelo_a_snapshot=JSON.stringify(s);assert.match(pending(b).pending.join(' '),/noutro mapa/);
});
test('actual previous credit use reduces eligible credit,not gross tax or simulated history',()=>{
const c=internationalCase();position(c).previousUse.amount='3';let m=calc(c);assert.equal(value(m.internationalCredit.positions[0].eligible),8);assert.equal(value(m.internationalCredit.positions[0].available),5);assert.equal(value(m.payable),703);
position(c).previousUse.amount='9';assert.match(pending(c).pending.join(' '),/excede/);position(c).previousUse={state:'absent',amount:'',reference:'QA no actual previous use'};assert.equal(value(calc(c).payable),700);
});
test('lack of collecta is separated from tax above eligibility limits and never refunds credit or PEC',()=>{
const c=internationalCase('2000','2000','200','0'),m=calc(c);assert.equal(m.status,'balanced');assert.equal(value(m.internationalCredit.deducted),1408);assert.equal(value(m.internationalCredit.insufficientCollecta),592);assert.equal(value(m.internationalCredit.positions[0].aboveLimit),0);assert.equal(value(m.pecDeduction.deducted),0);assert.equal(value(m.pecDeduction.remaining),200);assert.equal(value(m.recoverable),0);assert.equal(m.internationalCredit.countryAllocationCreated,false);
c.b.ppc={...internationalCase().b.ppc};const recovered=calc(c);assert.equal(value(recovered.recoverable),500);assert.equal(value(recovered.internationalCredit.insufficientCollecta),592);assert.equal(recovered.internationalCredit.carryCreated,false);
});
test('known zero,declared absence and missing retain distinct inputs and outcomes',()=>{
const zero=internationalCase('0');assert.equal(value(calc(zero).internationalCredit.deducted),0);assert.equal(value(calc(zero).payable),708);
const a=internationalCase();a.b.internationalCredits.state='absent';a.b.internationalCredits.positions=[];assert.equal(value(calc(a).payable),708);
a.b.internationalCredits.state='';pending(a);a.b.internationalCredits.state='known';pending(a);
const b=internationalCase();b.b.internationalCredits.state='absent';pending(b);position(b).previousUse={state:'absent',amount:'0',reference:'QA contradiction'};pending(b);
});
test('country,entity,year,inclusion,current assessment,domestic fraction and convention require support',()=>{
for(const patch of [{country:'MZ'},{country:'ZZ'},{country:''},{country:'PT'},{qualificationCountry:''},{subjectNuit:'999999999'},{year:'2025'},{incomeIncluded:''},{incomeIncluded:'no'},{incomeAmount:''},{incomeAmount:'0'},{incomeReference:''},{nationalLimit:''},{nationalLimitReference:''},{assessmentKey:'stale'},{treaty:''},{treaty:'no',treatyLimit:'0'},{treatyReference:''}]){const c=internationalCase();Object.assign(position(c),patch);pending(c);}
for(const patch of [{state:''},{amount:''},{reference:''}]){const c=internationalCase();Object.assign(position(c).previousUse,patch);pending(c);}
});
test('actual foreign income tax proof is required,not projections,other taxes,wrong exercise or unsupported currency',()=>{
for(const patch of [{effective:''},{effective:'no'},{incomeTax:''},{incomeTax:'no'},{taxReference:''},{document:''},{issuer:''},{reference:''},{subjectNuit:'999999999'},{year:'2025'},{date:'2026-02-30'},{date:'2025-09-30'},{currency:'USD'},{amount:'10.01'},{amount:'0.001'}]){const c=internationalCase();Object.assign(proof(c),patch);pending(c);}
const c=internationalCase();position(c).payments=[];pending(c);
});
test('payment date differs from income exercise only with explicit correspondence,never an automatic carry',()=>{
const c=internationalCase();proof(c).date='2027-01-15';pending(c);proof(c).exerciseReference='SYNTHETIC QA own-exercise2026 assessment and payment relation';const m=calc(c);assert.equal(value(m.payable),700);assert.equal(m.internationalCredit.carryCreated,false);
});
test('same physical foreign receipt cannot be duplicated,re-dated or used as PPC/PEC/RF',()=>{
const a=internationalCase();position(a).payments.push({...proof(a),id:'other-date',date:'2026-10-01'});pending(a);
for(const kind of ['ppc','pec','rf']){const c=internationalCase('10','8','200','500',kind==='rf'?'300':'0');const physical=kind==='ppc'?c.b.ppc.rows[0]:kind==='pec'?c.b.pecPayments.rows[0]:{issuer:'QA AT',document:'QA-RF-PEC-2026',proofAmount:'300'};Object.assign(proof(c),{issuer:physical.issuer,document:physical.document,proofAmount:physical.proofAmount});assert.match(pending(c).pending.join(' '),/naturez|comprovativo/i);}
});
test('a foreign receipt cannot move country/entity across context maps;external changes invalidate current result',()=>{
const c=internationalCase(),other=structuredClone(c.b);other.ppc.rows=[];other.ppc.state='absent';other.pecPayments.rows=[];other.pecPayments.state='absent';other.internationalCredits.positions[0].country='PT';const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);pending(c);
other.internationalCredits.positions[0].payments[0].document='OTHER-PT';c.d.modelo_a_snapshot=JSON.stringify(s);assert.equal(value(calc(c).payable),700);c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const saved=annualBook(c.d);assert.equal(annualCurrent(c.d,saved),true);
const edit=JSON.parse(c.d.modelo_a_snapshot);edit.irpcAnnual['opportunity:other'].internationalCredits.positions[0].country='ZA';c.d.modelo_a_snapshot=JSON.stringify(edit);assert.equal(annualCurrent(c.d,saved),false);pending({...c,b:saved});
});
test('economic/benefit positive or missing states block only the dependent annual balance,not known eligibility',()=>{
for(const kind of ['economic','benefits'])for(const patch of [{state:''},{state:'known',amount:'1'},{reference:''}]){const c=internationalCase();Object.assign(c.b.internationalCredits[kind],patch);const m=pending(c);assert.equal(value(m.internationalCredit.positions[0].eligible),8);assert.equal(value(m.assessment.colecta),1408);}
});
test('exact decimals and withholding are included once after credit and PEC',()=>{
const c=internationalCase('10.25','8.15','200','500','300');position(c).previousUse.amount='3.01';const m=calc(c);assert.deepEqual(m.internationalCredit.deducted,{numerator:'257',denominator:'50'});assert.equal(value(m.payable),402.86);assert.equal(value(m.knownWithholdings),300);
});
test('documented fractional ceilings retain exact arithmetic without assumed tax rounding',()=>{
const c=internationalCase('10','1/3');const m=calc(c);assert.deepEqual(m.internationalCredit.deducted,{numerator:'1',denominator:'3'});assert.deepEqual(m.payable,{numerator:'2123',denominator:'3'});
position(c).nationalLimit='1/0';pending(c);
});
test('prepare preserves deliberate prior summary and history;save/recover/report appends frozen records',()=>{
const c=internationalCase(),detail=structuredClone(c.b.internationalCredits);delete c.b.internationalCredits;const original=JSON.parse(c.d.modelo_a_snapshot),plan=c.d.business_plan_snapshot;c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const prior=structuredClone(annualBook(c.d).result);assert.equal(value(prior.model.payable),708);
let next=prepareInternationalBook(annualBook(c.d));next.internationalCredits=detail;c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,next,now));const back=annualBook(JSON.parse(JSON.stringify(c.d)));assert.deepEqual(back.history,[prior]);assert.equal(annualCurrent(c.d,back),true);assert.equal(value(back.result.model.payable),700);assert.deepEqual(back.credits,{state:'known',amount:'0',reference:'QA créditos precedentes conhecidos zero'});
const after=JSON.parse(c.d.modelo_a_snapshot);delete after.irpcAnnual;assert.deepEqual(after,original);assert.equal(c.d.business_plan_snapshot,plan);
const html=annualHtml(c.d);assert.match(html,/Crédito internacional — antes do PEC/);assert.match(html,/700.00/);assert.match(html,/1400.00/);assert.match(JSON.stringify(createProjectDossierReport(c.d,{selectedSections:new Set(['finance'])})),/Crédito internacional — ZA/);
const frozen=structuredClone(back.result);back.internationalCredits.positions[0].nationalLimit='7';c.d.modelo_a_snapshot=saveAnnualBook(c.d,back);assert.equal(annualCurrent(c.d,back),false);assert.deepEqual(back.result,frozen);assert.match(annualReportRows(c.d).flat().join(' '),/Histórico/);assert.throws(()=>annualHtml(c.d),/fontes actuais/);
});
test('malformed optional international detail never resets existing snapshots',()=>{
const c=internationalCase(),snapshot=c.d.modelo_a_snapshot;proof(c).incomeTax=null;assert.throws(()=>saveAnnualBook(c.d,c.b),/não recuperável/);assert.equal(c.d.modelo_a_snapshot,snapshot);
});
+11
View File
@@ -0,0 +1,11 @@
import fs from 'node:fs';
import crypto from 'node:crypto';
const source = new URL('../resources/data/iso3166.tab', import.meta.url);
const text = fs.readFileSync(source, 'utf8').replace(/\r\n/g, '\n');
const codes = text.split('\n').filter(line => line && !line.startsWith('#')).map(line => line.split('\t')[0]);
if (new Set(codes).size !== codes.length || codes.some(code => !/^[A-Z]{2}$/.test(code))) throw Error('Invalid existing ISO catalogue');
const hash = crypto.createHash('sha256').update(text).digest('hex');
fs.writeFileSync(new URL('../resources/js/lib/country-iso-codes.ts', import.meta.url),
'// Derived code projection of resources/data/iso3166.tab; keep the existing PHP catalogue authoritative.\n' +
'// Source SHA256 (LF): ' + hash + '; regenerate with node tools/country-iso-projection.mjs\n' +
"export const countryIsoCodes = '" + codes.join(' ') + "'.split(' ');\n");
+16
View File
@@ -0,0 +1,16 @@
import fs from 'node:fs';
import {internationalCase} from '../tests/frontend/helpers/irpc-international-case.mjs';
import {recordAnnual} from '../resources/js/lib/irpc-annual-settlement.ts';
const cases = {};
for (const name of ['payable','treaty','missing','limited-collecta','duplicate','absent','prior-use']) {
const c = name === 'limited-collecta' ? internationalCase('2000','2000') : internationalCase();
const p = c.b.internationalCredits.positions[0];
if (name === 'treaty') Object.assign(p, {treaty: 'yes', treatyLimit: '6', treatyReference: 'SYNTHETIC QA convention ceiling only, no actual treaty assertion'});
if (name === 'missing') p.treaty = '';
if (name === 'limited-collecta') p.incomeAmount = '6250';
if (name === 'duplicate') Object.assign(p.payments[0], {issuer: c.b.ppc.rows[0].issuer, document: c.b.ppc.rows[0].document, proofAmount: '500'});
if (name === 'absent') {c.b.internationalCredits.state = 'absent';c.b.internationalCredits.positions = [];}
if (name === 'prior-use') Object.assign(p.previousUse, {amount: '3', reference: 'SYNTHETIC QA: documented actual previous credit use of 3 MZN'});
cases[name] = recordAnnual(c.d, c.b, '2026-10-27T17:00:00+02:00');
}
fs.writeFileSync(new URL('../tests/frontend/fixtures/irpc-international-preserved.json', import.meta.url), JSON.stringify({synthetic: true, actualConventionData: false, base: 'irpc-assessment-preserved.json', cases}));
+26
View File
@@ -0,0 +1,26 @@
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import {fileURLToPath} from 'node:url';
import {annualBook,annualCurrent,calculateAnnual,annualHtml,annualReportRows} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const root=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
assert.equal(path.basename(root),'portal-investidor-irpc-international-qa-20261027');
const output=path.join(root,'outputs/irpc-l8-international-20261027');
const results=[];
for(const name of ['payable','treaty','missing','limited-collecta','duplicate','absent','prior-use']){
const d=JSON.parse(fs.readFileSync(path.join(output,'recovered-'+name+'.json'),'utf8'));
const book=annualBook(d),m=book.result.model;
assert.equal(annualCurrent(d,book),true);
const fresh=calculateAnnual(d,book);assert.equal(fresh.status,m.status);assert.deepEqual(fresh.arithmeticBalance,m.arithmeticBalance);assert.deepEqual(fresh.internationalCredit,m.internationalCredit);
const html=annualHtml(d),rows=annualReportRows(d);
const dossier=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-10-27T17:00:00+02:00'});
assert.match(html,/Crédito internacional — antes do PEC/);assert.match(JSON.stringify(dossier),/Crédito internacional — antes do PEC/);
if(['missing','duplicate'].includes(name)){assert.match(html,/Indeterminado/);assert.equal(m.arithmeticBalance,null);}
if(name==='payable'){assert.match(html,/700.00/);assert.match(html,/1400.00/);assert.match(html,/1200.00/);}
fs.writeFileSync(path.join(output,'report-'+name+'.html'),html);
fs.writeFileSync(path.join(output,'dossier-'+name+'.json'),JSON.stringify(dossier));
results.push({case:name,status:m.status,balance:m.arithmeticBalance,international:m.internationalCredit,pec:m.pecDeduction,reportRows:rows.length,history:book.history.length,apiRoundTrip:true,current:true,dossier:true});
}
fs.writeFileSync(path.join(output,'recovered-report-receipt.json'),JSON.stringify({synthetic:true,activeChanged:false,results},null,2));
console.log(JSON.stringify(results.map(({case:name,status,balance,history})=>({case:name,status,balance,history})),null,2));