feat(irpc): demonstrate ordinary economic credit and compatible carry in QA

This commit is contained in:
2026-10-28 14:29:55 +02:00
parent 6ededb597a
commit aae67c169d
21 changed files with 882 additions and 37 deletions
@@ -1591,3 +1591,20 @@ Escolha expressa das parcelas correntes e anteriores, vinculada à colecta selec
**Provas:**16novos+49afectados=65testes aprovados; API1teste/119asserções,6casos POST/PUT/GET na base guardada `proactive_investment_20261027_test`,loopback55441,com rollback. Seis recuperações recalculadas integralmente,HTML/dossier concordantes e resultados anteriores congelados.55oportunidades,64documentos,1vínculo,19rascunhos,40utilizadores,3organizações e409eventos anteriores preservados.31ficheiros do acervo byte a byte iguais,sem nova importação/publicação. TypeScript,build QA com exit0 ePint aprovados. Não foi feita nova prova manual de interface ou avaliação pela equipa. Sem migrations,grants,contas,alteração activa ou operações de servidor.
**Saldo concreto:**L8 continua parcial. Próximo incremento independente:crédito económico64,com gross-up53(1)a/exclusão40,qualificação dos lucros/pagador/beneficiário/imposto e limite proporcional correspondidos; fonte aceite já disponível. Permanecem fracção nacional/CDT automática,créditos/benefícios próprios,obrigação/calendárioPPC/PEC,autónomos,períodos especiais,outras moedas eM20/M22. Não declarar IRPC concluído nem repetir a validação das entregas independentes aceites.
## 28/10/2026 — crédito económico ordinário demonstrado em QA; agrupamento compatível
**Implementado e demonstrado em QA isolada, sem instalação.** Ramo `codex/irpc-l8-economic-qa-20261028`, base `6ededb59`, incluindo os saldos anteriores `d134a92d`. A última referência instalada documentada continua `838ad941`; não foi feita nova leitura do servidor. Não se reabre a aceitação dos históricos ou das provas anteriores.
**Cobertura:**CIRPC64(1–2),53(1)a,40(1)na redacção20/2009 eRIRPC21(2/5). Lucros ordinários distribuídos e IRPC correspondente documentados,beneficiário/distribuidor com sede/direcção efectiva emMoçambique,distribuidor sujeito/não isento,exclusão40(1)não aplicável com suporte. A selecção explícita da colecta corrente reutiliza NUIT/exercício/residência válidos e conserva edições divergentes. Novo detalhe opcional; resumo económico deliberado anterior e resultados congelados conservados.
**Cálculo:**60%doIRPCdocumentadamente correspondente aos lucros;confere o acréscimo53 já incluído emL6 e limita pela quota proporcional documentada,menos utilização efectiva anterior,até à colecta. O portal não deriva IRPC do dividendo líquido nem altera a base/colecta silenciosamente;inclusão/acréscimo e quota exigem correspondência do caso. A existência de perdas que reduzam a base abaixo dos lucros não é tratada como impedimento genérico. Crédito económico precede o internacional ePEC,sem reembolso ou transporte económico presumido. Exemplo sintético condicionado aos suportes:IRPC20→nominal12;1408−12−8internacional−200PEC−500PPC=**688MZN a pagar,simulado**.
**Mesma parcela:**identidade por emitente/documento/parcela com suporte;duplicação dos lucros também conferida independentemente de trocar o comprovativo de imposto,a data ou oID. O mesmo documento pode suportar parcelas de imposto distintas quando a separação estiver documentada em ambos os registos. Um comprovativo anterior sem parcela continua a abranger o documento inteiro,não se torna livre por receber novo nome noutro mapa. Atribuições anteriores e capacidade do comprovativo conferidas neste e noutros contextos do snapshot;divergências não são apagadas. ProtecçõesL5 conservadas. Não é reconciliação global de conta fiscal entre dossiers independentes.
**Agrupamento:**composição demonstrada com os saldos anteriores:1408−12económico−8internacionalcorrente−50internacionalanterior−200PECcorrente−100PECanterior−500PPC=**538MZN a pagar,simulado**. A prioridade económica não reduz automaticamente a afectação entre exercícios já escolhida;se exceder a colecta remanescente,o caso pede reconciliação conservando as escolhas. Os dois incrementos podem integrar uma entrega Git comum;nenhum foi instalado nesta passagem.
**Provas do delta:**20testes económicos novos+65afectados aceites=85aprovados;API1teste/210asserções,11casos POST/PUT/GET na base guardada `proactive_investment_20261027_test`,loopback55441,transacção com rollback. Onze recuperações recalculadas integralmente,HTML/dossier concordantes e históricos anteriores conservados.55oportunidades,64documentos,1vínculo,19rascunhos,40utilizadores,3organizações e409eventos anteriores intactos. TypeScript,build QA ePint aprovados.31ficheiros do acervo preservados por bytes,sem novas importações,publicações,scan declarado,migrations,contas ougrants. Não foi feita nova prova manual da interface nem avaliação pela equipa.
**Pendências concretas:**inclusão/correcção automática do dividendo/acréscimo emL6 e derivação da quota proporcional;exclusão40(1)na base com todas as suas condições;liquidação64(3)/associações64(4);créditos/benefícios específicos;fracção internacional/CDT automática;obrigação/calendárioPPC/PEC;autónomos,períodos especiais,outras moedas eM20/M22. Correspondências e parâmetros em falta afectam o caso dependente,sem revalidação genérica de fontes oficiais. Saldos anteriores mantêm o recorte ordinário já demonstrado. **L8 permanece parcial.** Recibo:`docs/investmoz/simulations/irpc-l8-economic-qa-20261028.json`.
@@ -0,0 +1,315 @@
{
"date": "2026-10-28",
"status": "ordinary economic credit implemented and demonstrated in isolated QA; not installed",
"branch": "codex/irpc-l8-economic-qa-20261028",
"base": "6ededb59",
"last_installed_receipt_reference": "838ad941a24ae76ec4bf91ccc3f92012668fe8cf",
"remote_status_read_this_turn": false,
"scope": {
"current_year": "2026",
"current_period": "calendar year, resident general",
"currency": "documented MZN; exact fractions without fiscal rounding",
"profit_kind": "ordinary distributions art64(1–2)",
"eligibility": "same-country seat/effective management; payer subject to IRPC and nonexempt; nonapplication of40(1) documented, including2009 version",
"nominal_credit": "60 percent of documented IRPC corresponding to included distributed profits; no assumed rate on net dividend",
"gross_up": "expected nominal60 percent reconciled with53(1)a amount already included in current L6; does not overwrite or recalculate source base",
"proportional_ceiling": "case-specific documented input tied to current assessment; not automatically derived",
"prior_use": "actual documented economic credit use, distinct from simulation history and prior source-tax attributions",
"order": [
"economic credit up to collecta",
"international credit current/prior according to existing explicit selection",
"PEC current/prior according to existing explicit selection",
"own withholdings and PPC"
],
"refund": "no economic-credit refund or carry created; PPC can create simulated recoverable balance"
},
"proof_parcels": {
"stable_identity": "issuer/document/parcel; distribution identity also independent of source-tax document,date and row id",
"economic_vs_international": "same physical tax parcel cannot fund both credit natures",
"same_paper_different_tax": "supported distinct parcels can coexist; old proof without a parcel overlaps the whole paper until reconciled",
"capacity": "same proof parcel amounts and prior attributions must agree and total allocation remains within available amount across snapshot contexts",
"L5": "existing withholding source/allocation checks preserved; parcel relabelling cannot turn repeated L5 use into new tax",
"partial_information": "saved and recovered as indeterminate, never reset or silently assumed absent",
"global_official_account_reconciliation": false
},
"compatibility_group": {
"included_prior_QA": "d134a92d169f9159a692ed158e3a051889f7ab3a; receipt6ededb59",
"candidate_contains_carry_increment": true,
"combined_API_case": "1408-12economic-8currentInternational-50priorInternational-200currentPEC-100priorPEC-500PPC=538MZN payable simulated",
"previous_carry_history_preserved": "550 MZN prior simulated result retained unchanged",
"changed_allocation": "economic priority checks remaining collecta but keeps user prior/current allocations; over-allocation becomes pending rather than reduced automatically",
"installation": false
},
"restriction_origins": {
"Tempo": "next economic increment, distinction from international, no duplicate same parcel, preserve approved evidence/history; compatible grouping allowed",
"accepted_requirement": "CIRPC64(1–2),53(1)a,40(1) amended by20/2009; RIRPC21(2/5)",
"implementation_choices": "optional versioned detail; normal resident general2026/MZN scope; documentary inclusion/gross-up/proportional quota; optional parcel annotations on existing proof types; no additional organizational authority"
},
"verification": {
"economic_new_frontend_tests": 20,
"previous_affected_tests": 65,
"frontend_pass": 85,
"frontend_fail": 0,
"API_tests": 1,
"API_assertions": 210,
"API_cases": 11,
"database": "proactive_investment_20261027_test",
"loopback_port": 55441,
"API_transactions_rolled_back": true,
"recovered_cases_recalculated_with_HTML_and_dossier": 11,
"frozen_histories_preserved": true,
"typescript": "passed",
"build": "isolated QA, subprocess exit0, final component hashes recorded",
"PHP_formatting": "pint passed",
"new_browser_manual_proof": false,
"team_usability_evaluation": "unchanged; awaiting team feedback"
},
"preservation": {
"protected_files_same_git_content": 558,
"raw_checkout_difference": {
"kind": "LF/CRLF environment difference only, no semantic loss",
"files": [
"docs/investmoz/simulations/irpc-l8-carry-qa-20261028.json"
]
},
"source_originals_same_bytes": 31,
"existing_QA_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
},
"existing_matrix": "only AUD-SIM-INT02 evidence updated; other records unchanged semantically",
"old_carry_receipt_and_fixture": "unchanged",
"schema_permissions_configuration": "no schema/configuration/migration/grant changes; no new live comparison claimed",
"active_data_writes": false,
"new_accounts": false,
"new_imports_or_publication": false,
"remote_operations": false,
"Songo_Gurue_SGF_TEMPO": "untouched"
},
"cases": [
{
"name": "payable",
"status": "payable",
"balance": {
"numerator": "688",
"denominator": "1"
},
"economicDeduction": {
"numerator": "12",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "combined",
"status": "payable",
"balance": {
"numerator": "538",
"denominator": "1"
},
"economicDeduction": {
"numerator": "12",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "quota",
"status": "payable",
"balance": {
"numerator": "695",
"denominator": "1"
},
"economicDeduction": {
"numerator": "5",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "prior-use",
"status": "payable",
"balance": {
"numerator": "691",
"denominator": "1"
},
"economicDeduction": {
"numerator": "9",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "duplicate",
"status": "indeterminate",
"balance": null,
"economicDeduction": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "missing-gross-up",
"status": "indeterminate",
"balance": null,
"economicDeduction": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "article40",
"status": "indeterminate",
"balance": null,
"economicDeduction": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "shared-paper",
"status": "payable",
"balance": {
"numerator": "688",
"denominator": "1"
},
"economicDeduction": {
"numerator": "12",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "partial-parcel",
"status": "indeterminate",
"balance": null,
"economicDeduction": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "fractional",
"status": "payable",
"balance": {
"numerator": "349997",
"denominator": "500"
},
"economicDeduction": {
"numerator": "3",
"denominator": "500"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "recoverable",
"status": "recoverable",
"balance": {
"numerator": "-500",
"denominator": "1"
},
"economicDeduction": {
"numerator": "12",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
}
],
"remaining": {
"economic": [
"automatic inclusion/correction of distributed profits and53 addition in L6",
"automatic proportional ceiling derivation and complete40(1) exclusion treatment at base stage",
"liquidation64(3),associations64(4),special-period and other-currency cases"
],
"other_L8": [
"specific benefit credits and their qualification,cumulation and limits",
"automatic international national-fraction and case-specific treaty matching",
"PPC/PEC obligation/calendar,interest/penalties and payment guides with applicable edition",
"autonomous taxation,special periods,other currencies and M20/M22 transport/submission",
"exceptional/transitional interyear cases beyond accepted ordinary carry scope"
]
},
"L8_complete": false,
"evidence": {
"api-result.json": "124fd50cb6d223cdd18fedc166d00af7254199b6e6dedf9be82f71db87af6d01",
"api.log": "e5cb58ba25c82feb1a14a8259cd112a58455b8cc1c17d8540b08ec30b87583c7",
"build-result.json": "256bb54e1d47ec73de37810b9673f10464c65fb970ea3b7ae28bbea36ba222e3",
"build.log": "69f17893b47b8d748651933f6a88a5f4206d58173397c1695d58472db9a26cf6",
"existing-records-preservation.json": "b45b3306d3c9dee63fbbaf9a04bba31008019e65892bd16322460b5ef53eda7d",
"frontend-result.json": "f73293b96cda7c81c86a6716a04a18d632e10361bf7d3f616695dbdc7695aeb0",
"frontend.log": "ff784389cda3f2c232f4760f74c053bc16b6d134ef975d0aad13132373e5e275",
"pint-result.json": "124fd50cb6d223cdd18fedc166d00af7254199b6e6dedf9be82f71db87af6d01",
"pint.log": "cd1a94fc2cf6a965b86e1a4809d6c7fb9148b1ee374e1010ed2ac96ff4876ec2",
"protected-consumers.json": "172542788e44fe604ca90d95a71d87f47a3baeae0c61950bc50d342658aa82ad",
"recovered-report-receipt.json": "ecce91a9c00c3a88d234486c020b5aa1681928eb1b1aeaeef0e7a569a2e17654",
"source-preservation.json": "6aceebe1cf883ca709ccbbbef7f2a756ee5dc51c12b3281c3659e83abfd90b70",
"types-result.json": "256bb54e1d47ec73de37810b9673f10464c65fb970ea3b7ae28bbea36ba222e3",
"types.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
}
@@ -214,3 +214,8 @@ A implementaçãoQA documentada acima foi instalada em`838ad941`,sem alterar fon
## 28/10/2026 — aplicação de65(3)/66 aos saldos anteriores
Reutilizados os31ficheiros do acervo, todos iguais por bytes a `cf4e0638`. Lei34/2007,BR52,PDF18/p768-(72),SHA256`cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e`: página conferida visualmente;65(3)cinco exercícios seguintes,66(1–2)três paraPEC,sem reembolso. Referência à regra/apuramento aplicáveis na origem é correspondência de período, não nova validação de autenticidade da fonte. Leitura remota do URL arquivado falhou por timeout; usado o exemplar oficial exacto já aceite. Nenhum original alterado,scan declarado ou novo cálculo activado por publicação. Recorte:origens anuais ordinárias documentadas,mesmo sujeito,fecho2026/MZN;sem situações transitórias/especiais presumidas. Recibo `irpc-l8-carry-qa-20261028.json`.
## 28/10/2026 — crédito económico64 e distinção de40/65
Inventário anterior reutilizado:31ficheiros do acervo byte a byte iguais a6ededb59. Sem novo original. Lei34/2007BR52,SHA256`cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e`,PDF18/p768-(72),64(1–2),ePDF15/p768-(69),53(1)a,conferidos visualmente.40(1):Lei20/2009,exemplarAT19/2009,PDF4/p272-(16),redacção20%/dois anos e condições próprias,substitui a redacção original25%. Listas dasLeis4/2012 e19/2013 conferidas;cadeias20/2022 e12/2025 já aceites reutilizadas nos seus âmbitos. Decreto9/2008,RIRPC21(2/5),PDF6/p121:económico,internacional,benefícios,PEC,até à colecta,sem reembolso. Qualificação do caso,inclusão/acréscimo e quota proporcional documentados;sem automatizar a exclusão40 ou alterarL6por presunção. Sem nova exigência de autenticidade de fonte,scan ou publicação/importação. Pesquisa dirigida não substituiu os exemplares oficiais arquivados. Recibo:`irpc-l8-economic-qa-20261028.json`.
@@ -7140,3 +7140,29 @@ Escolha expressa das parcelas correntes e anteriores, vinculada à colecta selec
**Provas:**16novos+49afectados=65testes aprovados; API1teste/119asserções,6casos POST/PUT/GET na base guardada `proactive_investment_20261027_test`,loopback55441,com rollback. Seis recuperações recalculadas integralmente,HTML/dossier concordantes e resultados anteriores congelados.55oportunidades,64documentos,1vínculo,19rascunhos,40utilizadores,3organizações e409eventos anteriores preservados.31ficheiros do acervo byte a byte iguais,sem nova importação/publicação. TypeScript,build QA com exit0 ePint aprovados. Não foi feita nova prova manual de interface ou avaliação pela equipa. Sem migrations,grants,contas,alteração activa ou operações de servidor.
**Saldo concreto:**L8 continua parcial. Próximo incremento independente:crédito económico64,com gross-up53(1)a/exclusão40,qualificação dos lucros/pagador/beneficiário/imposto e limite proporcional correspondidos; fonte aceite já disponível. Permanecem fracção nacional/CDT automática,créditos/benefícios próprios,obrigação/calendárioPPC/PEC,autónomos,períodos especiais,outras moedas eM20/M22. Não declarar IRPC concluído nem repetir a validação das entregas independentes aceites.
## 28/10/2026 — crédito económico: preservação e limites antes do novo delta
28/10/2026, antes de novos ensaios: inventário preservado do internacional838ad941/PECe182d1d7/sectores669cb897 instalados e saldos anterioresd134a92d/6ededb59 emQA sem instalação. Novo ramo económico parte de6ededb59, agregável com o delta compatível, sem reapresentar a aprovação dos anteriores.31ficheiros do acervo iguais por bytes;nenhuma perda de fontes confirmada. Sem acesso ao activo nesta preparação;comparação de dados/schema/permissões do destino só na entrega autorizada.
CIRPC64(1–2)/53(1)a e40(1) na redacçãoLei20/2009 são correspondidos no próprio acervo aceite. Regra económica distinta da internacional65;na liquidação precede65/PEC (RIRPC21). Novo detalhe opcional,sem alterar resultados congelados ou sobrescrever deliberadamente o resumo económico anterior. Sem instalação/publicação.
## 28/10/2026 — crédito económico ordinário demonstrado em QA; agrupamento compatível
**Implementado e demonstrado em QA isolada, sem instalação.** Ramo `codex/irpc-l8-economic-qa-20261028`, base `6ededb59`, incluindo os saldos anteriores `d134a92d`. A última referência instalada documentada continua `838ad941`; não foi feita nova leitura do servidor. Não se reabre a aceitação dos históricos ou das provas anteriores.
**Cobertura:**CIRPC64(1–2),53(1)a,40(1)na redacção20/2009 eRIRPC21(2/5). Lucros ordinários distribuídos e IRPC correspondente documentados,beneficiário/distribuidor com sede/direcção efectiva emMoçambique,distribuidor sujeito/não isento,exclusão40(1)não aplicável com suporte. A selecção explícita da colecta corrente reutiliza NUIT/exercício/residência válidos e conserva edições divergentes. Novo detalhe opcional; resumo económico deliberado anterior e resultados congelados conservados.
**Cálculo:**60%doIRPCdocumentadamente correspondente aos lucros;confere o acréscimo53 já incluído emL6 e limita pela quota proporcional documentada,menos utilização efectiva anterior,até à colecta. O portal não deriva IRPC do dividendo líquido nem altera a base/colecta silenciosamente;inclusão/acréscimo e quota exigem correspondência do caso. A existência de perdas que reduzam a base abaixo dos lucros não é tratada como impedimento genérico. Crédito económico precede o internacional ePEC,sem reembolso ou transporte económico presumido. Exemplo sintético condicionado aos suportes:IRPC20→nominal12;1408−12−8internacional−200PEC−500PPC=**688MZN a pagar,simulado**.
**Mesma parcela:**identidade por emitente/documento/parcela com suporte;duplicação dos lucros também conferida independentemente de trocar o comprovativo de imposto,a data ou oID. O mesmo documento pode suportar parcelas de imposto distintas quando a separação estiver documentada em ambos os registos. Um comprovativo anterior sem parcela continua a abranger o documento inteiro,não se torna livre por receber novo nome noutro mapa. Atribuições anteriores e capacidade do comprovativo conferidas neste e noutros contextos do snapshot;divergências não são apagadas. ProtecçõesL5 conservadas. Não é reconciliação global de conta fiscal entre dossiers independentes.
**Agrupamento:**composição demonstrada com os saldos anteriores:1408−12económico−8internacionalcorrente−50internacionalanterior−200PECcorrente−100PECanterior−500PPC=**538MZN a pagar,simulado**. A prioridade económica não reduz automaticamente a afectação entre exercícios já escolhida;se exceder a colecta remanescente,o caso pede reconciliação conservando as escolhas. Os dois incrementos podem integrar uma entrega Git comum;nenhum foi instalado nesta passagem.
**Provas do delta:**20testes económicos novos+65afectados aceites=85aprovados;API1teste/210asserções,11casos POST/PUT/GET na base guardada `proactive_investment_20261027_test`,loopback55441,transacção com rollback. Onze recuperações recalculadas integralmente,HTML/dossier concordantes e históricos anteriores conservados.55oportunidades,64documentos,1vínculo,19rascunhos,40utilizadores,3organizações e409eventos anteriores intactos. TypeScript,build QA ePint aprovados.31ficheiros do acervo preservados por bytes,sem novas importações,publicações,scan declarado,migrations,contas ougrants. Não foi feita nova prova manual da interface nem avaliação pela equipa.
**Pendências concretas:**inclusão/correcção automática do dividendo/acréscimo emL6 e derivação da quota proporcional;exclusão40(1)na base com todas as suas condições;liquidação64(3)/associações64(4);créditos/benefícios específicos;fracção internacional/CDT automática;obrigação/calendárioPPC/PEC;autónomos,períodos especiais,outras moedas eM20/M22. Correspondências e parâmetros em falta afectam o caso dependente,sem revalidação genérica de fontes oficiais. Saldos anteriores mantêm o recorte ordinário já demonstrado. **L8 permanece parcial.** Recibo:`docs/investmoz/simulations/irpc-l8-economic-qa-20261028.json`.
Preservação delimitada:558ficheiros protegidos com conteúdo Git idêntico à baseQA;uma diferençaLF/CRLF em `docs/investmoz/simulations/irpc-l8-carry-qa-20261028.json` entre checkouts,conferida como normalização de ambiente,sem perda de conteúdo. Os31originais do acervo são iguais por bytes.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
import type {FiscalProofParcel} from '@/lib/fiscal-proof-parcel';
export function FiscalProofParcelFields({value, change, label}: {value: FiscalProofParcel; change: (value: FiscalProofParcel) => void; label: string}) {
return <details className="rounded border p-2" open={!!value.parcel || !!value.parcelReference}>
<summary>Identificar parcela quando o documento suporta várias parcelas — {label}</summary>
<p>Sem identificação específica, o comprovativo existente abrange o documento inteiro. Para distinguir impostos ou parcelas, indique a linha/identificador exacto e o suporte dessa separação. Os montantes comprovado e seleccionado devem corresponder à parcela identificada. A mudança do identificador não comprova uma parcela diferente.</p>
<label className="block">Identificador da parcela<input aria-label={label + ' — Parcela documental'} value={value.parcel ?? ''} onChange={e => change({parcel: e.target.value})}/></label>
<label className="block">Suporte da separação das parcelas<input aria-label={label + ' — Suporte da parcela'} value={value.parcelReference ?? ''} onChange={e => change({parcelReference: e.target.value})}/></label>
</details>;
}
@@ -1,3 +1,5 @@
import {SimulationIrpcEconomic} from './simulation-irpc-economic';
import {FiscalProofParcelFields} from './fiscal-proof-parcel';
import {SimulationIrpcCarry} from './simulation-irpc-carry';
import {SimulationIrpcInternational} from './simulation-irpc-international';
import {SimulationIrpcPec} from './simulation-irpc-pec';
@@ -18,10 +20,10 @@ export function SimulationIrpcAnnual({draft,onChange}:Props){
<p>Colecta disponível:{a.colecta?a.colecta.numerator+'/'+a.colecta.denominator+' MZN':'Indeterminada'};origem:{a.kind==='special'?'L7 qualificado':'L6 normal'};NUIT:{a.subjectNuit||'Por identificar'};exercício:{a.year||'Por identificar'}.</p><button type="button" onClick={()=>{try{save(reuseAnnualAssessment(draft,b));}catch(e){setError(String(e));}}}>Reutilizar colecta corrente L6/L7</button>{field('assessmentReference','Suporte da colecta reutilizada L8')}
<fieldset className="space-y-2 rounded border p-3"><legend>Retenções efectivamente sofridas por conta</legend>{group('withholdings','Retenções sofridas')}
{options.length===0?<p>Não há afectações de comprovativosL5 neste contexto. Reutilize e reconcilie a retenção recebida emL5 ou declare ausência com suporte.</p>:options.map(o=><div key={o.rowId+'|'+o.paymentId} className="rounded border p-2"><p>{o.document||'Prova por identificar'};emitente:{o.issuer};data:{o.date};parcelaL5:{o.amount}MZN;{o.reason||'Retenção recebida/efectiva por conta reconciliada emL5.'}</p><button type="button" disabled={!o.ready||b.withholdings.rows.some(r=>r.rowId===o.rowId&&r.paymentId===o.paymentId)} onClick={()=>{try{save(reuseAnnualWithholding(draft,b,o.rowId,o.paymentId,crypto.randomUUID()));}catch(e){setError(String(e));}}}>Seleccionar prova L5 {o.document||o.paymentId}</button></div>)}
{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>
{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><FiscalProofParcelFields value={r} change={change} label={"Retenção L8 "+(index+1)}/><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>
<SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/><SimulationIrpcCarry 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>)}
{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><FiscalProofParcelFields value={r} change={change} label={"PPC L8 "+(index+1)}/><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>
<SimulationIrpcEconomic book={b} save={save} residentConfirmed={a.current&&a.book.scope==='resident_general'&&b.subjectNuit===a.subjectNuit&&b.year===a.year&&b.assessmentKey===a.key}/><SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/><SimulationIrpcCarry 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,41 @@
import {prepareEconomicBook, type AnnualBook} from '@/lib/irpc-annual-settlement';
import {economicSource, newEconomicPosition, type EconomicPosition} from '@/lib/irpc-economic-credit';
export function SimulationIrpcEconomic({book, save, residentConfirmed}: {book: AnnualBook; save: (book: AnnualBook) => void; residentConfirmed: boolean}) {
const c = book.economicCredits;
if (!c) return <fieldset className="rounded border p-3"><legend>Crédito por dupla tributação económica</legend><p>Para lucros distribuídos por entidade moçambicana sujeita e não isenta de IRPC, distinga o imposto correspondente aos lucros da retenção sofrida e do imposto estrangeiro. Reutiliza a colecta e conserva o resumo de créditos anterior.</p><button type="button" onClick={() => save(prepareEconomicBook(book))}>Preparar crédito económico</button></fieldset>;
const update = (next: typeof c) => save({...book, economicCredits: next});
const rowChange = (r: EconomicPosition) => update({...c, rows: c.rows.map(x => x.id === r.id ? r : x)});
return <fieldset className="space-y-3 rounded border p-3"><legend>Crédito económico — lucros distribuídos</legend>
<p>{economicSource}</p>
<p>O detalhe substitui nesta execução o resumo económico anterior, que permanece conservado: {book.internationalCredits?.economic.state || 'informação em falta'}; montante {book.internationalCredits?.economic.amount || 'não preenchido'}. Os benefícios e o crédito internacional mantêm os seus estados próprios.</p>
<label className="block">Estado do crédito económico<select aria-label="Estado do crédito económico L8" value={c.state} onChange={e => update({...c, state: e.target.value as typeof c.state})}><option value="">Informação em falta</option><option value="known">Lucros e IRPC conhecidos</option><option value="absent">Ausência documentada</option></select></label>
<label className="block">Suporte do inventário económico<input aria-label="Suporte económico L8" value={c.reference} onChange={e => update({...c, reference: e.target.value})}/></label>
{(c.state === 'known' || c.rows.length > 0) && <button type="button" onClick={() => update({...c, state: 'known', rows: [...c.rows, {...newEconomicPosition(crypto.randomUUID()), subjectNuit: book.subjectNuit, year: book.year, recipientResident: residentConfirmed ? 'yes' : ''}]})}>Registar lucros distribuídos</button>}
{c.rows.map((r, i) => {
const patch = (v: Partial<EconomicPosition>) => rowChange({...r, ...v});
const prefix = 'Crédito económico ' + (i + 1) + ' — ';
const field = (key: Exclude<keyof EconomicPosition, 'previousUse'>, label: string) => <label key={key} className="block">{label}<input aria-label={prefix + label} value={r[key]} onChange={e => patch({[key]: e.target.value})}/></label>;
const choice = (key: 'recipientResident' | 'payerResident' | 'payerIrpc' | 'payerExempt' | 'article40' | 'incomeIncluded', label: string) => <label key={key} className="block">{label}<select aria-label={prefix + label} value={r[key]} onChange={e => patch({[key]: e.target.value})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — documentado</option></select></label>;
const outside = !!r.kind && r.kind !== 'ordinary_profit' || r.article40 === 'yes';
const detail = <>
{([['distributionDocument', 'Documento da distribuição'], ['distributionParcel', 'Parcela dos lucros no documento'], ['distributionDate', 'Data da distribuição (AAAA-MM-DD)'], ['income', 'Lucros incluídos antes do crédito — MZN, ilíquidos de retenção'], ['incomeReference', 'Suporte da inclusão e do valor ilíquido na base corrente'], ['taxIssuer', 'Emitente do comprovativo de IRPC correspondente'], ['taxDocument', 'Documento do IRPC correspondente aos lucros'], ['taxParcel', 'Parcela desse IRPC no documento'], ['parcelReference', 'Suporte da identificação da parcela de imposto'], ['currency', 'Moeda comprovada'], ['proofAmount', 'IRPC comprovado nesta parcela — MZN'], ['taxAmount', 'IRPC correspondente aos lucros seleccionados — MZN'], ['attributionReference', 'Suporte da correspondência entre imposto e lucros'], ['previouslyAttributed', 'IRPC desta parcela já atribuído a outros lucros — MZN'], ['priorAttributionReference', 'Suporte das atribuições anteriores, incluindo zero conhecido'], ['grossUpIncluded', 'Crédito nominal já acrescido à base (art.53) — MZN'], ['grossUpReference', 'Suporte do acréscimo art.53 já incluído em L6'], ['nationalLimit', 'Quota proporcional da colecta após acréscimo — MZN'], ['nationalLimitReference', 'Suporte da quota proporcional da colecta']] as const).map(([key, label]) => field(key, label))}
{choice('incomeIncluded', 'Lucros ilíquidos e acréscimo incluídos na base corrente')}
<p>O cálculo confere 60% do IRPC correspondente com o acréscimo já incluído em L6 e a quota proporcional documentada. Se a base estiver incompleta, reconcilie-a no percurso existente. Esta acção conserva os valores e não altera a colecta automaticamente.</p>
<button type="button" onClick={() => patch({assessmentKey: book.assessmentKey})}>Confirmar inclusão e quota na colecta seleccionada</button>
<label className="block">Utilização anterior efectiva do crédito<select aria-label={prefix + 'Estado do uso anterior'} value={r.previousUse.state} onChange={e => patch({previousUse: {...r.previousUse, state: e.target.value as EconomicPosition['previousUse']['state']}})}><option value="">Informação em falta</option><option value="known">Montante conhecido</option><option value="absent">Ausência documentada</option></select></label>
{(r.previousUse.state === 'known' || r.previousUse.amount) && <label className="block">Crédito já utilizado — MZN<input aria-label={prefix + 'Uso anterior MZN'} value={r.previousUse.amount} onChange={e => patch({previousUse: {...r.previousUse, amount: e.target.value}})}/></label>}
<label className="block">Suporte do uso anterior ou da sua ausência<input aria-label={prefix + 'Suporte do uso anterior'} value={r.previousUse.reference} onChange={e => patch({previousUse: {...r.previousUse, reference: e.target.value}})}/></label>
</>;
return <fieldset key={r.id} className="space-y-2 rounded border p-3"><legend>Lucros {i + 1}</legend>
{field('subjectNuit', 'NUIT beneficiário')}{field('payerNuit', 'NUIT distribuidor')}{field('year', 'Exercício')}
<label className="block">Natureza<select aria-label={prefix + 'Natureza'} value={r.kind} onChange={e => patch({kind: e.target.value as EconomicPosition['kind']})}><option value="">Por identificar</option><option value="ordinary_profit">Lucros ordinários — art.64(1–2)</option><option value="liquidation">Partilha em liquidação — art.64(3)</option><option value="association">Associação — art.64(4)</option></select></label>
{residentConfirmed && r.recipientResident === 'yes' ? <p>Residência do beneficiário reutilizada da colecta corrente no regime geral. NUIT e exercício conservados.</p> : choice('recipientResident', 'Beneficiário com sede/direcção efectiva em Moçambique')}{choice('payerResident', 'Distribuidor com sede/direcção efectiva em Moçambique')}{choice('payerIrpc', 'Distribuidor sujeito a IRPC')}{choice('payerExempt', 'Distribuidor isento de IRPC')}{field('qualificationReference', 'Suporte da qualificação das entidades e dos lucros')}
{choice('article40', 'Esta parcela está abrangida pela exclusão do art.40(1)')}{field('article40Reference', 'Suporte da correspondência do art.40(1) e seu período')}
<p>O art.40(1), na redacção de 2009, contempla as entidades e condições próprias, incluindo participação directa de pelo menos20% e prazo de dois anos, nos termos da fonte. Uma parcela excluída da base não recebe também o crédito64. A simples percentagem de participação não decide todas as condições.</p>
{outside ? <><p>Esta parcela exige o tratamento específico identificado. O crédito ordinário não é aplicado; os valores introduzidos ficam conservados.</p><details><summary>Consultar ou reconciliar dados conservados</summary>{detail}</details></> : detail}
<button type="button" onClick={() => update({...c, rows: c.rows.filter(x => x.id !== r.id)})}>Retirar lucros {i + 1}</button>
</fieldset>;
})}
</fieldset>;
}
@@ -1,3 +1,4 @@
import {FiscalProofParcelFields} from './fiscal-proof-parcel';
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';
@@ -22,9 +23,9 @@ export function SimulationIrpcInternational({book, save}: {book: AnnualBook; sav
<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})}/>
{book.economicCredits ? <p>Resumo económico anterior conservado: {c.economic.state || "Informação em falta"}; {c.economic.amount || "Montante não preenchido"}. Nesta execução é usado o detalhe económico acima.</p> : <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>
<p>{book.economicCredits ? "Benefícios positivos exigem regras próprias; o crédito económico tem o seu detalhe separado. Informação em falta mantém o saldo dependente indeterminado." : "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});
@@ -47,7 +48,7 @@ export function SimulationIrpcInternational({book, save}: {book: AnnualBook; sav
{(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>
<FiscalProofParcelFields value={r} change={change} label={"Imposto estrangeiro " + (index + 1) + "." + (ri + 1)}/><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>
@@ -1,3 +1,4 @@
import {FiscalProofParcelFields} from './fiscal-proof-parcel';
import {newPecPayment, pecSource, type PecPayment} from '@/lib/irpc-pec';
import type {AnnualBook} from '@/lib/irpc-annual-settlement';
@@ -15,7 +16,7 @@ export function SimulationIrpcPec({book, save}: {book: AnnualBook; save: (book:
return <fieldset key={r.id} className="rounded border p-2"><legend>PEC {index + 1}</legend>
{([['document', 'Comprovativo PEC'], ['issuer', 'Emitente'], ['date', 'Data (AAAA-MM-DD)'], ['subjectNuit', 'NUIT beneficiário'], ['year', 'Exercício de origem'], ['currency', 'Moeda'], ['proofAmount', 'Montante comprovado'], ['previouslyUsed', 'Já utilizado fora destas parcelas'], ['priorUseReference', 'Suporte das utilizações anteriores ou do zero conhecido'], ['amount', 'Parcela disponível seleccionada'], ['reference', 'Suporte do pagamento PEC e afectação']] as const).map(([key, label]) => <label key={key} className="block">{label}<input aria-label={'PEC L8 ' + (index + 1) + ' — ' + label} value={r[key]} onChange={e => change({[key]: e.target.value})}/></label>)}
<label className="block">Pagamento PEC efectivamente comprovado<select aria-label={'PEC L8 ' + (index + 1) + ' — Efectivo'} value={r.effective} onChange={e => change({effective: e.target.value as PecPayment['effective']})}><option value="">Informação em falta</option><option value="yes">Sim — natureza PEC e pagamento reconciliados</option><option value="no">Não — previsão ou outra natureza</option></select></label>
<button type="button" onClick={() => update({...detail, rows: detail.rows.filter(x => x.id !== r.id)})}>Retirar selecção PEC {index + 1}</button>
<FiscalProofParcelFields value={r} change={change} label={"PEC L8 " + (index + 1)}/><button type="button" onClick={() => update({...detail, rows: detail.rows.filter(x => x.id !== r.id)})}>Retirar selecção PEC {index + 1}</button>
</fieldset>;
})}
<p>O detalhe substitui o resumo PEC apenas nesta execução. Os comprovativos e resultados de execuções anteriores permanecem no histórico; não há consumo automático de saldos oficiais.</p>
+12
View File
@@ -0,0 +1,12 @@
export type FiscalProofParcel = {parcel?: string; parcelReference?: string};
export const canonicalParcel = (s: string) => s.normalize('NFKC').trim().toLocaleLowerCase('pt').replace(/\s+/g, ' ');
export function validateProofParcel(r: FiscalProofParcel) {
if (r.parcel !== undefined && typeof r.parcel !== 'string' || r.parcelReference !== undefined && typeof r.parcelReference !== 'string') throw Error('Identificação da parcela documental não recuperável; original conservado.');
}
export function requireProofParcel(r: FiscalProofParcel) {
validateProofParcel(r);
if (!!r.parcel?.trim() !== !!r.parcelReference?.trim()) throw Error('Parcela documental e suporte devem corresponder; não se presume que linhas distintas representam impostos diferentes.');
}
export const parcelProjection = (r: FiscalProofParcel): FiscalProofParcel => ({...(r.parcel === undefined ? {} : {parcel: r.parcel}), ...(r.parcelReference === undefined ? {} : {parcelReference: r.parcelReference})});
// Old proofs without a parcel cover the physical document. They overlap any newly identified parcel until reconciled.
export const overlappingProofParcels = (a: FiscalProofParcel & {identity: string}, b: FiscalProofParcel & {identity: string}) => a.identity === b.identity && (!a.parcel?.trim() || !b.parcel?.trim() || canonicalParcel(a.parcel) === canonicalParcel(b.parcel));
+61 -26
View File
@@ -1,4 +1,6 @@
import {carryPlan,carryIdentity,carrySource,newCreditCarry,validateCreditCarry,type CreditCarry} from './irpc-credit-carry.ts';
import {economicTaxProof,economicGroupPending,economicDistributionIdentity,economicPosition,economicDeduction,economicSource,newEconomicCredits,validateEconomic,type EconomicCredits} from './irpc-economic-credit.ts';
import {validateProofParcel,requireProofParcel,parcelProjection,overlappingProofParcels,type FiscalProofParcel} from './fiscal-proof-parcel.ts';
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';
@@ -12,14 +14,20 @@ export const annualSettlementSource='CIRPC, arts. 63 e 67 (Lei 34/2007 e naturez
type State=''|'known'|'absent';
type Evidence={state:State;amount:string;reference:string};
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};
export type AnnualWithholding=FiscalProofParcel&{id:string;rowId:string;paymentId:string;sourceKey:string;subjectNuit:string;year:string;amount:string;incomeIncluded:''|'yes'|'no';reference:string};
export type AnnualPpc=FiscalProofParcel&{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;internationalCredits?:InternationalCredits;creditCarry?:CreditCarry;result?:AnnualRecord;history:AnnualRecord[]};
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;creditCarry?:CreditCarry;economicCredits?:EconomicCredits;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:'')+(b.creditCarry?' '+carrySource:'');
return annualSettlementSource.replace('PEC/créditos positivos',b.economicCredits?(b.pecPayments?'Benefícios positivos':'PEC/benefícios positivos'):excluded)+(b.pecPayments?' '+pecSource:'')+(b.internationalCredits?' '+internationalCreditSource:'')+(b.creditCarry?' '+carrySource:'')+(b.economicCredits?' '+economicSource:'');
};
export function prepareEconomicBook(b:AnnualBook):AnnualBook{
if(b.economicCredits)return b;
const next=prepareInternationalBook(b),e=newEconomicCredits(),old=next.internationalCredits!.economic;
if(old.state==='absent'){e.state='absent';e.reference=old.reference;}
return{...next,economicCredits:e};
}
export function prepareCreditCarry(b:AnnualBook):AnnualBook{return b.creditCarry?b:{...b,creditCarry:newCreditCarry()};}
export function prepareInternationalBook(b:AnnualBook):AnnualBook{
if(b.internationalCredits)return b;
@@ -43,6 +51,7 @@ const proofIdentity=(issuer:string,document:string,_date:string)=>JSON.stringify
function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros fiscais não recuperáveis;originais conservados.');return s;}
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?.economicCredits!==undefined)validateEconomic(b.economicCredits);
if(b?.creditCarry!==undefined)validateCreditCarry(b.creditCarry);
if(b?.pecPayments!==undefined)validatePec(b.pecPayments);
if(b?.internationalCredits!==undefined)validateInternational(b.internationalCredits);
@@ -51,6 +60,7 @@ export function validateAnnual(b:AnnualBook){
for(const g of[b.pec,b.credits])if(typeof g.amount!=='string')throw Error('CréditoL8 não recuperável.');
const w=['id','rowId','paymentId','sourceKey','subjectNuit','year','amount','incomeIncluded','reference'],p=Object.keys(newAnnualPpc(''));
for(const[rows,keys]of[[b.withholdings.rows,w],[b.ppc.rows,p]]as const)if(!Array.isArray(rows)||new Set(rows.map(r=>r?.id)).size!==rows.length||rows.some(r=>!r||keys.some(k=>typeof(r as unknown as Record<string,unknown>)[k]!=='string')))throw Error('ComprovativosL8 não recuperáveis ou identificador repetido.');
for(const r of[...b.withholdings.rows,...b.ppc.rows])validateProofParcel(r);
if(b.withholdings.rows.some(r=>!['','yes','no'].includes(r.incomeIncluded))||b.ppc.rows.some(r=>!['','yes','no'].includes(r.effective)))throw Error('QualificaçãoL8 não recuperável.');
for(const r of[b.result,...b.history])if(r&&(r.version!==annualSettlementVersion||typeof r.key!=='string'||typeof r.createdAt!=='string'||!r.model||!Array.isArray(r.model.pending)))throw Error('HistóricoL8 não recuperável.');
}
@@ -75,7 +85,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]:[]),...(b.internationalCredits?[b.internationalCredits]:[]),...(b.creditCarry?[b.creditCarry]:[])])];}
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]:[]),...(b.creditCarry?[b.creditCarry]:[]),...(b.economicCredits?[b.economicCredits]:[])])];}
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){
@@ -92,37 +102,55 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
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]),...(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}>=[];
const claims:Array<FiscalProofParcel&{id:string;kind:'withholding'|'ppc'|'pec'|'foreign_credit'|'economic_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(' ')});
try{requireProofParcel(r);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({...parcelProjection(r),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(' ')});
}
for(const r of b.ppc.rows){const issues:string[]=[];let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null;
try{if(!r.document.trim()||!r.issuer.trim()||!r.reference.trim()||r.effective!=='yes'||r.currency!=='MZN'||!iso(r.date)||!r.date.startsWith(b.year+'-')||r.subjectNuit!==b.subjectNuit||r.year!==b.year)throw Error('PPC exige comprovativo efectivo,MZN,data,entidade e exercício correspondentes;projecção/tesouraria não é pagamento');value=moneyInput(r.amount);proofAmount=moneyInput(r.proofAmount);if(cmp(value,proofAmount)>0n)throw Error('parcelaPPC ultrapassa o montante comprovado');}catch(e){issues.push(String(e));value=null;}
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(' ')});
try{requireProofParcel(r);if(!r.document.trim()||!r.issuer.trim()||!r.reference.trim()||r.effective!=='yes'||r.currency!=='MZN'||!iso(r.date)||!r.date.startsWith(b.year+'-')||r.subjectNuit!==b.subjectNuit||r.year!==b.year)throw Error('PPC exige comprovativo efectivo,MZN,data,entidade e exercício correspondentes;projecção/tesouraria não é pagamento');value=moneyInput(r.amount);proofAmount=moneyInput(r.proofAmount);if(cmp(value,proofAmount)>0n)throw Error('parcelaPPC ultrapassa o montante comprovado');}catch(e){issues.push(String(e));value=null;}
claims.push({...parcelProjection(r),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});}}
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({...parcelProjection(r),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});}}
if(b.internationalCredits){
pending.push(...internationalGroupPending(b.internationalCredits));
pending.push(...internationalGroupPending(b.internationalCredits,!!b.economicCredits));
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});}
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({...parcelProjection(r),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/crédito internacional);reconcilie sem dedução dupla.';
if(b.economicCredits){
pending.push(...economicGroupPending(b.economicCredits));
if(!b.internationalCredits)pending.push('Distinguir o detalhe económico do inventário internacional e dos benefícios; use a preparação existente sem apagar o resumo anterior.');
for(const r of b.economicCredits.rows){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,previouslyUsed:FiscalFraction|undefined,reason='';try{({value,proofAmount,previouslyUsed}=economicTaxProof(r));}catch(e){reason=String(e);}claims.push({id:r.id,kind:'economic_credit',positionId:r.id,identity:proofIdentity(r.taxIssuer,r.taxDocument,r.distributionDate),parcel:r.taxParcel,parcelReference:r.parcelReference,proofAmount,value,previouslyUsed,sourceKey:'',allocationAmount:null,document:r.taxDocument,issuer:r.taxIssuer,date:r.distributionDate,subjectNuit:r.subjectNuit,year:r.year,reason});}
}
const elsewhere:Array<FiscalProofParcel&{identity:string;subjectNuit:string;year:string;value:FiscalFraction;proofAmount:FiscalFraction;sourceKey:string;kind:'ppc'|'pec'|'withholding'|'foreign_credit'|'economic_credit';country?:string;previouslyUsed?:FiscalFraction}>=[];
for(const[scope,other]of Object.entries(books(d))){if(scope===acquisitionScope(d))continue;try{validateAnnual(other);for(const r of other.economicCredits?.rows??[]){
if(b.economicCredits?.rows.some(x=>economicDistributionIdentity(x)===economicDistributionIdentity(r)))pending.push('Parcela de lucros económicos já preparada noutro mapa;consolide sem duplicar o crédito, mesmo com documentos de imposto diferentes.');
const v=economicTaxProof(r);elsewhere.push({identity:proofIdentity(r.taxIssuer,r.taxDocument,r.distributionDate),parcel:r.taxParcel,parcelReference:r.parcelReference,subjectNuit:r.subjectNuit,year:r.year,...v,sourceKey:'',kind:'economic_credit'});
}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({...parcelProjection(r),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({...parcelProjection(r),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({...parcelProjection(r),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({...parcelProjection(r),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=>overlappingProofParcels(x,c)&&x.value),other=elsewhere.filter(x=>overlappingProofParcels(x,c));let issue='';
if(same.filter(x=>x.kind!=='withholding').length>1&&!same.every(x=>x.kind==='economic_credit')||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((c.kind==='pec'||c.kind==='economic_credit')&&c.previouslyUsed){if(other.some(x=>x.kind!==c.kind)||[...same,...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/IRPC económico com utilização/atribuiçã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/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(c.kind==='withholding'&&c.sourceKey&&c.allocationAmount&&cmp(addFractions([...claims.filter(x=>x.kind==='withholding'&&x.sourceKey===c.sourceKey).flatMap(x=>x.value?[x.value]:[]),...elsewhere.filter(x=>x.kind==='withholding'&&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 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 economicRows=b.economicCredits?.rows.map(r=>{
const issues=claims.filter(c=>c.kind==='economic_credit'&&c.positionId===r.id&&c.reason).map(c=>c.reason);
const m=economicPosition(r,b.subjectNuit,b.year,b.assessmentKey,colecta,issues);
pending.push(...m.pending.map(e=>'Lucros '+r.distributionDocument+': '+e));return m;
})??[];
if(b.economicCredits){
if(b.economicCredits.rows.length&&assessment?.kind!=='normal')pending.push('Crédito económico delimitado à colecta normal corrente;efeitos L7 exigem correspondência própria.');
}
const economicPreview=b.economicCredits?economicDeduction(economicRows,colecta,true):null;
const remainingForCarry=economicPreview?.afterCredit??colecta;
const pecAvailable=addFractions(claims.filter(c=>c.kind==='pec'&&!c.reason&&c.value).map(c=>c.value!));
const carryDuplicates:string[]=[];
if(b.creditCarry)for(const[scope,other]of Object.entries(books(d))){
@@ -130,17 +158,18 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
try{validateAnnual(other);if(other.creditCarry?.lots.some(r=>b.creditCarry!.lots.some(x=>carryIdentity(x)===carryIdentity(r))))carryDuplicates.push('Saldo anterior já preparado noutro mapa da entidade;consolide a origem e usos sem duplicar: '+scope);}
catch(e){carryDuplicates.push('Outro mapa da entidade não recuperável para conferir saldos anteriores: '+scope+';'+String(e));}
}
const carry=b.creditCarry?carryPlan(b.creditCarry,b.subjectNuit,b.year,b.assessmentKey,addFractions(positions.flatMap(p=>p.available&&!p.pending.length?[p.available]:[])),pecAvailable,colecta,carryDuplicates):null;
const carry=b.creditCarry?carryPlan(b.creditCarry,b.subjectNuit,b.year,b.assessmentKey,addFractions(positions.flatMap(p=>p.available&&!p.pending.length?[p.available]:[])),pecAvailable,remainingForCarry,carryDuplicates):null;
if(carry)pending.push(...carry.pending);
const ready=pending.length===0&&colecta!==null,international=b.internationalCredits?internationalDeduction(positions,colecta,ready,carry?.international??undefined):null;
const creditCollecta=ready&&carry?subtractFractions(international?.afterCredit??colecta!,carry.priorInternational):international?.afterCredit??colecta;
const ready=pending.length===0&&colecta!==null,economic=b.economicCredits?economicDeduction(economicRows,colecta,ready):null,afterEconomic=economic?.afterCredit??colecta;
const international=b.internationalCredits?internationalDeduction(positions,afterEconomic,ready,carry?.international??undefined):null;
const creditCollecta=ready&&carry?subtractFractions(international?.afterCredit??afterEconomic!,carry.priorInternational):international?.afterCredit??afterEconomic;
const pec=b.pecPayments?pecDeduction(pecAvailable,creditCollecta,ready,carry?.pec??undefined):null;
const afterPec=ready&&carry?subtractFractions(pec?.afterPec??creditCollecta!,carry.priorPec):pec?.afterPec??creditCollecta;
const arithmeticBalance=ready?subtractFractions(subtractFractions(afterPec!,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,internationalCredit:international,pecDeduction:pec,...(carry?{creditCarry:{...carry,afterInternational:ready?creditCollecta:null,afterPec:ready?afterPec:null}}:{}),knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.creditCarry?['Saldos anteriores documentados: escolha explícita das parcelas correntes e anteriores, sem prioridade presumida entre exercícios, reembolso ou criação/consumo de saldos oficiais.']:[]),...(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.creditCarry?'L8 parcial:créditos económicos/benefícios,obrigações,tributação autónoma,períodos especiais,outras moedas e correspondência M20/M22 separados. Prazos dos saldos anteriores demonstrados apenas para origens anuais ordinárias documentadas;sem criação automática de transporte.':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.']};
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,...(economic?{economicCredit:economic}:{}),internationalCredit:international,pecDeduction:pec,...(carry?{creditCarry:{...carry,afterInternational:ready?creditCollecta:null,afterPec:ready?afterPec:null}}:{}),knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.economicCredits?['Crédito económico ordinário:IRPC correspondente aos lucros,acréscimo53 já incluído e quota proporcional documentados. Sem cálculo automático da inclusão ou da exclusão40;liquidação64(3),associações64(4),benefícios e regras especiais separados. Sem reembolso ou transporte económico presumidos.']:[]),...(b.creditCarry?['Saldos anteriores documentados: escolha explícita das parcelas correntes e anteriores, sem prioridade presumida entre exercícios, reembolso ou criação/consumo de saldos oficiais.']:[]),...(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. '+(b.economicCredits?'Benefícios positivos ficam separados;crédito económico no detalhe próprio.':'Créditos económicos/benefícios positivos ficam separados.')]:[]),b.economicCredits?'L8 parcial:benefícios próprios,derivação automática de quotas,inclusão/correcção económica na base,liquidação/associações,obrigações,autónomos,períodos especiais,moedas eM20/M22 separados. Saldos anteriores só quando detalhados e documentados.':b.creditCarry?'L8 parcial:créditos económicos/benefícios,obrigações,tributação autónoma,períodos especiais,outras moedas e correspondência M20/M22 separados. Prazos dos saldos anteriores demonstrados apenas para origens anuais ordinárias documentadas;sem criação automática de transporte.':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)}};}
@@ -155,10 +184,16 @@ 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':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==='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??'')
'IRPC — L8',(c.kind==='withholding'?'Retenção sofrida':c.kind==='pec'?'PEC':c.kind==='economic_credit'?'IRPC correspondente aos lucros':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.parcel?'; parcela documental: '+c.parcel+'; '+(c.parcelReference??''):'')+'. '+(c.reason||'Reconciliada sem utilização duplicada.'),
c.kind==='economic_credit'?(m.input.economicCredits?.reference??'')+'; '+(m.input.economicCredits?.rows.find(r=>r.id===c.id)?.attributionReference??''):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.economicCredit?[
...m.economicCredit.rows.map(r=>['IRPC — L8','Crédito económico — '+r.input.distributionDocument,
'Lucros incluídos: '+r.input.income+' MZN;IRPC correspondente: '+r.input.taxAmount+' MZN;crédito nominal60%: '+show(r.nominal)+';acréscimo53 incluído: '+r.input.grossUpIncluded+' MZN;quota proporcional: '+show(r.nationalLimit)+';elegível: '+show(r.eligible)+';uso efectivo anterior: '+show(r.previousUse)+';disponível: '+show(r.available)+';estado: '+r.status+'. '+r.pending.join(' | '),
r.input.qualificationReference+';40(1): '+r.input.article40Reference+';inclusão: '+r.input.incomeReference+';53: '+r.input.grossUpReference+';quota: '+r.input.nationalLimitReference+';uso: '+r.input.previousUse.reference]as[string,string,string,string]),
['IRPC — L8','Crédito económico — antes do internacional','Disponível: '+show(m.economicCredit.available)+';dedução: '+show(m.economicCredit.deducted)+';colecta após crédito económico: '+show(m.economicCredit.afterCredit)+'. Sem reembolso ou criação/consumo de saldo oficial.',m.economicCredit.source+'; '+(m.input.economicCredits?.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)+(m.creditCarry?'; crédito corrente não escolhido: '+show(m.internationalCredit.notSelected??null):'')+'. 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]
+78
View File
@@ -0,0 +1,78 @@
import {addFractions, fraction, subtractFractions, type FiscalFraction} from './fiscal-rational.ts';
import {creditCompare, creditMoney, creditLimit, type CreditEvidence, type CreditState} from './irpc-international-credit.ts';
import {canonicalParcel} from './fiscal-proof-parcel.ts';
export const economicVersion = 'IRPC-economic-credit/1';
export const economicSource = 'CIRPC64(1–2), Lei34/2007, BR52, PDF18/p768-(72):60% do IRPC correspondente aos lucros distribuídos incluídos na base, limitado à colecta proporcional após acréscimo53(1)a (PDF15/p768-(69)). Distinto de65 e da exclusão40(1), na redacçãoLei20/2009,PDF4/p272-(16). RIRPC21(2/5),PDF6/p121:económico antes do internacional/benefícios/PEC,até à colecta,sem reembolso. Recorte2026,residente geral,ano civil,MZN,lucros ordinários,qualificação e quota proporcional documentadas na colecta corrente. Sem alterar a base por presunção, transporte ou consumo de saldos oficiais.';
type Choice = '' | 'yes' | 'no';
export type EconomicPosition = {
id: string; subjectNuit: string; year: string; assessmentKey: string;
kind: '' | 'ordinary_profit' | 'liquidation' | 'association'; payerNuit: string;
recipientResident: Choice; payerResident: Choice; payerIrpc: Choice; payerExempt: Choice;
article40: Choice; qualificationReference: string; article40Reference: string;
distributionDocument: string; distributionParcel: string; distributionDate: string;
income: string; incomeIncluded: Choice; incomeReference: string;
taxIssuer: string; taxDocument: string; taxParcel: string; parcelReference: string;
currency: string; proofAmount: string; taxAmount: string; attributionReference: string;
previouslyAttributed: string; priorAttributionReference: string;
grossUpIncluded: string; grossUpReference: string; nationalLimit: string; nationalLimitReference: string;
previousUse: CreditEvidence;
};
export type EconomicCredits = {version: 1; state: CreditState; reference: string; rows: EconomicPosition[]};
export const newEconomicCredits = (): EconomicCredits => ({version: 1, state: '', reference: '', rows: []});
export const newEconomicPosition = (id: string): EconomicPosition => ({id, subjectNuit: '', year: '', assessmentKey: '', kind: '', payerNuit: '', recipientResident: '', payerResident: '', payerIrpc: '', payerExempt: '', article40: '', qualificationReference: '', article40Reference: '', distributionDocument: '', distributionParcel: '', distributionDate: '', income: '', incomeIncluded: '', incomeReference: '', taxIssuer: '', taxDocument: '', taxParcel: '', parcelReference: '', currency: 'MZN', proofAmount: '', taxAmount: '', attributionReference: '', previouslyAttributed: '', priorAttributionReference: '', grossUpIncluded: '', grossUpReference: '', nationalLimit: '', nationalLimitReference: '', previousUse: {state: '', amount: '', reference: ''}});
export function validateEconomic(c: EconomicCredits) {
if (!c || c.version !== 1 || !['', 'known', 'absent'].includes(c.state) || typeof c.reference !== 'string' || !Array.isArray(c.rows) || new Set(c.rows.map(r => r?.id)).size !== c.rows.length) throw Error('Crédito económico não recuperável; originais conservados.');
const keys = Object.keys(newEconomicPosition('')).filter(k => k !== 'previousUse');
for (const r of c.rows) if (!r || keys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !['', 'ordinary_profit', 'liquidation', 'association'].includes(r.kind) || ['recipientResident', 'payerResident', 'payerIrpc', 'payerExempt', 'article40', 'incomeIncluded'].some(k => !['', 'yes', 'no'].includes(r[k as 'payerIrpc'])) || !r.previousUse || !['', 'known', 'absent'].includes(r.previousUse.state) || typeof r.previousUse.amount !== 'string' || typeof r.previousUse.reference !== 'string') throw Error('Posição económica não recuperável; originais conservados.');
}
const zero = () => fraction(0n);
const minimum = (...v: FiscalFraction[]) => v.reduce((a, b) => creditCompare(a, b) < 0n ? a : b);
const validDate = (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 const economicDistributionIdentity = (r: EconomicPosition) => JSON.stringify([r.payerNuit, canonicalParcel(r.distributionDocument), canonicalParcel(r.distributionParcel)]);
export function economicTaxProof(r: EconomicPosition) {
if (!r.taxIssuer.trim() || !r.taxDocument.trim() || !r.taxParcel.trim() || !r.parcelReference.trim() || !r.attributionReference.trim() || !r.priorAttributionReference.trim() || r.currency !== 'MZN') throw Error('IRPC correspondente exige documento, emitente/parcela, suporte da atribuição aos lucros e montante MZN; não é retenção do beneficiário nem imposto estrangeiro.');
const proofAmount = creditMoney(r.proofAmount), value = creditMoney(r.taxAmount), previouslyUsed = creditMoney(r.previouslyAttributed);
if (creditCompare(addFractions([value, previouslyUsed]), proofAmount) > 0n) throw Error('IRPC atribuído aos lucros mais parcelas já atribuídas ultrapassam o comprovado disponível.');
return {proofAmount, value, previouslyUsed};
}
export function economicGroupPending(c: EconomicCredits) {
const pending: string[] = [];
if (!c.state || !c.reference.trim()) pending.push('Crédito económico: inventário conhecido/ausente e suporte em falta.');
if (c.state === 'absent' && c.rows.length) pending.push('Ausência de crédito económico contradiz lucros registados; conserve e reconcilie.');
if (c.state === 'known' && !c.rows.length) pending.push('Crédito económico conhecido sem lucros/IRPC discriminados; zero não é ausência presumida.');
if (new Set(c.rows.map(economicDistributionIdentity)).size !== c.rows.length) pending.push('Mesma parcela de lucros distribuídos repetida; consolide a origem, mesmo que o documento de imposto ou a data tenham sido alterados.');
return pending;
}
export function economicPosition(r: EconomicPosition, subjectNuit: string, year: string, assessmentKey: string, colecta: FiscalFraction | null, proofIssues: string[]) {
const pending = [...proofIssues];
let nominal: FiscalFraction | null = null, nationalLimit: FiscalFraction | null = null, eligible: FiscalFraction | null = null, previousUse: FiscalFraction | null = null, available: FiscalFraction | null = null, included: FiscalFraction | null = null;
let status: 'qualified' | 'outside_scope' | 'indeterminate' = 'indeterminate';
try {
if (!r.id.trim() || !/^\d{9}$/.test(r.payerNuit) || r.subjectNuit !== subjectNuit || year !== '2026' || r.year !== year || r.currency !== 'MZN') throw Error('Titular, entidade distribuidora, exercício2026 e MZN por corresponder.');
if (r.kind !== 'ordinary_profit') {status = r.kind ? 'outside_scope' : 'indeterminate'; throw Error('Lucros ordinários64(1–2) por identificar; liquidação64(3) e associações64(4) exigem correspondência específica, sem converter a parcela em crédito internacional.');}
if (r.recipientResident !== 'yes' || r.payerResident !== 'yes' || r.payerIrpc !== 'yes' || r.payerExempt !== 'no' || !r.qualificationReference.trim()) throw Error('Exige sede/direcção efectiva em Moçambique de beneficiário e distribuidor, distribuidor sujeito a IRPC e não isento, com suporte.');
if (r.article40 !== 'no' || !r.article40Reference.trim()) {status = r.article40 === 'yes' ? 'outside_scope' : 'indeterminate'; throw Error(r.article40 === 'yes' ? 'Exclusão40(1) identificada: tratar na base; não somar crédito64 à mesma parcela excluída.' : 'Falta corresponder a não aplicação de40(1), incluindo a redacção20/2009; desconhecido não significa não aplicável.');}
if (!validDate(r.distributionDate) || !r.distributionDate.startsWith(year + '-') || !r.distributionDocument.trim() || !r.distributionParcel.trim()) throw Error('Documento/parcela e data da distribuição do exercício por identificar.');
if (!r.assessmentKey || r.assessmentKey !== assessmentKey || r.incomeIncluded !== 'yes' || !r.incomeReference.trim()) throw Error('Confirme a inclusão dos lucros ilíquidos de retenção e a correspondência com a colecta corrente; a origem pode ter mudado.');
const tax = economicTaxProof(r), income = creditMoney(r.income);
nominal = fraction(BigInt(tax.value.numerator) * 3n, BigInt(tax.value.denominator) * 5n);
if (creditCompare(income, zero()) === 0n && creditCompare(nominal, zero()) > 0n) throw Error('Lucros incluídos zero contradizem IRPC correspondente positivo.');
if (!r.grossUpReference.trim() || creditCompare(creditLimit(r.grossUpIncluded), nominal) !== 0n) throw Error('Acréscimo53(1)a incluído na base deve corresponder ao crédito nominal60%; reconcilie L6 sem alterar a colecta ou o histórico automaticamente.');
included = addFractions([income, nominal]);
nationalLimit = creditLimit(r.nationalLimit);
if (!r.nationalLimitReference.trim() || !colecta || creditCompare(nationalLimit, colecta) > 0n || creditCompare(included, zero()) === 0n && creditCompare(nationalLimit, zero()) > 0n) throw Error('Quota proporcional da colecta após acréscimo53 exige suporte e correspondência; não pode exceder a colecta nem ser positiva para rendimento zero.');
if (!r.previousUse.state || !r.previousUse.reference.trim()) throw Error('Utilização anterior efectiva do crédito em falta; simulação não é utilização fiscal.');
if (r.previousUse.state === 'absent') {if (r.previousUse.amount.trim()) throw Error('Ausência de uso anterior contradiz montante'); previousUse = zero();}
else previousUse = creditLimit(r.previousUse.amount);
eligible = minimum(nominal, nationalLimit);
if (creditCompare(previousUse, eligible) > 0n) throw Error('Uso anterior excede o crédito económico elegível.');
if (!pending.length) {available = subtractFractions(eligible, previousUse); status = 'qualified';}
} catch (e) {pending.push(String(e));}
return {id: r.id, nominal, nationalLimit, eligible, previousUse, available, included, status, pending, input: structuredClone(r)};
}
export function economicDeduction(rows: ReturnType<typeof economicPosition>[], colecta: FiscalFraction | null, ready: boolean) {
const available = addFractions(rows.flatMap(r => r.available && !r.pending.length ? [r.available] : []));
const deducted = ready && colecta ? minimum(available, colecta) : null;
return {version: economicVersion, rows, available, deducted, afterCredit: deducted && colecta ? subtractFractions(colecta, deducted) : null, source: economicSource, officialBalancesConsumed: false, carryCreated: false};
}
@@ -1,4 +1,5 @@
import type {AnnualPpc} from './irpc-annual-settlement.ts';
import {validateProofParcel, requireProofParcel} from './fiscal-proof-parcel.ts';
import {countryIsoCodes} from './country-iso-codes.ts';
import {addFractions, subtractFractions, decimalFraction, fraction, type FiscalFraction} from './fiscal-rational.ts';
@@ -32,6 +33,7 @@ export function validateInternational(c: InternationalCredits) {
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) {
validateProofParcel(r);
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);
}
@@ -53,18 +55,19 @@ 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) {
requireProofParcel(r);
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) {
export function internationalGroupPending(c: InternationalCredits, economicDetailed = false) {
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) {
for (const [label, e] of [...(economicDetailed ? [] : [['Dupla tributação económica', c.economic] as const]), ['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');}
+4 -1
View File
@@ -1,8 +1,9 @@
import {subtractFractions, decimalFraction, type FiscalFraction} from './fiscal-rational.ts';
import {validateProofParcel, requireProofParcel, type FiscalProofParcel} from './fiscal-proof-parcel.ts';
export const pecVersion = 'IRPC-PEC/1';
export const pecSource = 'CIRPC, art. 66(1–2), Lei 34/2007, exemplar BR52, p. 768-(72): dedução após créditos anteriores, até à colecta, sem reembolso do PEC. Recorte demonstrado: pagamento efectivo do próprio exercício 2026, residente geral, ano civil, MZN. Não calcula a obrigação PEC, transporte, caducidade ou consumo de saldos oficiais.';
export type PecPayment = {
export type PecPayment = FiscalProofParcel & {
id: string; document: string; issuer: string; date: string; subjectNuit: string;
year: string; currency: string; proofAmount: string; amount: string;
effective: '' | 'yes' | 'no'; reference: string;
@@ -15,6 +16,7 @@ export function validatePec(p: PecPayments) {
if (!p || p.version !== 1 || !['', 'known', 'absent'].includes(p.state) || typeof p.reference !== 'string' || !Array.isArray(p.rows) || new Set(p.rows.map(r => r?.id)).size !== p.rows.length || p.rows.some(r => !r || keys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !['', 'yes', 'no'].includes(r.effective))) {
throw Error('Comprovativos PEC não recuperáveis; originais conservados.');
}
for (const r of p.rows) validateProofParcel(r);
}
export const pecCompare = (a: FiscalFraction, b: FiscalFraction) => BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator);
const money = (s: string) => {
@@ -22,6 +24,7 @@ const money = (s: string) => {
return decimalFraction(s);
};
export function pecPaymentValue(r: PecPayment, subjectNuit: string, year: string) {
requireProofParcel(r);
if (!r.id.trim() || !r.document.trim() || !r.issuer.trim() || !r.reference.trim() || !r.priorUseReference.trim() || r.effective !== 'yes' || r.currency !== 'MZN' || !/^20\d{2}-\d{2}-\d{2}$/.test(r.date) || !Number.isFinite(Date.parse(r.date)) || new Date(r.date).toISOString().slice(0, 10) !== r.date || !r.date.startsWith(year + '-') || r.subjectNuit !== subjectNuit || r.year !== year || year !== '2026') {
throw Error('PEC: exige prova efectiva, natureza PEC, titular, exercício 2026, data válida e MZN; saldos anteriores e previsões ficam separados.');
}
@@ -0,0 +1,83 @@
<?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 IrpcEconomicCreditDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_economic_credit_and_compatible_carry_round_trip_without_overwriting_accepted_history(): 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));
$fixture = json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-economic-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
$this->assertTrue($fixture['synthetic']);
$this->assertFalse($fixture['actualTaxEvidence']);
$id = null;
$etag = null;
foreach ($fixture['cases'] as $name => $draft) {
$personal = ['person_type' => $draft['person_type']];
unset($draft['person_type']);
$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']);
$original = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$recovered = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
foreach (['irpcNormal', 'irpcWithholdings', 'books', 'currencyEvents', 'ispcQuarters'] as $map) {
$this->assertSame($original[$map], $recovered[$map]);
}
$book = $recovered['irpcAnnual'][$draft['context_opportunity']];
$model = $book['result']['model'];
$this->assertSame($original['irpcAnnual'][$draft['context_opportunity']], $book);
$this->assertCount(1, $book['history']);
$this->assertSame('IRPC-economic-credit/1', $model['economicCredit']['version']);
$this->assertFalse($model['economicCredit']['carryCreated']);
$this->assertFalse($model['economicCredit']['officialBalancesConsumed']);
$this->assertFalse($model['submitted']);
if (in_array($name, ['duplicate', 'missing-gross-up', 'article40', 'partial-parcel'], true)) {
$this->assertSame('indeterminate', $model['status']);
$this->assertNull($model['arithmeticBalance']);
$this->assertNull($model['economicCredit']['deducted']);
$this->assertNull($model['internationalCredit']['deducted']);
$this->assertNull($model['pecDeduction']['deducted']);
} else {
$expected = ['payable' => ['688', '1'], 'combined' => ['538', '1'], 'quota' => ['695', '1'], 'prior-use' => ['691', '1'], 'shared-paper' => ['688', '1'], 'fractional' => ['349997', '500'], 'recoverable' => ['-500', '1']][$name];
$this->assertEmpty($model['pending']);
$this->assertSame(['numerator' => $expected[0], 'denominator' => $expected[1]], $model['arithmeticBalance']);
}
if (getenv('INVESTMOZ_ECONOMIC_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-economic-20261028/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_ECONOMIC_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-economic-20261028/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,25 @@
import {internationalCase} from './irpc-international-case.mjs';
import {carryCase} from './irpc-carry-case.mjs';
import {prepareEconomicBook} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newEconomicPosition} from '../../../resources/js/lib/irpc-economic-credit.ts';
export function economicCase(withCarry = false) {
const c = withCarry ? carryCase() : internationalCase();
c.b = prepareEconomicBook(c.b);
Object.assign(c.b.economicCredits, {state: 'known', reference: 'SYNTHETIC QA full ordinary economic-credit inventory', rows: [{
...newEconomicPosition('economic-1'), subjectNuit: c.b.subjectNuit, year: c.b.year, assessmentKey: c.b.assessmentKey,
kind: 'ordinary_profit', payerNuit: '111222333', recipientResident: 'yes', payerResident: 'yes', payerIrpc: 'yes', payerExempt: 'no',
article40: 'no', qualificationReference: 'SYNTHETIC QA entities seated/effectively managed in MZ; payer subject nonexempt IRPC',
article40Reference: 'SYNTHETIC QA explicit nonapplication of40(1), including2009 participation/period/recipient conditions',
distributionDocument: 'SYNTHETIC-DIVIDEND2026', distributionParcel: 'distribution-1', distributionDate: '2026-09-30',
income: '100', incomeIncluded: 'yes', incomeReference: 'SYNTHETIC QA100 gross of beneficiary withholding already included in current L6; no actual dividend assertion',
taxIssuer: 'SYNTHETIC domestic IRPC issuer', taxDocument: 'SYNTHETIC-SOURCE-IRPC2026', taxParcel: 'underlying-tax-1',
parcelReference: 'SYNTHETIC QA underlying domestic IRPC parcel, distinct from recipient withholding or foreign income tax',
proofAmount: '20', taxAmount: '20', attributionReference: 'SYNTHETIC QA documented IRPC20 corresponding to included profits100, not inferred by applying a rate to dividends',
previouslyAttributed: '0', priorAttributionReference: 'SYNTHETIC QA actual previous attribution to other profits known zero',
grossUpIncluded: '12', grossUpReference: 'SYNTHETIC QA53 nominal credit12 already included in reconciled current base4400; no base mutation',
nationalLimit: '32', nationalLimitReference: 'SYNTHETIC QA documented proportional current collecta ceiling32 after credit addition; not an automatic fraction derivation',
previousUse: {state: 'known', amount: '0', reference: 'SYNTHETIC QA actual prior economic-credit use known zero'},
}]});
return c;
}
@@ -0,0 +1,141 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {economicCase} from './helpers/irpc-economic-case.mjs';
import {internationalCase} from './helpers/irpc-international-case.mjs';
import {calculateAnnual, prepareEconomicBook, annualBook, recordAnnual, saveAnnualBook, annualCurrent, annualHtml, annualReportRows} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
import {irpcBook, recordIrpc, saveIrpcBook} from '../../resources/js/lib/irpc-withholdings.ts';
const number = v => v ? Number(v.numerator) / Number(v.denominator) : null;
const row = c => c.b.economicCredits.rows[0];
const foreign = c => c.b.internationalCredits.positions[0].payments[0];
const calc = c => calculateAnnual(c.d, c.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.economicCredit.deducted, null); assert.equal(m.internationalCredit.deducted, null); assert.equal(m.pecDeduction.deducted, null); return m;};
test('economic60% of documented sourceIRPC20 yields12 before international8,PEC200 andPPC500:688', () => {
const m = calc(economicCase()); assert.deepEqual(m.pending, []); assert.equal(number(m.economicCredit.rows[0].nominal), 12);
assert.equal(number(m.economicCredit.deducted), 12); assert.equal(number(m.economicCredit.afterCredit), 1396);
assert.equal(number(m.internationalCredit.deducted), 8); assert.equal(number(m.internationalCredit.afterCredit), 1388); assert.equal(number(m.payable), 688);
assert.equal(m.economicCredit.carryCreated, false); assert.equal(m.economicCredit.officialBalancesConsumed, false); assert.equal(m.submitted, false);
});
test('documented proportional ceiling and prior actual use limit economic deduction,not a second tax rate on dividends', () => {
const c = economicCase(); row(c).nationalLimit = '5'; let m = calc(c); assert.equal(number(m.economicCredit.rows[0].nominal), 12); assert.equal(number(m.payable), 695);
row(c).previousUse.amount = '3'; m = calc(c); assert.equal(number(m.economicCredit.deducted), 2); assert.equal(number(m.payable), 698);
row(c).previousUse.amount = '6'; pending(c);
});
test('article40 exclusion, payer exemption/foreign residence and special distributions do not become international credit', () => {
for (const patch of [{article40: ''}, {article40: 'yes'}, {article40Reference: ''}, {payerResident: ''}, {payerResident: 'no'}, {recipientResident: 'no'}, {payerIrpc: 'no'}, {payerExempt: ''}, {payerExempt: 'yes'}, {kind: 'liquidation'}, {kind: 'association'}, {kind: ''}, {qualificationReference: ''}]) {
const c = economicCase(); Object.assign(row(c), patch); const m = pending(c); assert.equal(m.internationalCredit.positions.length, 1); assert.equal(number(m.internationalCredit.positions[0].eligible), 8);
if (patch.article40 === 'yes' || patch.kind === 'liquidation' || patch.kind === 'association') assert.equal(m.economicCredit.rows[0].status, 'outside_scope');
}
});
test('inclusion,credit gross-up and current assessment must be reconciled without silently changing L6', () => {
for (const patch of [{incomeIncluded: ''}, {incomeIncluded: 'no'}, {incomeReference: ''}, {assessmentKey: ''}, {assessmentKey: 'stale'}, {grossUpIncluded: ''}, {grossUpIncluded: '0'}, {grossUpIncluded: '11.99'}, {grossUpReference: ''}, {nationalLimit: ''}, {nationalLimit: '1409'}, {nationalLimitReference: ''}, {income: '0'}]) {
const c = economicCase(), before = c.d.modelo_a_snapshot; Object.assign(row(c), patch); pending(c); assert.equal(c.d.modelo_a_snapshot, before);
}
});
test('dividend/source tax identity,date,year,subject and actual allocated source amount require support', () => {
for (const patch of [{subjectNuit: '999999999'}, {payerNuit: ''}, {year: '2025'}, {distributionDate: '2026-02-30'}, {distributionDate: '2025-12-31'}, {distributionDocument: ''}, {distributionParcel: ''}, {taxIssuer: ''}, {taxDocument: ''}, {taxParcel: ''}, {parcelReference: ''}, {currency: 'USD'}, {proofAmount: ''}, {taxAmount: '21'}, {taxAmount: ''}, {attributionReference: ''}, {previouslyAttributed: ''}, {previouslyAttributed: '1'}, {priorAttributionReference: ''}]) {
const c = economicCase(); Object.assign(row(c), patch); pending(c);
}
});
test('same distributed profit parcel cannot be re-used by changing source-tax document,id or date', () => {
const c = economicCase(); c.b.economicCredits.rows.push({...structuredClone(row(c)), id: 'other', taxDocument: 'OTHER', distributionDate: '2026-10-01', distributionDocument: ' synthetic-dividend2026 ', distributionParcel: ' DISTRIBUTION-1 '});
assert.match(pending(c).pending.join(' '), /Mesma parcela de lucros/);
});
test('one source-tax parcel can support distinct documented distributions only within its actual available amount', () => {
const c = economicCase(), other = {...structuredClone(row(c)), id: 'second', distributionParcel: 'distribution-2'};
c.b.economicCredits.rows.push(other); assert.match(pending(c).pending.join(' '), /disponível/);
for (const r of c.b.economicCredits.rows) {r.taxAmount = '10'; r.grossUpIncluded = '6';}
let m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.economicCredit.deducted), 12);
row(c).previouslyAttributed = '1'; other.previouslyAttributed = '1'; pending(c);
row(c).taxAmount = '9'; other.taxAmount = '9'; row(c).grossUpIncluded = '5.4'; other.grossUpIncluded = '5.4'; m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.economicCredit.deducted), 10.8);
other.previouslyAttributed = '0'; assert.match(pending(c).pending.join(' '), /divergente/);
});
test('economic and international use of the same actual tax parcel is rejected even with changed receipt date', () => {
const c = economicCase(); Object.assign(foreign(c), {issuer: row(c).taxIssuer, document: row(c).taxDocument, parcel: ' UNDERLYING-TAX-1 ', parcelReference: 'SYNTHETIC same parcel', proofAmount: '20', date: '2026-10-01'});
assert.match(pending(c).pending.join(' '), /natureza|naturezas/);
});
test('an old proof with no parcel overlaps the whole document;new names require supported partition on both sides', () => {
const c = economicCase(); Object.assign(foreign(c), {issuer: row(c).taxIssuer, document: row(c).taxDocument, proofAmount: '20'}); pending(c);
foreign(c).parcel = 'foreign-tax-line'; pending(c); foreign(c).parcelReference = 'SYNTHETIC evidence of a distinct foreign-tax line in shared document';
const m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 688);
});
test('economic source IRPC is not beneficiary withholding,PPC or PEC;distinct supported parcels may share a document', () => {
for (const kind of ['ppc', 'pec']) {
const c = economicCase(), proof = kind === 'ppc' ? c.b.ppc.rows[0] : c.b.pecPayments.rows[0];
Object.assign(row(c), {taxIssuer: proof.issuer, taxDocument: proof.document, proofAmount: proof.proofAmount}); pending(c);
Object.assign(proof, {parcel: 'own-' + kind, parcelReference: 'SYNTHETIC distinct payment parcel'}); assert.deepEqual(calc(c).pending, []);
proof.parcel = row(c).taxParcel; pending(c);
}
});
test('underlying domestic IRPC and beneficiary withholding remain separate even when supported by the same paper', () => {
const c = internationalCase('10', '8', '200', '500', '300'); c.b = prepareEconomicBook(c.b); c.b.economicCredits = economicCase().b.economicCredits;
const withholding = c.b.withholdings.rows[0], proof = calc(c).claims.find(x => x.kind === 'withholding');
Object.assign(row(c), {taxIssuer: proof.issuer, taxDocument: proof.document, proofAmount: '300'}); pending(c);
Object.assign(withholding, {parcel: 'beneficiary-withholding', parcelReference: 'SYNTHETIC separate recipient-tax parcel'});
assert.equal(number(calc(c).payable), 388);
const other = structuredClone(c.b), s = JSON.parse(c.d.modelo_a_snapshot);
other.economicCredits.rows = []; other.economicCredits.state = 'absent'; other.internationalCredits.positions = []; other.internationalCredits.state = 'absent';
other.ppc.rows = []; other.ppc.state = 'absent'; other.pecPayments.rows = []; other.pecPayments.state = 'absent';
other.withholdings.rows[0].parcel = 'relabelled-same-L5-allocation';
s.irpcAnnual = {'opportunity:other': other}; s.irpcWithholdings['opportunity:other'] = structuredClone(Object.values(s.irpcWithholdings)[0]); c.d.modelo_a_snapshot = JSON.stringify(s);
c.d.modelo_a_snapshot = saveIrpcBook(c.d, recordIrpc(c.d, irpcBook(c.d), '2026-10-28T14:00:00+02:00'));
assert.match(pending(c).pending.join(' '), /calculada e reconciliada emL5|afectação L5/);
});
test('documented economic inclusion is not rejected merely because fiscal losses reduce total taxable base below dividends', () => {
const c = economicCase(); row(c).income = '5000';
const m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 688);
});
test('cross-context source capacity and distributed-income duplication cannot be evaded with new ids/documents', () => {
const c = economicCase(), other = structuredClone(c.b), s = JSON.parse(c.d.modelo_a_snapshot);
other.internationalCredits.positions = []; other.internationalCredits.state = 'absent'; other.ppc.rows = []; other.ppc.state = 'absent'; other.pecPayments.rows = []; other.pecPayments.state = 'absent';
other.economicCredits.rows[0].id = 'other-id'; other.economicCredits.rows[0].taxDocument = 'OTHER';
s.irpcAnnual = {'opportunity:other': other}; c.d.modelo_a_snapshot = JSON.stringify(s); assert.match(pending(c).pending.join(' '), /lucros económicos já/);
other.economicCredits.rows[0].distributionParcel = 'other-distribution'; other.economicCredits.rows[0].taxDocument = row(c).taxDocument; c.d.modelo_a_snapshot = JSON.stringify(s); pending(c);
other.economicCredits.rows[0].taxAmount = '10'; row(c).taxAmount = '10'; row(c).grossUpIncluded = '6'; c.d.modelo_a_snapshot = JSON.stringify(s);
assert.deepEqual(calc(c).pending, []);
});
test('economic and accepted carry increments compose:1408-12-8-50-200-100-500=538', () => {
const m = calc(economicCase(true)); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 538);
assert.equal(number(m.creditCarry.afterInternational), 1338); assert.equal(number(m.creditCarry.afterPec), 1038);
});
test('economic priority leaves prior explicit allocations intact when remaining collecta becomes insufficient', () => {
const c = economicCase(true); c.b.creditCarry.lots[0].eligible = '2000'; c.b.creditCarry.lots[0].selected = '1300'; c.b.creditCarry.currentPec = '0';
const before = structuredClone(c.b.creditCarry); assert.match(pending(c).pending.join(' '), /ultrapassam a colecta/); assert.deepEqual(c.b.creditCarry, before);
c.b.creditCarry.lots[0].selected = '1288'; const m = calc(c); assert.deepEqual(m.pending, []); assert.equal(number(m.recoverable), 500); assert.equal(m.economicCredit.carryCreated, false);
});
test('zero,absence and missing differ; earlier deliberate economic summary remains frozen and does not block detailed calculation', () => {
const c = economicCase(); c.b.internationalCredits.economic = {state: 'known', amount: '999', reference: 'SYNTHETIC previous deliberate summary'};
assert.equal(number(calc(c).payable), 688); assert.equal(c.b.internationalCredits.economic.amount, '999');
c.b.economicCredits.state = 'absent'; pending(c); c.b.economicCredits.rows = []; assert.equal(number(calc(c).payable), 700);
c.b.economicCredits.state = ''; pending(c);
const z = economicCase(); Object.assign(row(z), {taxAmount: '0', proofAmount: '0', grossUpIncluded: '0', income: '0', nationalLimit: '0'}); assert.equal(number(calc(z).payable), 700);
});
test('exact60% arithmetic and fractional quota never invent fiscal rounding', () => {
const c = economicCase(); Object.assign(row(c), {taxAmount: '0.01', proofAmount: '0.01', grossUpIncluded: '0.006'});
const m = calc(c); assert.deepEqual(m.economicCredit.deducted, {numerator: '3', denominator: '500'}); assert.deepEqual(m.payable, {numerator: '349997', denominator: '500'});
row(c).nationalLimit = '1/1000'; assert.deepEqual(calc(c).economicCredit.deducted, {numerator: '1', denominator: '1000'});
});
test('save/recover/history,HTML and dossier preserve current L6,original snapshots and accepted carry results', () => {
const c = economicCase(true), prior = structuredClone(c.b); delete prior.economicCredits;
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, prior, '2026-10-28T12:00:00+02:00'));
const old = annualBook(c.d), frozen = structuredClone(old.result), base = JSON.parse(c.d.modelo_a_snapshot), plan = c.d.business_plan_snapshot;
const next = prepareEconomicBook(old); next.economicCredits = c.b.economicCredits;
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, next, '2026-10-28T14:00:00+02:00'));
const back = annualBook(JSON.parse(JSON.stringify(c.d))); assert.equal(annualCurrent(c.d, back), true); assert.deepEqual(back.history, [frozen]); assert.equal(number(back.result.model.payable), 538);
const after = JSON.parse(c.d.modelo_a_snapshot); delete after.irpcAnnual; delete base.irpcAnnual; assert.deepEqual(after, base); assert.equal(c.d.business_plan_snapshot, plan);
assert.match(annualHtml(c.d), /Crédito económico — antes do internacional/); assert.match(annualHtml(c.d), /538.00/);
assert.match(JSON.stringify(createProjectDossierReport(c.d, {selectedSections: new Set(['finance'])})), /Crédito económico/);
back.economicCredits.rows[0].nationalLimit = '5'; c.d.modelo_a_snapshot = saveAnnualBook(c.d, back); assert.equal(annualCurrent(c.d, back), false); assert.deepEqual(back.result, annualBook(c.d).result); assert.throws(() => annualHtml(c.d), /fontes actuais/); assert.match(annualReportRows(c.d).flat().join(' '), /Histórico/);
});
test('malformed optional evidence and parcels are rejected without resetting snapshots', () => {
for (const mutate of [c => c.b.economicCredits.version = 2, c => row(c).previousUse = null, c => row(c).article40 = null, c => foreign(c).parcel = 1]) {
const c = economicCase(), before = c.d.modelo_a_snapshot; mutate(c); assert.throws(() => saveAnnualBook(c.d, c.b), /recuperável/); assert.equal(c.d.modelo_a_snapshot, before);
}
const c = internationalCase(); const once = prepareEconomicBook(c.b); assert.equal(prepareEconomicBook(once), once);
});
test('a partially identified document parcel remains recoverable and indeterminate rather than acquiring absent fields', () => {
const c = economicCase(); foreign(c).parcel = 'foreign-tax-line';
const model = pending(c); c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b));
const back = annualBook(JSON.parse(JSON.stringify(c.d))); assert.deepEqual(calculateAnnual(c.d, back), back.result.model); assert.equal(back.result.model.status, model.status);
});
+24
View File
@@ -0,0 +1,24 @@
import fs from 'node:fs';
import {economicCase} from '../tests/frontend/helpers/irpc-economic-case.mjs';
import {recordAnnual, saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
const cases = {};
for (const name of ['payable', 'combined', 'quota', 'prior-use', 'duplicate', 'missing-gross-up', 'article40', 'shared-paper', 'partial-parcel', 'fractional', 'recoverable']) {
const c = economicCase(name === 'combined');
const previous = structuredClone(c.b); delete previous.economicCredits;
c.b.history = [recordAnnual(c.d, previous, '2026-10-28T12:00:00+02:00').result];
const r = c.b.economicCredits.rows[0], p = c.b.internationalCredits.positions[0], f = p.payments[0];
if (name === 'quota') r.nationalLimit = '5';
if (name === 'prior-use') r.previousUse.amount = '3';
if (name === 'duplicate' || name === 'shared-paper') {
Object.assign(f, {issuer: r.taxIssuer, document: r.taxDocument, proofAmount: '20', parcel: name === 'duplicate' ? r.taxParcel : 'foreign-tax-line', parcelReference: 'SYNTHETIC QA documented tax line in shared paper, no actual tax evidence'});
}
if (name === 'missing-gross-up') r.grossUpIncluded = '';
if (name === 'article40') r.article40 = 'yes';
if (name === 'partial-parcel') f.parcel = 'foreign-tax-line';
if (name === 'fractional') Object.assign(r, {proofAmount: '0.01', taxAmount: '0.01', grossUpIncluded: '0.006'});
if (name === 'recoverable') {f.proofAmount = '2000'; f.amount = '2000'; p.nationalLimit = '2000'; p.incomeAmount = '6250';}
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b, '2026-10-28T14:00:00+02:00'));
cases[name] = c.d;
}
fs.writeFileSync(new URL('../tests/frontend/fixtures/irpc-economic-preserved.json', import.meta.url), JSON.stringify({synthetic: true, actualTaxEvidence: false, cases}));
console.log('Prepared eleven synthetic economic-credit cases with accepted annual/carry history.');
+22
View File
@@ -0,0 +1,22 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {annualBook, annualCurrent, calculateAnnual, annualHtml} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const output = new URL('../outputs/irpc-l8-economic-20261028/', import.meta.url);
const receipts = [];
for (const name of ['payable', 'combined', 'quota', 'prior-use', 'duplicate', 'missing-gross-up', 'article40', 'shared-paper', 'partial-parcel', 'fractional', 'recoverable']) {
const d = JSON.parse(fs.readFileSync(new URL('recovered-' + name + '.json', output), 'utf8'));
const b = annualBook(d), m = b.result.model;
assert.equal(annualCurrent(d, b), true); assert.deepEqual(calculateAnnual(d, b), m);
assert.equal(b.history.length, 1); assert.equal(b.history[0].model.payable.numerator, name === 'combined' ? '550' : '700');
const html = annualHtml(d), dossier = createProjectDossierReport(d, {selectedSections: new Set(['finance']), generatedAt: '2026-10-28T14:00:00+02:00'});
assert.match(html, /Crédito económico — antes do internacional/); assert.match(JSON.stringify(dossier), /Crédito económico/);
if (name === 'combined') {assert.match(html, /538.00/); assert.match(html, /Saldo anterior — PEC — 2023/);}
if (name === 'payable') {assert.match(html, /688.00/); assert.match(html, /1396.00/);}
if (name === 'fractional') assert.deepEqual(m.arithmeticBalance, {numerator: '349997', denominator: '500'});
fs.writeFileSync(new URL('report-' + name + '.html', output), html);
fs.writeFileSync(new URL('dossier-' + name + '.json', output), JSON.stringify(dossier));
receipts.push({name, status: m.status, balance: m.arithmeticBalance, economicDeduction: m.economicCredit.deducted, apiRecovered: true, recalculationEqual: true, html: true, dossier: true, frozenHistoryPreserved: true});
}
fs.writeFileSync(new URL('recovered-report-receipt.json', output), JSON.stringify({synthetic: true, activeChanged: false, receipts}, null, 2));
console.log(JSON.stringify(receipts.map(({name, status, balance}) => ({name, status, balance})), null, 2));