Group compatible IRPC withholding rates and reuse delivered FX selection
This commit is contained in:
@@ -27480,5 +27480,52 @@
|
||||
}
|
||||
],
|
||||
"active_changes": false
|
||||
},
|
||||
"irpc_l5_grouped_QA_20261025": {
|
||||
"status": "QA_grouped_compatible_L5_prepared_not_installed",
|
||||
"receipt": "irpc-l5-grouped-qa-20261025.json",
|
||||
"new_frontend_tests": 19,
|
||||
"API": "1/34",
|
||||
"FX": "Existingselectorandconversionunchanged",
|
||||
"remaining": [
|
||||
{
|
||||
"item": "Dispensas68 e casos residentes/EP dependentes",
|
||||
"data_needed": "Titular/operação e condições de68(a/b/c/d/f/g),incluindo gestão de imóveis próprios/transparência,participações e tipos de juros;casos digitais residentes/EP confrontados com62(5)/68(d).",
|
||||
"resolution": "Ramo específico por dispensa/factos;não bloquear royalties comuns,retenções definitivas ou especiais independentes."
|
||||
},
|
||||
{
|
||||
"item": "Convenções/benefícios invocados",
|
||||
"data_needed": "País/convenção/artigo,beneficiário,residência,condições/período e prova invocados",
|
||||
"resolution": "Só esse caso fica indeterminado;sem taxa reduzida presumida."
|
||||
},
|
||||
{
|
||||
"item": "Outros rendimentos/devedores/períodos",
|
||||
"data_needed": "Artigo próprio para categorias fora dos ramos agrupados,via devedorIRPS,anos fora2026,EP de categorias especiais62(4)quando não cabe62(2)",
|
||||
"resolution": "Não aplicar20%ou10%genéricos;avançar os casos compatíveis."
|
||||
},
|
||||
{
|
||||
"item": "Juros,rectificações e arredondamento de entrega",
|
||||
"data_needed": "Regra/taxa/base/período dejuros,declaração/guia anterior,documentos de correcção e regra de arredondamento legal da entrega se aplicável",
|
||||
"resolution": "Aritmética exacta preservada;aproximação assinalada;sem zero dejuros ou crédito anual automático."
|
||||
},
|
||||
{
|
||||
"item": "EntregaAT com comprovativo em moeda estrangeira",
|
||||
"data_needed": "MontanteMZN efectivamente entregue no comprovativo;conversão do pagamento ao beneficiário não prova entregaIRPC",
|
||||
"resolution": "Base e comprovativo de retenção sofrida já convertem pelo mecanismo entregue;esta limitação não reabre o câmbio geral."
|
||||
}
|
||||
],
|
||||
"external": [
|
||||
{
|
||||
"item": "MRFTA/CER e remissão67(7)",
|
||||
"data_needed": "Correspondência concreta de rubricas/CER para novos casos;67(7)refere62(6),nova taxaestá62(5);manteroriginal",
|
||||
"resolution": "Preparação/exportação disponível;sem adivinhar código,assinatura ou submissão."
|
||||
},
|
||||
{
|
||||
"item": "ActosAT",
|
||||
"data_needed": "Submissão,recepção,aceitação e eventuais comprovativos externos",
|
||||
"resolution": "Externos;exportar não executa nem certifica esses actos."
|
||||
}
|
||||
],
|
||||
"active_changes": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"status": "QA_grouped_compatible_L5_prepared_not_installed",
|
||||
"installed_baseline": "5c0cb25cea7826811c389b55a64bcdf5e43b937f",
|
||||
"previous_QA": "da0d8abb4775a7a68142f84129e312b86dadfe23",
|
||||
"branch": "codex/irpc-l5-withholdings-qa-20261025",
|
||||
"authorization": "Tempo:agrupar restantes enquadramentos compatíveis,reutilizar contrapartes/documentos/pagamentos e câmbio entregue;preparar entrega conjunta;manterportal5c/L3/ISPC;sem instalar este incremento.",
|
||||
"implemented": [
|
||||
"Firstda0d8abb scope preserved;MZN10%digital/nonresident resultscurrentandunchanged without forcedrecalculation.",
|
||||
"Ordinary20% qualifiedroyalties/equipment/services/nonlistedcapital;rentstandard20%onaccount;residentroyalties20%onaccount;onlydependentdispensascaseslimited.",
|
||||
"62(3):20%definitivelisteddebt/Treasuryinterestandbankliquidityswaps with exactnationalheadquarters/effectivemanagementfacts.",
|
||||
"62(4):10%qualifiedtelecom/internationaltransport/connectedinstallation/aircraftmaintenancecharter/publicruralelectricity/maritimefishingcabotage/otherBMlistedsecurities;notgeneral10%.",
|
||||
"Expiredagriculturalmeasures31/12/2025 not carriedto2026;source2022gapresolved.",
|
||||
"ExistingFXselector/selectFx/fxSelectionKey/convertCents reused unchanged through IRPCadapter;originalcurrency/amount,bulletin,column,direction,date andhistory retained.",
|
||||
"Foreignwithholdingproofcapacityconvertedonce;MZNallocations,sharedpaymentlimits andsourcechangeschecked;ATremittance/beneficiarypaymentnotconfused.",
|
||||
"Private draft persistence/recovery/report/preparationexport;annualcredits/ATactsnotautomaticallygenerated."
|
||||
],
|
||||
"new_source": {
|
||||
"url": "https://www.cipmoz.org/wp-content/uploads/2023/financas/17F%29%20Lei%2029_2022%20Plano%20Economico%20Social%20Or%C3%A7amento%20do%20Estado%202023.pdf",
|
||||
"sha256": "1159fd3ce1ba5581671796d083534da48bd3c5578298819e896f1c2a8026318d",
|
||||
"pages": 268,
|
||||
"bytes": 17966640,
|
||||
"path": "docs/investmoz/simulations/source-archive/BR252-I-Serie-2022-Lei20-PESOE.pdf",
|
||||
"citation": "BR252/I,30/12/2022,p2731/PDF1,Lei20/2022 arts1/2",
|
||||
"read_scope": "Full268pages preserved/inventoried;first3pages textread;page1 visuallyreviewed;budgetpages not usedforwithholding",
|
||||
"provenance": "Original officialBR hostedCIP;acceptance ofofficialsource preserved,no second authenticityvalidation",
|
||||
"scan": "Not claimed performed;private source archive,not librarypublication/import"
|
||||
},
|
||||
"source_mapping_reused": [
|
||||
"Lei34/2007,62(1)/67/68,PDF17–19",
|
||||
"Lei19/2013,62(3/4),PDF2/p682(22),visuallyreviewed",
|
||||
"Lei20/2022,62(1–5),PDF1/p2731,visuallyreviewed",
|
||||
"Lei12/2025,62(5)/67(5/7),previouslyacceptedBR248/2025PDF19"
|
||||
],
|
||||
"runtime_delta": [
|
||||
{
|
||||
"path": "resources/js/lib/irpc-withholdings.ts",
|
||||
"sha256_LF": "f10d82f1bf353b1a92c37790b213fad9e769ed39139a99577747ee703be63b49"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-irpc-withholdings.tsx",
|
||||
"sha256_LF": "5cffcc26c72ed736e88386589710cf7929ec9bc25f78910648ba676fb1cf4b08"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/irpc-fx.ts",
|
||||
"sha256_LF": "5c4a7a477429f8d775acf6db3e117008468f5554e5731dec81f60fbb75e948fb"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-form.tsx",
|
||||
"sha256_LF": "01d20f4761933a3e4dd7c776bd4adcf9610ac0435d918a1998ae9f0c3ce3f234"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/project-dossier.ts",
|
||||
"sha256_LF": "df8b224b9472449711d0b69a873fa75f9cc9f4d84dce1a314fc576fae9c6eae2"
|
||||
}
|
||||
],
|
||||
"additional_runtime_since_da0d8abb": [
|
||||
"resources/js/lib/irpc-withholdings.ts",
|
||||
"resources/js/components/simulation-irpc-withholdings.tsx",
|
||||
"resources/js/lib/irpc-fx.ts"
|
||||
],
|
||||
"existing_FX_files_unchanged": {
|
||||
"resources/js/lib/iva-fx-selection.ts": "9aa07ad443bc777b80510a3b5bd6aff9ddb9814d2450f3c690e531e74d71d998",
|
||||
"resources/js/components/simulation-fx-selection.tsx": "39bb6d87d404745b0b66349bc11626545a3672ae27321ce9b4658416592059bf",
|
||||
"resources/js/lib/business-financial-plan.ts": "c20274722d1ffcd0360f041a6cacb5d868fe0621cb27a1aca6e06826612c3ebb",
|
||||
"resources/js/lib/iva-currency-events.ts": "cc9f2045d302d7987310da4006cc250fc5c99a27397918d166d649bfc32fab08"
|
||||
},
|
||||
"previous_resources_unchanged_except_manifest": 438,
|
||||
"old_original_PDFs_preserved": 26,
|
||||
"total_source_original_PDFs": 27,
|
||||
"previous_QA_proofs_reused": {
|
||||
"new_frontend_distinct": 17,
|
||||
"frontend": "frontend.txt:16pass;directed-cross-context.txt:1pass",
|
||||
"API": "1test/23assertions;guardedloopback55441/simulator_fiscal_delta_20261021_test;DatabaseTransactionsrollback;allpreviousL3/ISPCmapsidentical",
|
||||
"types": "types-final.txt:exit0",
|
||||
"build": "build-final.txt:exit0",
|
||||
"UI": "Actualcomponent calculation/save/recover/report andexport;localfileharness distinguishedfromprivateAPI;reuse existingacquisition at373 demonstrated;UI proof reused for unchanged positive route after final shared-payment-key refinement;directed test proves only that delta",
|
||||
"previous_functional_suites_repeated": false
|
||||
},
|
||||
"previous_QA_proof_hashes_reconciled": true,
|
||||
"verification": {
|
||||
"new_frontend_distinct": 19,
|
||||
"frontend_proofs": [
|
||||
"frontend.txt:17pass;listedcapitalcase superseded bydirectedcapital",
|
||||
"directed-capital.txt:updatedcapitalcase+1newcasepass",
|
||||
"directed-report.txt:3affectedreportcasespass",
|
||||
"directed-fx-origin.txt:1newcasepass"
|
||||
],
|
||||
"API": "1test/34assertions;loopback55441/simulator_fiscal_delta_20261021_test;DatabaseTransactionsrollback;acceptedhistory/oldmaps preserved",
|
||||
"types": "types-final.txt:exit0",
|
||||
"build": "build-final.txt:exit0",
|
||||
"UI": "ActualexistingFXpreview/confirmationinsideIRPC,calculation/save/recover/report/export;localfileharness distinguishedfromprivateAPI",
|
||||
"unaffected_old_functional_suites_repeated": false
|
||||
},
|
||||
"example": {
|
||||
"foreign_royalty": "1000USD×64=64000MZN;20%=12800;ATreceipt5000MZN;balance7800MZN;FXdate20Oct vs costevent2Oct",
|
||||
"special_telecom": "100000MZN;qualified62(4)a10%=10000;notdigital62(5)",
|
||||
"resident_royalty": "50000MZN;20%=10000;onaccount,notannualcredit",
|
||||
"foreign_withholding_proof": "100USDbase→6400MZN;tax1280;10USDproof→640MZN;difference640MZN"
|
||||
},
|
||||
"remaining_calculations_and_specific_mapping": [
|
||||
{
|
||||
"item": "Dispensas68 e casos residentes/EP dependentes",
|
||||
"data_needed": "Titular/operação e condições de68(a/b/c/d/f/g),incluindo gestão de imóveis próprios/transparência,participações e tipos de juros;casos digitais residentes/EP confrontados com62(5)/68(d).",
|
||||
"resolution": "Ramo específico por dispensa/factos;não bloquear royalties comuns,retenções definitivas ou especiais independentes."
|
||||
},
|
||||
{
|
||||
"item": "Convenções/benefícios invocados",
|
||||
"data_needed": "País/convenção/artigo,beneficiário,residência,condições/período e prova invocados",
|
||||
"resolution": "Só esse caso fica indeterminado;sem taxa reduzida presumida."
|
||||
},
|
||||
{
|
||||
"item": "Outros rendimentos/devedores/períodos",
|
||||
"data_needed": "Artigo próprio para categorias fora dos ramos agrupados,via devedorIRPS,anos fora2026,EP de categorias especiais62(4)quando não cabe62(2)",
|
||||
"resolution": "Não aplicar20%ou10%genéricos;avançar os casos compatíveis."
|
||||
},
|
||||
{
|
||||
"item": "Juros,rectificações e arredondamento de entrega",
|
||||
"data_needed": "Regra/taxa/base/período dejuros,declaração/guia anterior,documentos de correcção e regra de arredondamento legal da entrega se aplicável",
|
||||
"resolution": "Aritmética exacta preservada;aproximação assinalada;sem zero dejuros ou crédito anual automático."
|
||||
},
|
||||
{
|
||||
"item": "EntregaAT com comprovativo em moeda estrangeira",
|
||||
"data_needed": "MontanteMZN efectivamente entregue no comprovativo;conversão do pagamento ao beneficiário não prova entregaIRPC",
|
||||
"resolution": "Base e comprovativo de retenção sofrida já convertem pelo mecanismo entregue;esta limitação não reabre o câmbio geral."
|
||||
}
|
||||
],
|
||||
"external_formalities": [
|
||||
{
|
||||
"item": "MRFTA/CER e remissão67(7)",
|
||||
"data_needed": "Correspondência concreta de rubricas/CER para novos casos;67(7)refere62(6),nova taxaestá62(5);manteroriginal",
|
||||
"resolution": "Preparação/exportação disponível;sem adivinhar código,assinatura ou submissão."
|
||||
},
|
||||
{
|
||||
"item": "ActosAT",
|
||||
"data_needed": "Submissão,recepção,aceitação e eventuais comprovativos externos",
|
||||
"resolution": "Externos;exportar não executa nem certifica esses actos."
|
||||
}
|
||||
],
|
||||
"currency_gap_previous": "Resolvedforbaseandforeignwithholdingproofbyexistingoperationalmechanism;no newselector ormeanformula. ATremittanceamount remainsactualdocumentedMZN.",
|
||||
"new_organizational_restrictions": false,
|
||||
"restriction_origin": "Tempoexpressedreuse;legalrates/conditionsfromacceptedBR;optionaltypedlinks/fingerprints aretechnicalchoices,notnewapprovers.",
|
||||
"L3_ISPC": "Preserved;ISPCinterest/otherexcess/instructionsinexistingmatrix;IRPSstillonlypreviousdemonstratedcase;P001/P058nonblocking;P084/P085closed;statistics/AIdeferred.",
|
||||
"L5_integrally_completed": false,
|
||||
"IRPC_integrally_completed": false,
|
||||
"IRPS_integrally_completed": false,
|
||||
"schema_backend_config_dependency_changes": false,
|
||||
"active_changes": false,
|
||||
"execution_remaining": [
|
||||
"Authenticatedfreshdestinationcomparison,backupandisolatedrestorebeforeinstallation.",
|
||||
"FastforwarddeliverybyexistingGitprocessafterfreshguards;nevermergerootP001experimentalruntime.",
|
||||
"Verifyinstalledruntime/compiledmarkers/persistentfiles/configuration/schema/permissions/dataunder equivalentidentity;reuseacceptedfunctionalproofs."
|
||||
],
|
||||
"compiled_markers": {
|
||||
"Calcular e guardar retenções IRPC": [
|
||||
"assets/services-Es7_7t9u.js"
|
||||
],
|
||||
"Base do rendimento/conversão por resolver:": [
|
||||
"assets/project-dossier-DZQJzKcc.js"
|
||||
],
|
||||
"Retenção liberatória20%62(3)a/b": [
|
||||
"assets/project-dossier-DZQJzKcc.js"
|
||||
],
|
||||
"Reutilizar selecção cambial": [
|
||||
"assets/services-Es7_7t9u.js"
|
||||
],
|
||||
"Calcular e guardar apuramento trimestral ISPC": [
|
||||
"assets/services-Es7_7t9u.js"
|
||||
],
|
||||
"Calcular e guardar restituição E": [
|
||||
"assets/services-Es7_7t9u.js"
|
||||
]
|
||||
},
|
||||
"active_readonly_revision": {
|
||||
"commit": "5c0cb25cea7826811c389b55a64bcdf5e43b937f",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-25T16:24:27.707113+00:00",
|
||||
"manifest_sha256": "385a289f9ba26a632aa0164fbba3913cc8dc2fb891641881fc8d2dcda1951626"
|
||||
},
|
||||
"proof_hashes": {
|
||||
"outputs/irpc-l5-grouped-20261025/preservation-before-tests.json": "ab8a892a14cd7cac916cbda51c1904eae87f3008d5f2e41d65494abb09e25dd8",
|
||||
"outputs/irpc-l5-grouped-20261025/frontend.txt": "1939704865fc1f963adf4616837b615aeb5dfb7d5fa1567b8dc9d978bd39299a",
|
||||
"outputs/irpc-l5-grouped-20261025/directed-capital.txt": "ba5dbd9d0047decb36e646f6152258552f0a36e34141b60070e0c36e41b4d610",
|
||||
"outputs/irpc-l5-grouped-20261025/directed-report.txt": "2bc8ac7c9e5bfdfe18a59eb3e0eb7e2450c9ae0cb211efb4e07752851c5250a7",
|
||||
"outputs/irpc-l5-grouped-20261025/directed-fx-origin.txt": "121c998e6d95820b8d6dda8fc09dc9d190416d3f64e5fc8dfed6df78dd18e523",
|
||||
"outputs/irpc-l5-grouped-20261025/api.txt": "58365c069bb4c5d7bc91c24834575c19e1458049b73f5b42e9b1e543cd2b4944",
|
||||
"outputs/irpc-l5-grouped-20261025/types-final.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
|
||||
"outputs/irpc-l5-grouped-20261025/build-final.txt": "db910282d69133fbdaee6c5d22b0397d377ed0b652072094be0a165884d97fba",
|
||||
"outputs/irpc-l5-grouped-20261025/ui-fx-preview.txt": "a5ddc73db4cf9a8424ce0a8fcec9da55d8828f2cb13faf9f8715c0bcd6074523",
|
||||
"outputs/irpc-l5-grouped-20261025/ui-report.txt": "0561e14566e4b9b4ab333c5c2a13b03f055c4ae413d3f9d211e663f2da38a7b5",
|
||||
"outputs/irpc-l5-grouped-20261025/preparation.html": "c3c3e8df77e3f22e34e0cfbd2a8cc04a7d3cdc2e7080a0a70415f8f1c661818b",
|
||||
"outputs/irpc-l5-grouped-20261025/active-baseline.json": "2f2d1a9d3e311f12099d2372d0c1a01eb5ff3918a39218c73b668dfbe51c66f8"
|
||||
}
|
||||
}
|
||||
@@ -190,5 +190,11 @@
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-25T16:24:27.707113+00:00",
|
||||
"manifest_sha256": "385a289f9ba26a632aa0164fbba3913cc8dc2fb891641881fc8d2dcda1951626"
|
||||
},
|
||||
"current_balance_followup": {
|
||||
"receipt": "irpc-l5-grouped-qa-20261025.json",
|
||||
"source2022_gap": "ResolvedbynewfullBR,p2731",
|
||||
"currency_gap": "Resolvedthroughdeliveredmechanismforbaseandwithholdingproof",
|
||||
"previous_proofs": "Preserved;originalscopeandhistorynotrevoked"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,3 +133,11 @@ Reutilizados os inventários de8143ficheiros locais e39formulários/189documento
|
||||
- AT-MRFTA-19-39.pdf;SHA256`8ea62c53066b482f87078a7a7b1ecf15ca98a12a122e8d7abca63acfc993186b`;https://www.at.gov.mz/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIRPS%2FIRPS-IRPC%20MRFTA%20-%2019-39.pdf;MRFTA p1–2 visually reviewed;consultation/preparation,not submission.
|
||||
|
||||
Originaisprivadospreservados;sempublicação/activaçãoautomática,semdeclararscanoucertificaçãoefectuados. Lei4/2012,19/2013eRegulamento9/2008foramrecuperados/inventariados;correspondênciaintegralpendente,nãoforamusadosparaactivartaxasantigas. Recibo:irpc-l5-withholdings-qa-20261025.json.
|
||||
|
||||
## 25/10/2026 — L5: Lei 20/2022 recuperada e correspondência das taxas
|
||||
|
||||
O novo exemplar integral do BR 252/I, de 30/12/2022, foi recuperado em arquivo público do CIP. A Lei 20/2022 consta da primeira página, impressa 2731, conferida visualmente. O ficheiro anterior continua identificado como 3.º suplemento turístico; não foi substituído nem reinterpretado. As 268 páginas do novo original ficam inventariadas e preservadas; o restante PESOE não é usado para activar retenções.
|
||||
|
||||
Origem: https://www.cipmoz.org/wp-content/uploads/2023/financas/17F%29%20Lei%2029_2022%20Plano%20Economico%20Social%20Or%C3%A7amento%20do%20Estado%202023.pdf
|
||||
|
||||
SHA256: `1159fd3ce1ba5581671796d083534da48bd3c5578298819e896f1c2a8026318d`. Exemplar: `docs/investmoz/simulations/source-archive/BR252-I-Serie-2022-Lei20-PESOE.pdf`. Lei19/2013 PDF2/p682(22) também conferida visualmente:62(3)liberatória20% e62(4)a–d10% específicos. Lei20/2022 mantém62(1/3/4) e termina medidas agrícolas em31/12/2025;Lei12/2025 estabelece62(5)digital/moeda electrónica. A fonte oficial permanece aceite; a nova leitura resolve uma lacuna de texto, sem revalidação genérica. Sem publicação/importação automática ou scan declarado. Recibo: `irpc-l5-grouped-qa-20261025.json`.
|
||||
|
||||
Binary file not shown.
@@ -5848,3 +5848,25 @@ O MRFTA 19/39 da AT foi conferido nas duas páginas, incluindo as instruções.
|
||||
Provas do delta: 17 testes novos distintos; API privada em QA, 1 teste e 23 verificações com rollback; tipos e compilação; componente real com cálculo, gravação, recuperação, relatório, exportação e reutilização de uma aquisição anterior. A demonstração visual local em ficheiro é distinguida da prova da API privada. O controlo adicional de pagamentos partilhados foi conferido por caso dirigido, sem repetir as provas funcionais anteriores.
|
||||
|
||||
L3 e ISPC permanecem preservados, assim como os 21 originais anteriores e os 131 fechos de L1. Os residuais ISPC ficam na mesma matriz; o IRPS conserva apenas o caso anteriormente demonstrado. P001/P058 não bloqueiam e P084/P085 permanecem fechados. Estatísticas e IA continuam adiadas. L5, IRPC e IRPS não estão integralmente concluídos. Não houve instalação, migração, importação ou alteração activa. Recibo: `irpc-l5-withholdings-qa-20261025.json`.
|
||||
|
||||
## 25/10/2026 — inventário anterior ao agrupamento de L5
|
||||
|
||||
Recuperadas as provas aceites de `da0d8abb`, L3, ISPC instalado `5c0cb25c` e 131 fechos de L1. Sem perdas confirmadas. O selector cambial, a conversão e os históricos entregues foram identificados por hash antes do novo delta e serão reutilizados. O novo exemplar integral do BR 252/2022 contém a Lei 20/2022 na primeira página; as 268 páginas ficam preservadas e inventariadas, sem activar outras regras do PESOE. Dispensas, convenções, juros e MRFTA permanecem discriminados. Nenhum teste novo ou alteração activa neste inventário.
|
||||
|
||||
## 25/10/2026 — L5: agrupamento de taxas e reutilização do câmbio entregue
|
||||
|
||||
O conjunto preparado inclui `da0d8abb` e os novos enquadramentos compatíveis, sem instalar no portal. A Lei 20/2022 foi recuperada no exemplar completo do BR 252/I, página 2731, resolvendo a falta de texto que impedia o cotejo anterior. O original antigo, com o 3.º suplemento turístico, permanece identificado e preservado. Não se repetem pesquisas ou validações genéricas sem nova evidência.
|
||||
|
||||
Foram acrescentados os ramos qualificados de 20% para royalties, equipamentos, serviços e capitais não cotados de não residentes; royalties de residentes e rendas comuns conservam a natureza por conta quando aplicável. Os ramos dependentes das dispensas ficam separados. Os juros de bilhetes do Tesouro/dívida cotada e permutas de liquidez qualificadas de entidades com sede e direcção efectiva nacionais têm retenção liberatória de 20% pelo artigo 62(3), sem crédito anual automático.
|
||||
|
||||
As taxas especiais de 10% do artigo 62(4) ficam limitadas às condições demonstradas: telecomunicações, transportes internacionais, equipamentos conexos, manutenção/fretamento de aeronaves, infraestruturas eléctricas em projectos públicos de electrificação rural, afretamento marítimo de pesca/cabotagem e outros títulos cotados na Bolsa de Valores de Moçambique, com a exclusão da dívida/bilhetes abrangidos pelo número 3(a). Não se confundem com a taxa digital do número 5. Os benefícios agrícolas com termo em 31/12/2025 não foram transportados para 2026.
|
||||
|
||||
O câmbio reutiliza, sem alteração, `SimulationFxSelection`, `selectFx`, `fxSelectionKey` e `convertCents`. O adaptador de retenções liga a selecção existente à base ou ao comprovativo de retenção sofrida. Conserva montante/moeda originais, boletim, coluna, sentido e data; não usa taxa actual nem calcula uma média própria. A data cambial fica distinta do evento do artigo 67(5). Uma confirmação sem alteração conserva o vínculo à origem; uma edição deliberada passa a selecção para o controlo dessa linha, mantendo o histórico anterior. Uma origem alterada torna visível a divergência apenas nos resultados dependentes.
|
||||
|
||||
Exemplo de royalty: 1.000 USD, com taxa sintética 64 MZN/USD, produzem base de 64.000 MZN e retenção de 12.800 MZN a 20%; comprovativo AT de 5.000 MZN deixa 7.800 MZN por reconciliar. No exemplo de retenção sofrida, a base de 100 USD converte para 6.400 MZN, o imposto é de 1.280 MZN e o comprovativo de 10 USD corresponde a 640 MZN, com diferença de 640 MZN. A conversão não transforma o pagamento ao beneficiário em entrega à AT; esta conserva o montante MZN efectivamente documentado. Pagamentos e parcelas continuam ligados e não podem ser usados além do montante disponível, incluindo entre contextos.
|
||||
|
||||
Passaram 19 testes novos distintos, com casos dirigidos para natureza dos capitais, relatório e vínculo cambial; os 17 testes anteriores de L5 não foram repetidos. A API privada em QA passou 1 teste e 34 verificações com rollback e guardas contra a base activa. Tipos, compilação, conferência do selector existente dentro da retenção, cálculo, gravação, recuperação, relatório e exportação foram demonstrados. A página visual sintética grava apenas um ficheiro local; a prova da API privada é separada. Os resultados aceites de `da0d8abb` permanecem actuais quando não afectados e foram preservados no histórico do conjunto.
|
||||
|
||||
Permanecem separados: dispensas e casos residentes/EP que delas dependem, convenções invocadas, categorias/devedores/períodos fora deste recorte, juros, rectificações e correspondências/CER do MRFTA. Cada pendência identifica os dados e a condição de resolução no recibo `irpc-l5-grouped-qa-20261025.json`; não bloqueia casos independentes. Exportar a preparação não submete nem obtém aceitação AT. L5, IRPC e IRPS não estão integralmente concluídos.
|
||||
|
||||
O portal mantém `5c0cb25c`, com L3 e ISPC preservados. O saldo ISPC continua na mesma matriz e o IRPS conserva o caso anteriormente demonstrado. P001/P058 não bloqueiam; P084/P085 permanecem fechados; estatísticas e IA continuam adiadas. Foram conservados os 26 originais anteriores e acrescentado um novo original. A instalação futura exige a comparação autenticada do destino, backup/restauro fresco e conferência do delta pelo procedimento Git existente, sem repetir provas aceites.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
|
||||
import {SimulationFxSelection} from '@/components/simulation-fx-selection';
|
||||
import {irpcFxSources,irpcFxRow,updateIrpcFx,type IrpcFxAmount} from '@/lib/irpc-fx';
|
||||
import {useState} from 'react';
|
||||
import {irpcBook,saveIrpcBook,reuseIrpcSources,irpcSources,newIrpcRow,recordIrpc,irpcKinds,irpcReportRows,irpcPreparationHtml,type IrpcBook,type IrpcRow,type IrpcEvent,type IrpcPayment} from '@/lib/irpc-withholdings';
|
||||
import {irpcBook,saveIrpcBook,reuseIrpcSources,irpcSources,newIrpcRow,recordIrpc,irpcKinds,emptyIrpcOrdinaryFacts,irpcReportRows,irpcPreparationHtml,type IrpcBook,type IrpcRow,type IrpcEvent,type IrpcPayment,type IrpcOrdinaryFacts} from '@/lib/irpc-withholdings';
|
||||
export function SimulationIrpcWithholdings({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');if(draft.fiscal_component!=='irpc'&&draft.fiscal_component!=='withholding')return null;
|
||||
try{const book=irpcBook(draft),sources=irpcSources(draft),save=(b:IrpcBook)=>{try{onChange('modelo_a_snapshot',saveIrpcBook(draft,b));setError('');}catch(e){setError(String(e));}},change=(id:string,p:Partial<IrpcRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...p}:r)}),uid=()=>crypto.randomUUID();
|
||||
@@ -7,16 +9,21 @@ export function SimulationIrpcWithholdings({draft,onChange}:{draft:Record<string
|
||||
const select=(r:IrpcRow,k:keyof IrpcRow,label:string,options:Array<[string,string]>)=><label className="block">{label}<select className="block rounded border p-2" value={String(r[k]??'')} onChange={e=>change(r.id,{[k]:e.target.value})}>{options.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>;
|
||||
const yn:Array<[string,string]>=[['','Por esclarecer'],['yes','Sim'],['no','Não']];
|
||||
const eventLabels:Array<[IrpcEvent['kind'],string]>=[['cost','Reconhecimento do custo'],['payment','Pagamento do rendimento'],['maturity','Vencimento, ainda que presumido'],['availability','Colocação à disposição'],['liquidation','Liquidação'],['quantification','Apuramento do quantitativo']];
|
||||
const fxSources=irpcFxSources(draft);
|
||||
const renderFx=(id:string,input:IrpcFxAmount,apply:(patch:Partial<IrpcFxAmount>)=>void)=><div className="space-y-2"><p>A base/comprovativo conserva a moeda e o montante originais. A conversão reutiliza o selector e a aritmética entregues; a data cambial fica distinta do evento da retenção.</p>{fxSources.map(src=><div key={src.id}><span>{src.label};data:{src.selection.applied?.date||'por confirmar'};selecção actual:{input.fxSelection?.applied?.rate||'nenhuma'}.</span><button type="button" className="rounded border p-2" onClick={()=>{if(window.confirm(`Substituir a selecção cambial desta linha (${input.fxSelection?.applied?.rate||'vazia'}) pela origem ${src.label},data ${src.selection.applied?.date||'por confirmar'}? O montante e a moeda originais são conservados;resultado anterior permanece no histórico até novo cálculo.`))apply({fxSelection:structuredClone(src.selection),fxLink:{id:src.id,fingerprint:src.fingerprint}});}}>Reutilizar selecção cambial {src.id}</button></div>)}<SimulationFxSelection row={irpcFxRow(id,input)} onChange={patch=>{if(patch.fxSelection)apply(updateIrpcFx(input,patch.fxSelection));}}/></div>;
|
||||
const ordinarySelect=(r:IrpcRow,k:keyof IrpcOrdinaryFacts,label:string,options:Array<[string,string]>)=><label className="block">{label}<select value={String((r.ordinary??emptyIrpcOrdinaryFacts())[k]??'')} onChange={e=>change(r.id,{ordinary:{...(r.ordinary??emptyIrpcOrdinaryFacts()),[k]:e.target.value}})}>{options.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>;
|
||||
const paymentChange=(id:string,p:Partial<IrpcPayment>)=>save({...book,payments:book.payments.map(v=>v.id===id?{...v,...p}:v)});
|
||||
return<section aria-label="Retenções de IRPC" className="space-y-4 rounded border p-4"><h4>IRPC — retenções e preparação documental</h4><p>Reutilize documentos e contrapartes existentes. Classifique o rendimento e o evento aplicável:facturar,pagar e reconhecer o custo são factos distintos. Este incremento calcula10% nos casos digitais/moeda electrónica não residentes,não imputáveis aEP,documentados abaixo. Outros enquadramentos ficam visíveis e indeterminados,sem bloquear estes casos.</p>{error&&<p role="alert">{error}</p>}
|
||||
return<section aria-label="Retenções de IRPC" className="space-y-4 rounded border p-4"><h4>IRPC — retenções e preparação documental</h4><p>Reutilize documentos e contrapartes existentes. Classifique o rendimento e o evento aplicável:facturar,pagar e reconhecer o custo são factos distintos. O conjunto calcula os casos digitais de 10%, as taxas normais de 20% e as taxas especiais do artigo 62(4) nos âmbitos identificados. As dispensas e convenções ficam separadas e só condicionam os rendimentos que delas dependem.</p>{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" className="rounded border p-2" onClick={()=>{try{save(reuseIrpcSources(draft,book));}catch(e){setError(String(e));}}}>Reutilizar contrapartes e documentos existentes</button><button type="button" className="rounded border p-2" onClick={()=>save({...book,rows:[...book.rows,newIrpcRow(uid())]})}>Acrescentar rendimento IRPC</button>
|
||||
{book.rows.map(r=><fieldset key={r.id} className="space-y-3 rounded border p-3"><legend>{r.description||r.documentRef||'Rendimento por identificar'}</legend>{r.source&&<p>Origem:{r.source.id}. Entradas deliberadamente editadas são conservadas. {sources.find(s=>s.id===r.source!.id)?.key!==r.source.key&&<><strong>Origem alterada ou indisponível.</strong>{sources.find(s=>s.id===r.source!.id)&&<><span> Base actual:{r.base};base na origem:{sources.find(s=>s.id===r.source!.id)?.amount||'desconhecida'};documento actual:{r.documentRef};origem:{sources.find(s=>s.id===r.source!.id)?.documentRef||'desconhecido'}.</span><button type="button" className="rounded border p-2" onClick={()=>{const s=sources.find(s=>s.id===r.source!.id)!;if(window.confirm('Confirma a reconciliação da origem indicada, conservando as entradas deliberadas e os resultados anteriores no histórico?'))change(r.id,{source:{...r.source!,key:s.key}});}}>Conservar edições e reconciliar origem</button></>}</>}</p>}
|
||||
<div className="grid gap-3 md:grid-cols-2">{select(r,'direction','Perspectiva',[['paid','Rendimento pago — retenção a entregar'],['received','Rendimento recebido — retenção sofrida']])}{field(r,'counterparty','Contraparte')}{field(r,'documentRef','Documento / operação / linha distinta')}{field(r,'description','Descrição')}{select(r,'origin','Origem',[['projected','Projecção com pressupostos'],['effective','Factos documentados']])}{select(r,'kind','Natureza do rendimento',irpcKinds)}{field(r,'base','Base do rendimento, separada do IVA')}{field(r,'currency','Moeda da base')}{select(r,'recipientIrpc','Titular sujeito a IRPC neste rendimento',yn)}{select(r,'payerIrpc','Devedor sujeito a IRPC',yn)}{select(r,'residence','Residência do titular',[['','Por esclarecer'],['resident','Residente'],['nonresident','Não residente']])}{r.residence==='nonresident'&&<>{select(r,'permanentEstablishment','Existe estabelecimento estável em Moçambique',yn)}{r.permanentEstablishment==='yes'&&select(r,'attributable','Rendimento imputável ao estabelecimento',yn)}</>}{select(r,'mozambique','Rendimento obtido em Moçambique, conforme qualificação legal',yn)}{field(r,'qualificationRef','Suporte / pressuposto da qualificação, residência e incidência')}{select(r,'specialRelief','Convenção, isenção ou dispensa aplicável',[['','Por esclarecer'],['none','Não invocada neste caso qualificado'],['claimed','Invocada — correspondência específica necessária']])}{r.specialRelief==='claimed'&&field(r,'reliefRef','Artigo, condições, período e prova invocados')}</div>
|
||||
{r.currency!=='MZN'&&renderFx(r.id,r,patch=>change(r.id,patch))}
|
||||
{r.kind&&!['digital_goods','digital_services','emoney_commission','other'].includes(r.kind)&&<fieldset className="space-y-2 rounded border p-2"><legend>Factos específicos do rendimento</legend>{ordinarySelect(r,'nonDigital','Rendimento fora do âmbito digital/moeda electrónica do artigo62(5)',yn)}{r.kind==='capital'&&ordinarySelect(r,'capitalType','Categoria dos capitais',[['','Por esclarecer'],['unlisted_interest','Juros não cotados, fora das permutas de liquidez'],['unlisted_dividend','Dividendos não cotados'],['listed_debt_mz','Juros de bilhetes do Tesouro/títulos de dívida cotados em bolsa —62(3)a'],['listed_other_mz','Outros títulos cotados na Bolsa de Valores de Moçambique —62(4)d'],['liquidity_repo','Juros de permutas de liquidez em bancos,com ou sem garantia —62(3)b'],['other','Outra categoria']])}{r.kind==='capital'&&r.residence==='resident'&&['listed_debt_mz','liquidity_repo'].includes(r.ordinary?.capitalType??'')&&ordinarySelect(r,'capitalSeatAndManagement','Titular com sede e direcção efectiva nacionais —62(3)',yn)}{r.kind==='rent'&&ordinarySelect(r,'rentOwnPropertyManagement','Titular é sociedade com objecto de gestão de imóveis próprios',yn)}{(['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage'].includes(r.kind)||r.kind==='capital'&&r.ordinary?.capitalType==='listed_other_mz')&&ordinarySelect(r,'specialFacts','Factos exactos da categoria62(4) identificados no suporte — não apenas actividade semelhante',yn)}<label className="block">Suporte ou pressuposto específico da classificação<input value={r.ordinary?.reference??''} onChange={e=>change(r.id,{ordinary:{...(r.ordinary??emptyIrpcOrdinaryFacts()),reference:e.target.value}})}/></label></fieldset>}
|
||||
<h5>Eventos aplicáveis — artigo67(5), versão2026</h5>{r.events.map((e,n)=><div key={n} className="grid gap-2 md:grid-cols-3"><label>Evento<select value={e.kind} onChange={ev=>change(r.id,{events:r.events.map((v,j)=>j===n?{...v,kind:ev.target.value as IrpcEvent['kind']}:v)})}>{eventLabels.map(([k,l])=><option key={k} value={k}>{l}</option>)}</select></label><label>Data<input type="date" value={e.date} onChange={ev=>change(r.id,{events:r.events.map((v,j)=>j===n?{...v,date:ev.target.value}:v)})}/></label><label>Suporte / pressuposto<input value={e.reference} onChange={ev=>change(r.id,{events:r.events.map((v,j)=>j===n?{...v,reference:ev.target.value}:v)})}/></label></div>)}<button type="button" onClick={()=>change(r.id,{events:[...r.events,{kind:'cost',date:'',reference:''}]})}>Acrescentar evento aplicável</button>{select(r,'eventsComplete','Todos os eventos aplicáveis identificados, incluindo custo quando aplicável',yn)}
|
||||
{select(r,'settlement','Reconciliação da entrega / prova de retenção sofrida',[['','Por esclarecer'],['none','Ainda sem entrega / prova'],['complete','Pagamentos / provas associados completos']])}{field(r,'settlementRef','Referência da situação declarada')}
|
||||
{r.allocations.map((a,n)=><div key={n}><label>Pagamento / prova existente<select value={a.paymentId} onChange={e=>change(r.id,{allocations:r.allocations.map((v,j)=>j===n?{...v,paymentId:e.target.value}:v)})}><option value="">Seleccione</option>{book.payments.map(p=><option key={p.id} value={p.id}>{p.reference} — {p.amount} {p.currency} — {p.kind}</option>)}</select></label><label>Parcela usada<input value={a.amount} onChange={e=>change(r.id,{allocations:r.allocations.map((v,j)=>j===n?{...v,amount:e.target.value}:v)})}/></label></div>)}{r.settlement==='complete'&&<button type="button" onClick={()=>change(r.id,{allocations:[...r.allocations,{paymentId:'',amount:''}]})}>Associar pagamento/prova existente</button>}
|
||||
{r.allocations.map((a,n)=><div key={n}><label>Pagamento / prova existente<select value={a.paymentId} onChange={e=>change(r.id,{allocations:r.allocations.map((v,j)=>j===n?{...v,paymentId:e.target.value}:v)})}><option value="">Seleccione</option>{book.payments.map(p=><option key={p.id} value={p.id}>{p.reference} — {p.amount} {p.currency} — {p.kind}</option>)}</select></label><label>Parcela usada em MZN<input value={a.amount} onChange={e=>change(r.id,{allocations:r.allocations.map((v,j)=>j===n?{...v,amount:e.target.value}:v)})}/></label></div>)}{r.settlement==='complete'&&<button type="button" onClick={()=>change(r.id,{allocations:[...r.allocations,{paymentId:'',amount:''}]})}>Associar pagamento/prova existente</button>}
|
||||
</fieldset>)}
|
||||
<h5>Pagamentos e comprovativos reutilizáveis neste livro</h5><p>Uma referência é registada uma vez e repartida entre rendimentos sem exceder o montante. Pagamento ao fornecedor não é entrega à AT; pagamentos de IVA conservam o seu vínculo anterior e não são automaticamente usados em IRPC.</p>{book.payments.map(p=><fieldset key={p.id} className="grid gap-2 rounded border p-2 md:grid-cols-3"><label>Natureza<select value={p.kind} onChange={e=>paymentChange(p.id,{kind:e.target.value as IrpcPayment['kind']})}><option value="remittance">Entrega IRPC à AT</option><option value="withholding_proof">Comprovativo de retenção sofrida</option><option value="beneficiary">Pagamento ao beneficiário</option></select></label>{(['reference','date','amount','currency']as const).map(k=><label key={k}>{({reference:'Referência',date:'Data',amount:'Montante',currency:'Moeda'})[k]}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>paymentChange(p.id,{[k]:e.target.value})}/></label>)}</fieldset>)}<button type="button" onClick={()=>save({...book,payments:[...book.payments,{id:uid(),kind:'remittance',reference:'',date:'',amount:'',currency:'MZN'}]})}>Registar pagamento/prova para reutilização</button>
|
||||
<h5>Pagamentos e comprovativos reutilizáveis neste livro</h5><p>Uma referência é registada uma vez e repartida entre rendimentos sem exceder o montante. Pagamento ao fornecedor não é entrega à AT; pagamentos de IVA conservam o seu vínculo anterior e não são automaticamente usados em IRPC.</p>{book.payments.map(p=><fieldset key={p.id} className="grid gap-2 rounded border p-2 md:grid-cols-3"><label>Natureza<select value={p.kind} onChange={e=>paymentChange(p.id,{kind:e.target.value as IrpcPayment['kind']})}><option value="remittance">Entrega IRPC à AT</option><option value="withholding_proof">Comprovativo de retenção sofrida</option><option value="beneficiary">Pagamento ao beneficiário</option></select></label>{(['reference','date','amount','currency']as const).map(k=><label key={k}>{({reference:'Referência',date:'Data',amount:'Montante',currency:'Moeda'})[k]}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>paymentChange(p.id,{[k]:e.target.value})}/></label>)}{p.currency!=='MZN'&&(p.kind==='withholding_proof'?renderFx(p.id,p,patch=>paymentChange(p.id,patch)):<p>A entrega IRPC à AT requer o montante MZN do comprovativo; converter o pagamento ao beneficiário não prova entrega do imposto.</p>)}</fieldset>)}<button type="button" onClick={()=>save({...book,payments:[...book.payments,{id:uid(),kind:'remittance',reference:'',date:'',amount:'',currency:'MZN'}]})}>Registar pagamento/prova para reutilização</button>
|
||||
<div className="space-x-3"><button type="button" className="rounded border p-2" onClick={()=>{try{save(recordIrpc(draft,book));}catch(e){setError(String(e));}}}>Calcular e guardar retenções IRPC</button><button type="button" className="rounded border p-2" onClick={()=>{try{const html=irpcPreparationHtml(draft),u=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),a=document.createElement('a');a.href=u;a.download='IRPC-L5-preparacao.html';a.click();URL.revokeObjectURL(u);setError('');}catch(e){setError(String(e));}}}>Exportar preparação IRPC</button></div>
|
||||
{irpcReportRows(draft).map((r,n)=><p key={n}><strong>{r[1]}</strong>: {r[2]}<small className="block">{r[3]}</small></p>)}<p>Formalidades externas:MRFTA19/39 e guia M/39 preservados. CER/rubricas digitais e remissão67(7)→62(6) por corresponder. Exportar não submete nem obtém aceitação AT. L5 continua aberto;ISPC e IRPS conservam os seus limites anteriores.</p></section>;
|
||||
}catch(e){return<p role="alert">{String(e)}</p>;}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {selectFx,fxSelectionKey,type FxSelection} from './iva-fx-selection.ts';
|
||||
import {currencyEventBook,newCurrencyEvent} from './iva-currency-events.ts';
|
||||
import {convertCents} from './business-financial-plan.ts';
|
||||
import {decimalFraction,fraction,type FiscalFraction} from './fiscal-rational.ts';
|
||||
import {closureFingerprint} from './iva-closure.ts';
|
||||
|
||||
export type IrpcFxLink={id:string;fingerprint:string};
|
||||
export type IrpcFxAmount={currency:string;fxSelection?:FxSelection;fxLink?:IrpcFxLink};
|
||||
export type IrpcFxEvidence={originalAmount:string;originalCurrency:string;convertedCurrency:'MZN';column:string;rate:string;date:string;direction:string;source:string;selectionKey:string;rounding:string};
|
||||
export function irpcFxSources(d:Record<string,unknown>){return currencyEventBook(d).rows.filter(r=>r.fxSelection).map(r=>({id:r.id,label:`${r.description||r.id} — ${r.sourceCurrency} — ${r.fxSelection?.applied?.rate??'por aplicar'}`,fingerprint:closureFingerprint(r),selection:structuredClone(r.fxSelection!)}));}
|
||||
export function irpcFxDependency(d:Record<string,unknown>,input:IrpcFxAmount):string{
|
||||
if(input.currency==='MZN'||!input.fxLink)return'';
|
||||
const source=irpcFxSources(d).find(s=>s.id===input.fxLink!.id);
|
||||
return !source?'Selecção cambial de origem indisponível;vínculo e histórico conservados.':source.fingerprint!==input.fxLink.fingerprint?'Selecção cambial de origem alterada;compare a nova selecção antes de a aplicar às retenções.':'';
|
||||
}
|
||||
export function irpcFxRow(id:string,input:IrpcFxAmount){const s=input.fxSelection,a=s?.applied;return{...newCurrencyEvent(id,'MZN'),sourceCurrency:input.currency,fxSelection:s,fxDirection:s?.direction??'',fxRate:a?.rate??'',fxDate:a?.date??'',fxSource:a?.source??''};}
|
||||
export function convertIrpcAmount(d:Record<string,unknown>,amount:string,input:IrpcFxAmount):{value:FiscalFraction;evidence:IrpcFxEvidence|null}{
|
||||
const original=decimalFraction(amount);if(input.currency==='MZN')return{value:original,evidence:null};
|
||||
const issue=irpcFxDependency(d,input);if(issue)throw Error(issue);
|
||||
const s=input.fxSelection;if(!s)throw Error('Reutilize ou aplique a selecção cambial já entregue:moeda original,boletim,coluna,data e sentido;sem taxa actual por defeito.');
|
||||
const selected=selectFx(s),a=s.applied;
|
||||
if(!a||a.selectionKey!==fxSelectionKey(s)||a.rate!==selected.rate||a.date!==selected.date||a.source!==selected.source||a.column!==selected.column||s.sourceCurrency!==input.currency)throw Error('Selecção cambial alterada,não confirmada ou de outra moeda;use a conferência e confirmação existentes,sem recalcular históricos.');
|
||||
const [whole,places='']=selected.rate.split('.'),rate=BigInt(whole+places.padEnd(8,'0')),scale=100000000n;
|
||||
const micro=BigInt(original.numerator)*1000000n/BigInt(original.denominator);
|
||||
const converted=s.direction==='mzn_per_unit'?convertCents(micro,rate,scale):convertCents(micro,scale,rate);
|
||||
return{value:fraction(converted,1000000n),evidence:{originalAmount:amount,originalCurrency:input.currency,convertedCurrency:'MZN',column:selected.column,rate:selected.rate,date:selected.date,direction:s.direction,source:selected.source,selectionKey:a.selectionKey,rounding:'Mesmo convertCents entregue;conversão à unidade de0,000001MZN;apresentação separada,não regra nova de arredondamento do imposto.'}};
|
||||
}
|
||||
|
||||
export function irpcFxEvidenceLabel(e:IrpcFxEvidence):string{const column=({buy:'Compra',sell:'Venda',mean:'Média de referência BM',exception:'Excepção fiscal identificada'} as Record<string,string>)[e.column]??e.column;const direction=e.direction==='mzn_per_unit'?'MZN por '+e.originalCurrency:e.originalCurrency+' por MZN';return`Original: ${e.originalAmount} ${e.originalCurrency}; coluna: ${column}; taxa: ${e.rate}; cotação: ${direction}; data cambial: ${e.date}; fonte: ${e.source.split(';perspectiva:')[0]}. Montante e selecção originais conservados.`;}
|
||||
|
||||
export function updateIrpcFx(input:IrpcFxAmount,selection:FxSelection):Partial<IrpcFxAmount>{return{fxSelection:selection,fxLink:input.fxSelection&&fxSelectionKey(input.fxSelection)===fxSelectionKey(selection)?input.fxLink:undefined};}
|
||||
@@ -1,24 +1,30 @@
|
||||
import {convertIrpcAmount,irpcFxDependency,irpcFxEvidenceLabel,type IrpcFxAmount,type IrpcFxEvidence} from './irpc-fx.ts';
|
||||
import {acquisitionBook,acquisitionScope} from './iva-acquisitions.ts';
|
||||
import {salesBook} from './iva-operations.ts';
|
||||
import {closureFingerprint} from './iva-closure.ts';
|
||||
import {addFractions,decimalFraction,fraction,fractionPresentation,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
|
||||
|
||||
export const irpcWithholdingVersion='irpc-L5/1';
|
||||
export const irpcWithholdingSource='Lei12/2025,BR248/2025,PDF19/p2180(19):arts5(5/7),20(2)c,62(5)a/b e67(5/7);CIRPC Lei34/2007 arts2,67(3),68,PDF18–19/p768(72–73). Art67(7)remete62(6),enquanto taxa nova consta62(5):divergência formal preservada,sem renumerar o original. MRFTA19/39AT,p1–2. Código2002 revogado não usado. Outros regimes/taxas dependem da correspondência específica,sem zero presumido.';
|
||||
export const irpcWithholdingSource='Lei12/2025,BR248/2025,PDF19/p2180(19):arts5(5/7),20(2)c,62(5)a/b e67(5/7);CIRPC Lei34/2007 arts2,62/67/68,PDF17–19/p768(71–73);Lei19/2013 PDF2/p682(22),62(3/4);Lei20/2022 BR252 p2731/PDF1,62(1–5). Art67(7)remete62(6),enquanto taxa nova consta62(5):divergência formal preservada,sem renumerar o original. MRFTA19/39AT,p1–2. Código2002 revogado não usado. Outros regimes/taxas dependem da correspondência específica,sem zero presumido.';
|
||||
type Choice=''|'yes'|'no';
|
||||
export type IrpcKind=''|'digital_goods'|'digital_services'|'emoney_commission'|'royalty'|'equipment'|'rent'|'capital'|'services'|'other';
|
||||
export const irpcKinds:Array<[IrpcKind,string]>=[['','Por identificar'],['digital_goods','Transmissão de bens digitais'],['digital_services','Prestação de serviços digitais'],['emoney_commission','Comissão de agente de moeda electrónica'],['royalty','Propriedade intelectual/industrial'],['equipment','Uso de equipamento'],['rent','Rendimento predial'],['capital','Aplicação de capitais'],['services','Outras prestações/intermediação'],['other','Outro enquadramento']];
|
||||
export type IrpcKind=''|'digital_goods'|'digital_services'|'emoney_commission'|'royalty'|'equipment'|'rent'|'capital'|'services'|'telecom'|'international_transport'|'connected_installation'|'aircraft_maintenance_charter'|'rural_public_electrification'|'maritime_fishing_cabotage'|'other';
|
||||
export const irpcKinds:Array<[IrpcKind,string]>=[['','Por identificar'],['digital_goods','Transmissão de bens digitais'],['digital_services','Prestação de serviços digitais'],['emoney_commission','Comissão de agente de moeda electrónica'],['royalty','Propriedade intelectual/industrial'],['equipment','Uso de equipamento'],['rent','Rendimento predial'],['capital','Aplicação de capitais'],['services','Outras prestações/intermediação'],['telecom','Serviços de telecomunicações — artigo62(4)a'],['international_transport','Transportes internacionais — artigo62(4)a'],['connected_installation','Montagem/instalação de equipamentos conexos a telecomunicações/transportes —62(4)a'],['aircraft_maintenance_charter','Manutenção e fretamento de aeronaves —62(4)a'],['rural_public_electrification','Construção/reabilitação de infraestruturas de produção,transporte e distribuição de energia eléctrica,nas zonas rurais,em projectos públicos de electrificação rural —62(4)b'],['maritime_fishing_cabotage','Afretamento de embarcações marítimas para pesca/cabotagem —62(4)c'],['other','Outro enquadramento']];
|
||||
export type IrpcEvent={kind:'cost'|'payment'|'maturity'|'availability'|'liquidation'|'quantification';date:string;reference:string};
|
||||
export type IrpcPayment={id:string;kind:'beneficiary'|'remittance'|'withholding_proof';reference:string;date:string;amount:string;currency:string};
|
||||
export type IrpcRow={id:string;direction:'paid'|'received';counterparty:string;documentRef:string;description:string;base:string;currency:string;origin:'projected'|'effective';kind:IrpcKind;recipientIrpc:Choice;payerIrpc:Choice;residence:''|'resident'|'nonresident';permanentEstablishment:Choice;attributable:Choice;mozambique:Choice;qualificationRef:string;specialRelief:''|'none'|'claimed';reliefRef:string;eventsComplete:Choice;events:IrpcEvent[];settlement:''|'none'|'complete';settlementRef:string;allocations:Array<{paymentId:string;amount:string}>;source?:{id:string;key:string;fingerprint:string};};
|
||||
export type IrpcResult={id:string;status:'calculated'|'indeterminate'|'outside_scope';reason:string;period:string;eventDate:string;nature:'definitive'|'on_account'|null;rate:number|null;base:FiscalFraction|null;tax:FiscalFraction|null;settled:FiscalFraction|null;outstanding:FiscalFraction|null;direction:IrpcRow['direction']};
|
||||
export type IrpcBook={version:1;rows:IrpcRow[];payments:IrpcPayment[];result?:{version:string;key:string;calculatedAt:string;inputs:IrpcRow[];payments:IrpcPayment[];rows:IrpcResult[]};history?:Array<NonNullable<IrpcBook['result']>>};
|
||||
export type IrpcPayment=IrpcFxAmount&{id:string;kind:'beneficiary'|'remittance'|'withholding_proof';reference:string;date:string;amount:string;currency:string};
|
||||
export type IrpcOrdinaryFacts={nonDigital:Choice;capitalType:''|'unlisted_interest'|'unlisted_dividend'|'listed_debt_mz'|'listed_other_mz'|'liquidity_repo'|'other';rentOwnPropertyManagement:Choice;specialFacts:Choice;capitalSeatAndManagement?:Choice;reference:string};
|
||||
export const emptyIrpcOrdinaryFacts=():IrpcOrdinaryFacts=>({nonDigital:'',capitalType:'',rentOwnPropertyManagement:'',specialFacts:'',capitalSeatAndManagement:'',reference:''});
|
||||
export type IrpcRow=IrpcFxAmount&{ordinary?:IrpcOrdinaryFacts;id:string;direction:'paid'|'received';counterparty:string;documentRef:string;description:string;base:string;currency:string;origin:'projected'|'effective';kind:IrpcKind;recipientIrpc:Choice;payerIrpc:Choice;residence:''|'resident'|'nonresident';permanentEstablishment:Choice;attributable:Choice;mozambique:Choice;qualificationRef:string;specialRelief:''|'none'|'claimed';reliefRef:string;eventsComplete:Choice;events:IrpcEvent[];settlement:''|'none'|'complete';settlementRef:string;allocations:Array<{paymentId:string;amount:string}>;source?:{id:string;key:string;fingerprint:string};};
|
||||
export type IrpcResult={id:string;status:'calculated'|'indeterminate'|'outside_scope';reason:string;period:string;eventDate:string;nature:'definitive'|'on_account'|null;rate:number|null;base:FiscalFraction|null;tax:FiscalFraction|null;settled:FiscalFraction|null;outstanding:FiscalFraction|null;direction:IrpcRow['direction'];conversion?:IrpcFxEvidence|null};
|
||||
export type IrpcBook={version:1;rows:IrpcRow[];payments:IrpcPayment[];result?:{version:string;key:string;calculatedAt:string;inputs:IrpcRow[];payments:IrpcPayment[];rows:IrpcResult[];paymentConversions?:Record<string,IrpcFxEvidence|null>};history?:Array<NonNullable<IrpcBook['result']>>};
|
||||
export const newIrpcRow=(id:string):IrpcRow=>({id,direction:'paid',counterparty:'',documentRef:'',description:'',base:'',currency:'MZN',origin:'projected',kind:'',recipientIrpc:'',payerIrpc:'',residence:'',permanentEstablishment:'',attributable:'',mozambique:'',qualificationRef:'',specialRelief:'',reliefRef:'',eventsComplete:'',events:[],settlement:'',settlementRef:'',allocations:[]});
|
||||
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 preservados.');return s;}
|
||||
function map(d:Record<string,unknown>):Record<string,IrpcBook>{const m=store(d).irpcWithholdings;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa IRPC não recuperável;originais preservados.');return m;}
|
||||
function validate(b:IrpcBook){
|
||||
if(!b||b.version!==1||!Array.isArray(b.rows)||!Array.isArray(b.payments)||b.history!==undefined&&!Array.isArray(b.history))throw Error('Livro IRPC não recuperável.');
|
||||
for(const r of b.rows){const template=newIrpcRow('');if(!r||Object.keys(template).filter(k=>typeof template[k as keyof IrpcRow]==='string').some(k=>typeof r[k as keyof IrpcRow]!=='string')||!['paid','received'].includes(r.direction)||!['projected','effective'].includes(r.origin)||!irpcKinds.some(([k])=>k===r.kind)||!['','resident','nonresident'].includes(r.residence)||!['','none','claimed'].includes(r.specialRelief)||!['','none','complete'].includes(r.settlement)||['recipientIrpc','payerIrpc','permanentEstablishment','attributable','mozambique','eventsComplete'].some(k=>!['','yes','no'].includes(r[k as keyof IrpcRow]as string))||!Array.isArray(r.events)||!Array.isArray(r.allocations)||r.events.some(e=>!e||!['cost','payment','maturity','availability','liquidation','quantification'].includes(e.kind)||typeof e.date!=='string'||typeof e.reference!=='string')||r.allocations.some(a=>!a||typeof a.paymentId!=='string'||typeof a.amount!=='string')||r.source!==undefined&&(!r.source||!['id','key','fingerprint'].every(k=>typeof r.source![k as keyof NonNullable<IrpcRow['source']>]==='string')))throw Error('Entradas IRPC não recuperáveis;sem apagar ou repor vazios.');}
|
||||
for(const r of b.rows)if(r.ordinary&&(['nonDigital','rentOwnPropertyManagement','specialFacts'].some(k=>!['','yes','no'].includes(r.ordinary![k as 'nonDigital'|'rentOwnPropertyManagement'|'specialFacts']))||typeof r.ordinary.reference!=='string'||!['','unlisted_interest','unlisted_dividend','listed_debt_mz','listed_other_mz','liquidity_repo','other'].includes(r.ordinary.capitalType)))throw Error('Factos específicos IRPC não recuperáveis.');
|
||||
for(const r of b.rows)if(r.ordinary?.capitalSeatAndManagement!==undefined&&!['','yes','no'].includes(r.ordinary.capitalSeatAndManagement))throw Error('Factos de sede/direcção não recuperáveis.');
|
||||
for(const v of [...b.rows,...b.payments])if(v.fxSelection!==undefined&&(!v.fxSelection||typeof v.fxSelection!=='object'||Array.isArray(v.fxSelection))||v.fxLink!==undefined&&(!v.fxLink||typeof v.fxLink.id!=='string'||typeof v.fxLink.fingerprint!=='string'))throw Error('Vínculo/selecção cambial não recuperável;original preservado.');
|
||||
for(const p of b.payments)if(!p||!['id','reference','date','amount','currency'].every(k=>typeof p[k as keyof IrpcPayment]==='string')||!['beneficiary','remittance','withholding_proof'].includes(p.kind))throw Error('Pagamento IRPC inválido;originais preservados.');
|
||||
if(b.result&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)||typeof b.result.key!=='string'||b.result.version!==irpcWithholdingVersion))throw Error('Resultado IRPC não recuperável.');
|
||||
if(b.payments.some((p,i)=>p.reference.trim()&&b.payments.some((q,j)=>i!==j&&q.kind===p.kind&&q.reference.trim()===p.reference.trim()&&q.date===p.date&&q.currency===p.currency)))throw Error('Pagamento/prova já registado:reutilize a referência existente em vez de duplicar.');
|
||||
@@ -36,27 +42,52 @@ const date=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(s)&&!Number.isNaN(
|
||||
const positive=(f:FiscalFraction)=>BigInt(f.numerator)>0n;
|
||||
export function evaluateIrpc(d:Record<string,unknown>,r:IrpcRow):IrpcResult{
|
||||
const v:IrpcResult={id:r.id,status:'indeterminate',reason:'Identifique o rendimento e os intervenientes.',period:'',eventDate:'',nature:null,rate:null,base:null,tax:null,settled:null,outstanding:null,direction:r.direction};
|
||||
const issue=sourceIssue(d,r);if(issue)return{...v,reason:issue};
|
||||
const issue=sourceIssue(d,r)||irpcFxDependency(d,r);if(issue)return{...v,reason:issue};
|
||||
if(r.recipientIrpc==='no')return{...v,status:'outside_scope',reason:'Titular fora de IRPC;sem converter em IRPS ou em isenção.'};
|
||||
if(!r.counterparty.trim()||!r.documentRef.trim()||r.recipientIrpc!=='yes'||r.payerIrpc!=='yes'||!r.residence||!r.kind||!r.qualificationRef.trim())return{...v,reason:'Faltam contraparte,documento,qualificação do rendimento,titular IRPC,devedor IRPC ou residência. A via devedor IRPS exige âmbito próprio.'};
|
||||
if(r.currency!=='MZN')return{...v,reason:'Base em moeda diferente de MZN: falta conversão documentada com data,taxa e fonte;não relabelar nem usar taxa actual.'};
|
||||
if(r.mozambique!=='yes')return{...v,reason:'Falta correspondência territorial aos arts5/67;ser estrangeiro não resolve incidência nem isenção.'};
|
||||
if(r.specialRelief!=='none')return{...v,reason:'Convenção,isenção ou dispensa invocada/desconhecida:identificar artigo,período,condições e prova;sem taxa reduzida ou zero automáticos.'};
|
||||
if(!r.eventsComplete||r.eventsComplete!=='yes'||!r.events.length||r.events.some(e=>!date(e.date)||!e.reference.trim()))return{...v,reason:'Identifique os eventos aplicáveis67(5),incluindo reconhecimento do custo,e confirme a sua completude com suporte;uma factura ou pagamento isolado não fixa o momento.'};
|
||||
v.eventDate=r.events.map(e=>e.date).sort()[0];v.period=v.eventDate.slice(0,7);
|
||||
if(!v.eventDate.startsWith('2026-'))return{...v,reason:'Primeiro evento aplicável fora de2026;corresponder versão/período próprio,sem deslocar para o pagamento posterior.'};
|
||||
try{v.base=decimalFraction(r.base);}catch{return{...v,reason:'Base do rendimento necessária,em decimal MZN separado do IVA;desconhecido não é zero.'};}
|
||||
if(!['digital_goods','digital_services','emoney_commission'].includes(r.kind))return{...v,reason:'Rendimento preservado paraL5:confrontar arts62/67/68 e alterações20/2009,4/2012,19/2013,20/2022. O exemplar2002 não suporta uma taxa2026;normal/especial não presumidos.'};
|
||||
if(r.residence==='nonresident'&&r.permanentEstablishment==='no')v.nature='definitive';
|
||||
else if(r.residence==='nonresident'&&r.permanentEstablishment==='yes'&&r.attributable==='no')v.nature='definitive';
|
||||
else return{...v,reason:'Para residente ou rendimento imputável aEP,corresponder concretamente a taxa62(5) com as dispensas68,em especial68(d),antes da liquidação. EP desconhecido permanece indeterminado;90dias/serviço digital não criam ausência deEP automática.'};
|
||||
v.rate=10;v.tax=fraction(BigInt(v.base.numerator),BigInt(v.base.denominator)*10n);v.status='calculated';v.reason='Taxa10%62(5) sobre o rendimento identificado;retenção definitiva67(3) no recorte não residente/não imputável aEP. Qualificação e eventos documentados ou pressupostos expressos;procedimento67(7)/remissão62(6) e CER permanecem separados. Não é IVA nem crédito anual automático.';return v;
|
||||
try{const converted=convertIrpcAmount(d,r.base,r);v.base=converted.value;v.conversion=converted.evidence;}catch(e){return{...v,reason:'Base do rendimento/conversão por resolver: '+String(e)};}
|
||||
const digital=['digital_goods','digital_services','emoney_commission'].includes(r.kind);
|
||||
if(digital){
|
||||
if(r.residence==='nonresident'&&r.permanentEstablishment==='no'||r.residence==='nonresident'&&r.permanentEstablishment==='yes'&&r.attributable==='no')v.nature='definitive';
|
||||
else return{...v,reason:'Para residente ou rendimento imputável aEP,corresponder concretamente a taxa62(5) com as dispensas68,em especial68(d),antes da liquidação. EP desconhecido permanece indeterminado;90dias/serviço digital não criam ausência deEP automática.'};
|
||||
v.rate=10;v.reason='Taxa10%62(5) no âmbito digital/moeda electrónica demonstrado;retenção definitiva67(3). Procedimento67(7)/remissão62(6) e CER permanecem separados.';
|
||||
}else{
|
||||
const f=r.ordinary;
|
||||
if(!f||f.nonDigital!=='yes'||!f.reference.trim())return{...v,reason:'Classifique o rendimento não abrangido pelo ramo digital/moeda electrónica e identifique os factos específicos. Cotejo62/67:Lei34/2007,Lei19/2013 eLei20/2022 agora disponível;sem taxa genérica.'};
|
||||
const nonresident=r.residence==='nonresident';
|
||||
if(nonresident&&r.permanentEstablishment!=='no'&&!(r.permanentEstablishment==='yes'&&['yes','no'].includes(r.attributable)))return{...v,reason:'Falta identificar EP e imputação;ausência física ou nacionalidade não resolvem este facto.'};
|
||||
const attributable=!nonresident||r.permanentEstablishment==='yes'&&r.attributable==='yes';
|
||||
v.nature=r.kind==='rent'||attributable?'on_account':'definitive';
|
||||
if(['equipment','services'].includes(r.kind)&&v.nature==='on_account')return{...v,reason:'Rendimento67(1)b/g com imposto por conta:dispensa68(d) identificada,por corresponder no ramo de dispensas;não reter20% nem declarar isenção do rendimento.'};
|
||||
if(r.kind==='rent'&&f.rentOwnPropertyManagement!=='no')return{...v,reason:'Rendimento predial mantém natureza por conta67(3). Identifique a gestão de imóveis próprios e eventual dispensa68(f)/transparência;apenas este caso depende dessa correspondência.'};
|
||||
if(r.kind==='capital'){
|
||||
if(!f.capitalType||f.capitalType==='other')return{...v,reason:'Identifique juros/dividendos não cotados,títulos cotadosBM ou permutas de liquidez;62(3/4) não permite taxa única para capitais.'};
|
||||
if(r.residence==='resident'&&['listed_debt_mz','liquidity_repo'].includes(f.capitalType)){if(f.capitalSeatAndManagement!=='yes')return{...v,reason:'Art62(3):identifique sede e direcção efectiva nacionais do titular,além da categoria exacta dos títulos/permuta;residência genérica não substitui estes factos.'};v.nature='definitive';}
|
||||
if(v.nature==='on_account'&&f.capitalType!=='listed_debt_mz')return{...v,reason:'Capitais com imposto por conta:correspondência específica das dispensas68(a/b/c/g),incluindo titular e operação,permanece separada;sem bloquear capitais definitivos independentes.'};
|
||||
if(f.capitalType==='liquidity_repo'&&r.residence!=='resident')return{...v,reason:'Permutas de liquidez62(3)b exigem entidade com sede/direcção efectiva nacional;caso não residente mantém correspondência própria.'};
|
||||
}
|
||||
const special=['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage'].includes(r.kind)||r.kind==='capital'&&f.capitalType==='listed_other_mz';
|
||||
if(special){
|
||||
if(!nonresident||r.permanentEstablishment!=='no')return{...v,reason:'Taxa especial62(4) refere entidades62(2),não residentes semEP. O caso residente/EP tem correspondência própria;sem copiar10%.'};
|
||||
if(f.specialFacts!=='yes')return{...v,reason:'Confirme os factos exactos62(4)a–d:conexão dos equipamentos,projecto público rural,pesca/cabotagem ou título cotadoBM que não é dívida/bilhete62(3)a,conforme categoria. Sem taxa especial por descrição genérica.'};
|
||||
v.rate=10;v.reason='Taxa especial10%62(4),redacçãoLei19/2013,conservada emLei20/2022 eLei12/2025;entidade não residente semEP e categoria específica identificadas. Não é a taxa digital62(5).';
|
||||
}else{
|
||||
if(!['royalty','equipment','rent','capital','services'].includes(r.kind))return{...v,reason:'Natureza fora dos ramos agrupados;identificar artigo e condições próprios,sem taxa por defeito.'};
|
||||
v.rate=20;v.reason=(r.kind==='capital'&&r.residence==='resident'&&['listed_debt_mz','liquidity_repo'].includes(f.capitalType)?'Retenção liberatória20%62(3)a/b:juros de dívida/bilhetes cotados ou permuta de liquidez qualificados,com sede e direcção efectiva nacionais. ':'')+'Taxa20%62(1),com62(2/3) quando aplicável;incidência67(1),natureza67(3). Lei20/2022,p2731,confrontada:as medidas agrícolas terminam31/12/2025 e não são transportadas para2026. Sem aplicar10% digital a rendimentos não digitais.';
|
||||
}
|
||||
}
|
||||
v.tax=fraction(BigInt(v.base.numerator)*BigInt(v.rate!),BigInt(v.base.denominator)*100n);v.status='calculated';v.reason+=' Base separada do IVA;entrega/prova e liquidação anual mantêm reconciliação própria.';return v;
|
||||
}
|
||||
function sharedPayments(d:Record<string,unknown>,b:IrpcBook){const used=b.payments.filter(p=>b.rows.some(r=>r.allocations.some(a=>a.paymentId===p.id)));return Object.entries(map(d)).filter(([ctx])=>ctx!==acquisitionScope(d)).flatMap(([ctx,other])=>{validate(other);const payments=other.payments.filter(q=>used.some(p=>p.reference.trim()===q.reference.trim()&&p.date===q.date&&p.currency===q.currency&&p.kind===q.kind));return payments.length?[[ctx,payments,other.rows.flatMap(r=>r.allocations.filter(a=>payments.some(p=>p.id===a.paymentId)))]]:[];});}
|
||||
function key(d:Record<string,unknown>,b:IrpcBook){return JSON.stringify([irpcWithholdingVersion,acquisitionScope(d),b.rows,b.payments,b.rows.map(r=>sourceIssue(d,r)),sharedPayments(d,b)]);}
|
||||
function key(d:Record<string,unknown>,b:IrpcBook){const fx=[...b.rows,...b.payments].filter(r=>r.currency!=='MZN'&&r.fxLink).map(r=>irpcFxDependency(d,r));return JSON.stringify([irpcWithholdingVersion,acquisitionScope(d),b.rows,b.payments,b.rows.map(r=>sourceIssue(d,r)),sharedPayments(d,b),...(fx.length?[fx]:[])]);}
|
||||
export function irpcCurrent(d:Record<string,unknown>,b:IrpcBook){return !!b.result&&b.result.key===key(d,b);}
|
||||
export function recordIrpc(d:Record<string,unknown>,b:IrpcBook,now=new Date().toISOString()):IrpcBook{
|
||||
validate(b);const rows=b.rows.map(r=>evaluateIrpc(d,r));const allocations=new Map<string,FiscalFraction[]>();
|
||||
validate(b);const rows=b.rows.map(r=>evaluateIrpc(d,r));const paymentConversions:Record<string,IrpcFxEvidence|null>={};const allocations=new Map<string,FiscalFraction[]>();
|
||||
const fail=(i:number,reason:string)=>{rows[i]={...rows[i],status:'indeterminate',reason,settled:null,outstanding:null};};
|
||||
b.rows.forEach((r,i)=>{
|
||||
if(rows[i].status!=='calculated')return;
|
||||
@@ -64,17 +95,17 @@ export function recordIrpc(d:Record<string,unknown>,b:IrpcBook,now=new Date().to
|
||||
if(!r.settlement||!r.settlementRef.trim()){rows[i].reason+=' Entrega à AT/retenção sofrida ainda não reconciliada;não presumir zero pago.';return;}
|
||||
const parts:FiscalFraction[]=[];const seen=new Set<string>();
|
||||
for(const a of r.allocations){const p=b.payments.find(p=>p.id===a.paymentId);try{
|
||||
if(!p||seen.has(p.id)||p.currency!=='MZN'||!date(p.date)||p.date<rows[i].eventDate||!p.reference.trim()||p.kind!==(r.direction==='paid'?'remittance':'withholding_proof'))throw Error();
|
||||
seen.add(p.id);const amount=decimalFraction(a.amount);decimalFraction(p.amount);parts.push(amount);allocations.set(p.id,[...(allocations.get(p.id)??[]),amount]);
|
||||
}catch{fail(i,'Pagamento/prova ausente,incoerente,repetido ou de outra natureza:pagamento ao fornecedor e imposto IVA não são entrega IRPC.');return;}}
|
||||
if(!p||seen.has(p.id)||p.kind==='remittance'&&p.currency!=='MZN'||!date(p.date)||p.date<rows[i].eventDate||!p.reference.trim()||p.kind!==(r.direction==='paid'?'remittance':'withholding_proof'))throw Error();
|
||||
seen.add(p.id);const amount=decimalFraction(a.amount);paymentConversions[p.id]=convertIrpcAmount(d,p.amount,p).evidence;parts.push(amount);allocations.set(p.id,[...(allocations.get(p.id)??[]),amount]);
|
||||
}catch(e){fail(i,'Pagamento/prova ausente,incoerente,repetido ou de outra natureza:pagamento ao fornecedor e imposto IVA não são entrega IRPC. '+String(e));return;}}
|
||||
const settled=addFractions(parts);if(positive(subtractFractions(settled,rows[i].tax!))){fail(i,'Valor associado excede a retenção calculada;reconcilie excesso sem gerar crédito automático.');return;}
|
||||
rows[i].settled=settled;rows[i].outstanding=subtractFractions(rows[i].tax!,settled);
|
||||
});
|
||||
for(const [id,parts]of allocations){const p=b.payments.find(p=>p.id===id)!;let excess=false;try{const elsewhere:FiscalFraction[]=[];for(const [ctx,other]of Object.entries(map(d))){if(ctx===acquisitionScope(d))continue;validate(other);for(const q of other.payments.filter(q=>q.reference.trim()===p.reference.trim()&&q.date===p.date&&q.kind===p.kind&&q.currency===p.currency)){if(q.amount!==p.amount)throw Error('Montantes divergentes');for(const r of other.rows)for(const a of r.allocations.filter(a=>a.paymentId===q.id))elsewhere.push(decimalFraction(a.amount));}}excess=positive(subtractFractions(addFractions([...parts,...elsewhere]),decimalFraction(p.amount)));}catch{excess=true;}if(excess)b.rows.forEach((r,i)=>{if(r.allocations.some(a=>a.paymentId===id))fail(i,'Mesmo pagamento utilizado além do montante disponível;reparta sem duplicação.');});}
|
||||
for(const [id,parts]of allocations){const p=b.payments.find(p=>p.id===id)!;let excess=false;try{const elsewhere:FiscalFraction[]=[];for(const [ctx,other]of Object.entries(map(d))){if(ctx===acquisitionScope(d))continue;validate(other);for(const q of other.payments.filter(q=>q.reference.trim()===p.reference.trim()&&q.date===p.date&&q.kind===p.kind&&q.currency===p.currency)){if(q.amount!==p.amount||q.currency!=='MZN'&&JSON.stringify(q.fxSelection)!==JSON.stringify(p.fxSelection))throw Error('Montantes/conversões divergentes');for(const r of other.rows)for(const a of r.allocations.filter(a=>a.paymentId===q.id))elsewhere.push(decimalFraction(a.amount));}}excess=positive(subtractFractions(addFractions([...parts,...elsewhere]),convertIrpcAmount(d,p.amount,p).value));}catch{excess=true;}if(excess)b.rows.forEach((r,i)=>{if(r.allocations.some(a=>a.paymentId===id))fail(i,'Mesmo pagamento utilizado além do montante disponível;reparta sem duplicação.');});}
|
||||
// An identical source document/income cannot become a second tax just by adding a row.
|
||||
b.rows.forEach((r,i)=>{if(b.rows.some((s,j)=>j!==i&&(r.source&&s.source?.id===r.source.id||r.documentRef.trim()&&s.documentRef.trim()===r.documentRef.trim()&&s.counterparty.trim()===r.counterparty.trim()&&s.direction===r.direction))){fail(i,'Documento/rendimento repetido neste livro;identifique parcelas próprias antes de somar,sem duplicação.');}});
|
||||
return{...b,history:b.result?[...(b.history??[]),structuredClone(b.result)]:b.history,result:{version:irpcWithholdingVersion,key:key(d,b),calculatedAt:now,inputs:structuredClone(b.rows),payments:structuredClone(b.payments),rows}};
|
||||
return{...b,history:b.result?[...(b.history??[]),structuredClone(b.result)]:b.history,result:{version:irpcWithholdingVersion,key:key(d,b),calculatedAt:now,inputs:structuredClone(b.rows),payments:structuredClone(b.payments),rows,paymentConversions}};
|
||||
}
|
||||
const amount=(v:FiscalFraction|null)=>v?fractionPresentation(v).decimal+' MZN':'Indeterminado';
|
||||
export function irpcReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=irpcBook(d),r=b.result;if(!r)return[];return r.rows.map(v=>{const i=r.inputs.find(i=>i.id===v.id)!;return['IRPC — retenções',`${i.documentRef} — ${i.counterparty}`,`${v.status};${i.direction==='paid'?'retida a entregar':'sofrida'};base:${amount(v.base)};taxa:${v.rate??'por corresponder'}%;retenção:${amount(v.tax)};entrega/prova:${amount(v.settled)};saldo:${amount(v.outstanding)};${v.reason}`,`${acquisitionScope(d)};${i.origin};${v.period};evento:${v.eventDate};${v.nature??'natureza por determinar'};${irpcCurrent(d,b)?'Entradas correspondentes':'Histórico;entradas/origem divergentes'};${r.calculatedAt};${irpcWithholdingSource}`];});}catch(e){return[['IRPC — retenções','Livro não recuperável',String(e),'Originais e histórico preservados.']];}}
|
||||
const amount=(v:FiscalFraction|null)=>{if(!v)return'Indeterminado';const p=fractionPresentation(v);return(p.approximate?'≈':'')+p.decimal+' MZN';};
|
||||
export function irpcReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=irpcBook(d),r=b.result;if(!r)return[];return r.rows.map(v=>{const i=r.inputs.find(i=>i.id===v.id)!;return['IRPC — retenções',`${i.documentRef} — ${i.counterparty}`,`${v.status};${i.direction==='paid'?'retida a entregar':'sofrida'};base:${amount(v.base)};taxa:${v.rate??'por corresponder'}%;retenção:${amount(v.tax)};entrega/prova:${amount(v.settled)};saldo:${amount(v.outstanding)};${v.reason}`,`${acquisitionScope(d)};${i.origin};${v.period};evento:${v.eventDate};${v.nature==='on_account'?'Imposto por conta (sem crédito anual automático)':v.nature??'natureza por determinar'};${irpcCurrent(d,b)?'Entradas correspondentes':'Histórico;entradas/origem divergentes'};reconciliação:${i.settlementRef};comprovativos:${i.allocations.map(a=>{const p=r.payments.find(p=>p.id===a.paymentId);return p?`${p.reference}/${p.date}:parcela${a.amount}MZN de${p.amount}${p.currency};${r.paymentConversions?.[p.id]?irpcFxEvidenceLabel(r.paymentConversions[p.id]!):'sem conversão usada'}`:'referência por resolver';}).join(' | ')};${r.calculatedAt};${v.conversion?'Câmbio:'+irpcFxEvidenceLabel(v.conversion)+';':''}${irpcWithholdingSource}`];});}catch(e){return[['IRPC — retenções','Livro não recuperável',String(e),'Originais e histórico preservados.']];}}
|
||||
export function irpcPreparationHtml(d:Record<string,unknown>):string{const b=irpcBook(d);if(!b.result||!irpcCurrent(d,b))throw Error('Calcule e guarde as entradas actuais antes de preparar.');const esc=(v:unknown)=>String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));return'<!doctype html><html lang="pt"><meta charset="utf-8"><title>IRPC — preparação das retenções</title><body><h1>Preparação IRPC — L5</h1><p>Mapa de trabalho;sem submissão,recepção ou aceitação AT. MRFTA19/39:quadro1 retenção;quadro2 mês/ano único por declaração;quadro7 base/taxa/imposto. CER e correspondência das rubricas digitais/moeda electrónica por esclarecer;67(7)remete62(6),mas taxa nova em62(5). Não preencher assinatura ou uso dos serviços. Não consolidar períodos diferentes nem transformar retenções sofridas em imposto a entregar.</p>'+irpcReportRows(d).map(r=>'<section><h2>'+esc(r[1])+'</h2><p>'+esc(r[2])+'</p><small>'+esc(r[3])+'</small></section>').join('')+'<p>Instruções do exemplar:prazo nominal dia20 do mês seguinte à dedução,sem ajuste de calendário/prorrogação ou procedimento2026 presumidos. Juros não calculados. GuiaM/39 anterior preservada,sem actualizar remissões do original.</p></body></html>';}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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 IrpcGroupedDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
public function test_grouped_irpc_rates_and_existing_fx_recover_with_originals_and_accepted_history():void
|
||||
{
|
||||
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
|
||||
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
|
||||
$d=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-grouped-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
|
||||
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');$this->assertSame($d['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcWithholdings'][$d['context_opportunity']];
|
||||
$foreign=$b['result']['rows'][2];$this->assertSame('calculated',$foreign['status']);$this->assertSame(20,$foreign['rate']);$this->assertSame(['numerator'=>'64000','denominator'=>'1'],$foreign['base']);$this->assertSame(['numerator'=>'12800','denominator'=>'1'],$foreign['tax']);$this->assertSame(['numerator'=>'7800','denominator'=>'1'],$foreign['outstanding']);$this->assertSame('1000',$foreign['conversion']['originalAmount']);$this->assertSame('USD',$foreign['conversion']['originalCurrency']);$this->assertSame('2026-10-20',$foreign['conversion']['date']);$this->assertSame('2026-10-02',$foreign['eventDate']);
|
||||
$this->assertSame(10,$b['result']['rows'][3]['rate']);$this->assertSame('on_account',$b['result']['rows'][4]['nature']);$this->assertSame(['numerator'=>'10000','denominator'=>'1'],$b['result']['rows'][4]['tax']);$this->assertSame(['numerator'=>'640','denominator'=>'1'],$b['result']['rows'][5]['settled']);$this->assertSame(['numerator'=>'640','denominator'=>'1'],$b['result']['rows'][5]['outstanding']);$this->assertSame('10',$b['result']['paymentConversions']['p4']['originalAmount']);$this->assertSame('USD',$b['result']['payments'][3]['currency']);
|
||||
$old=json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-l5-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$prior=json_decode($old['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);foreach($prior as$k=>$v){if(in_array($k,['irpcWithholdings','currencyEvents'],true))continue;$this->assertSame($v,$s[$k],$k);}
|
||||
$oldBook=$prior['irpcWithholdings'][$d['context_opportunity']];$this->assertSame($oldBook['result'],$b['history'][array_key_last($b['history'])]);$this->assertSame($oldBook['rows'],array_slice($b['rows'],0,count($oldBook['rows'])));$this->assertSame($oldBook['payments'],array_slice($b['payments'],0,count($oldBook['payments'])));foreach(($prior['currencyEvents']??[])as$ctx=>$value){if($ctx!==$d['context_opportunity'])$this->assertSame($value,$s['currencyEvents'][$ctx]);}
|
||||
if(is_dir(base_path('outputs/irpc-l5-grouped-20261025')))file_put_contents(base_path('outputs/irpc-l5-grouped-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {irpcBook,newIrpcRow,recordIrpc,saveIrpcBook,emptyIrpcOrdinaryFacts,irpcCurrent,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
|
||||
import {convertIrpcAmount,irpcFxSources,updateIrpcFx} from '../../resources/js/lib/irpc-fx.ts';
|
||||
import {emptyFxSelection,selectFx} from '../../resources/js/lib/iva-fx-selection.ts';
|
||||
import {currencyEventBook,newCurrencyEvent,saveCurrencyEventBook} from '../../resources/js/lib/iva-currency-events.ts';
|
||||
const accepted=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l5-preserved.json',import.meta.url),'utf8'));
|
||||
const draft=()=>structuredClone(accepted);
|
||||
const row=(id='normal')=>({...newIrpcRow(id),counterparty:'SYNTHETIC IRPC counterparty',documentRef:'SYNTHETIC-'+id,base:'100000',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'SYNTHETIC classified non-digital income,IRPCpayer/recipient,Mozambique source;residence andEP explicitly recorded',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'SYNTHETIC applicable cost'},{kind:'payment',date:'2026-11-02',reference:'SYNTHETIC payment'}],settlement:'none',settlementRef:'SYNTHETIC no remittance',ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'SYNTHETIC exact category facts;CIRPC62/67'}});
|
||||
const book=(r=row())=>({version:1,rows:[r],payments:[]});
|
||||
const calc=r=>recordIrpc(draft(),book(r)).result.rows[0];
|
||||
const fx=(patch={})=>{const s={...emptyFxSelection(),nature:'commercial',submissionDate:'2026-10-20',bulletinDate:'2026-10-20',bulletinRef:'SYNTHETIC fictional BM bulletin for QA',source:'SYNTHETIC archived QA exemplar',issuer:'Banco de Moçambique',sourceCurrency:'USD',direction:'mzn_per_unit',mean:'64',...patch};return{...s,applied:selectFx(s)};};
|
||||
test('ordinary nonresident royalties equipment services and nonlisted capitals use20%,not digital10%',()=>{for(const kind of ['royalty','equipment','services','capital']){const r=row();r.kind=kind;if(kind==='capital')r.ordinary.capitalType='unlisted_interest';const v=calc(r);assert.equal(v.status,'calculated');assert.equal(v.rate,20);assert.equal(v.tax.numerator,'20000');assert.equal(v.nature,'definitive');}});
|
||||
test('resident royalties and ordinary rent are on account;predial nonresident is also on account',()=>{for(const patch of [{kind:'royalty',residence:'resident'},{kind:'rent',residence:'resident'},{kind:'rent',residence:'nonresident'}]){const r={...row(),...patch};r.ordinary.rentOwnPropertyManagement='no';const v=calc(r);assert.equal(v.status,'calculated');assert.equal(v.nature,'on_account');assert.equal(v.tax.numerator,'20000');assert.equal('annualCredit'in v,false);}});
|
||||
test('identified dispensa branches and unknown facts block only their own income line',()=>{const d=draft(),rs=[{...row('resident-services'),residence:'resident',kind:'services'},{...row('property'),kind:'rent'},{...row('capital'),residence:'resident',kind:'capital',ordinary:{...row().ordinary,capitalType:'unlisted_interest'}},row('independent')];const out=recordIrpc(d,{version:1,rows:rs,payments:[]}).result.rows;assert.ok(out.slice(0,3).every(v=>v.status==='indeterminate'&&v.tax===null));assert.equal(out[3].tax.numerator,'20000');});
|
||||
test('special categories62(4)a-c retain10% with exact facts,not generic20% or genericinstallation10%',()=>{for(const kind of ['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage']){const r={...row(),kind};r.ordinary.specialFacts='yes';const v=calc(r);assert.equal(v.rate,10);assert.equal(v.tax.numerator,'10000');assert.match(v.reason,/62\(4\)/);}const unknown={...row(),kind:'connected_installation'};assert.equal(calc(unknown).tax,null);const generic={...row(),kind:'services'};assert.equal(calc(generic).rate,20);});
|
||||
test('special62(4) entity condition is distinct from digital noEP/nonattributable branch',()=>{for(const patch of [{residence:'resident'},{permanentEstablishment:'yes',attributable:'no'}]){const r={...row(),kind:'telecom',...patch};r.ordinary.specialFacts='yes';assert.equal(calc(r).tax,null);}});
|
||||
test('listed debt and other BMsecurities remain distinct;liquidity repos are not assumed',()=>{for(const[capitalType,rate]of [['listed_debt_mz',20],['listed_other_mz',10]]){const r={...row(),kind:'capital'};r.ordinary.capitalType=capitalType;r.ordinary.specialFacts='yes';assert.equal(calc(r).rate,rate);}const resident={...row(),kind:'capital',residence:'resident'};resident.ordinary.capitalType='listed_debt_mz';resident.ordinary.capitalSeatAndManagement='yes';assert.equal(calc(resident).rate,20);assert.equal(calc(resident).nature,'definitive');resident.ordinary.capitalType='liquidity_repo';assert.equal(calc(resident).rate,20);assert.equal(calc(resident).nature,'definitive');});
|
||||
test('expired2025 agricultural measures do not extend into2026 foreign services/interest',()=>{for(const kind of ['services','capital']){const r={...row(),kind,description:'SYNTHETIC nationalagriculturalcompany / external agricultural financing'};r.ordinary.capitalType='unlisted_interest';const v=calc(r);assert.equal(v.rate,20);assert.match(v.reason,/31\/12\/2025/);}const r=row();r.events[0].date='2025-12-31';assert.equal(calc(r).tax,null);});
|
||||
test('applied existing BMmean converts base, preserves original and keeps event/date separate',()=>{const r={...row(),currency:'USD',base:'1000',fxSelection:fx()};const v=calc(r);assert.equal(v.base.numerator,'64000');assert.equal(v.tax.numerator,'12800');assert.equal(v.period,'2026-10');assert.equal(v.eventDate,'2026-10-02');assert.equal(v.conversion.date,'2026-10-20');assert.equal(v.conversion.originalAmount,'1000');assert.equal(v.conversion.originalCurrency,'USD');assert.equal(r.base,'1000');assert.equal(r.currency,'USD');});
|
||||
test('purchase/sale perspective,inverse quote and fiscalexception use deliveredselector',()=>{const opposite=fx({nature:'purchase',relation:'opposite',bulletinPerspective:'SYNTHETIC issuer',buy:'63',sell:'65'});let r={...row(),currency:'USD',base:'100',fxSelection:opposite};assert.equal(calc(r).base.numerator,'6500');const inverse=fx({direction:'units_per_mzn',mean:'0.02'});r.fxSelection=inverse;assert.equal(calc(r).base.numerator,'5000');r.fxSelection=fx({mode:'exception',exceptionRef:'SYNTHETIC identifiedfiscalprovision',exceptionScope:'SYNTHETIC retention scope',exceptionDate:'2026-10-02',exceptionRate:'66'});assert.equal(calc(r).base.numerator,'6600');assert.equal(calc(r).conversion.column,'exception');});
|
||||
test('unapplied,stale,missingmean,wrongcurrency or undated FX remains indeterminate',()=>{const samples=[undefined,{...fx(),applied:undefined},{...fx(),mean:'65'},{...fx(),sourceCurrency:'EUR'},{...fx(),bulletinDate:'2026-10-19'}];for(const f of samples){const v=calc({...row(),currency:'USD',base:'1000',fxSelection:f});assert.equal(v.tax,null);assert.match(v.reason,/conversão/);}});
|
||||
test('existing saved FX selections are linked,reused,and changed source marks only linked results stale',()=>{const d=draft(),cb=currencyEventBook(d),s=fx(),f={...newCurrencyEvent('shared-fx','MZN'),fxSelection:s,sourceCurrency:'USD',fxDirection:s.direction,fxRate:s.applied.rate,fxDate:s.applied.date,fxSource:s.applied.source,description:'SYNTHETIC existing FX'};d.modelo_a_snapshot=saveCurrencyEventBook(d,{...cb,rows:[...cb.rows,f]});const source=irpcFxSources(d).find(s=>s.id===f.id),r={...row(),currency:'USD',base:'1000',fxSelection:source.selection,fxLink:{id:source.id,fingerprint:source.fingerprint}},b=recordIrpc(d,book(r));d.modelo_a_snapshot=saveIrpcBook(d,b);assert.equal(irpcCurrent(d,b),true);const current=currencyEventBook(d);d.modelo_a_snapshot=saveCurrencyEventBook(d,{...current,rows:current.rows.map(v=>v.id===f.id?{...v,description:'SYNTHETIC revisedsource'}:v)});assert.equal(irpcCurrent(d,b),false);assert.match(recordIrpc(d,b).result.rows[0].reason,/origem alterada/);const m=recordIrpc(d,book(row('independentMZN')));assert.equal(m.result.rows[0].tax.numerator,'20000');});
|
||||
test('foreign withholding proof converts available amount once;allocations remainMZN',()=>{const r={...row(),direction:'received',settlement:'complete',settlementRef:'SYNTHETIC provedpartialwithholding',allocations:[{paymentId:'p',amount:'640'}]},p={id:'p',kind:'withholding_proof',reference:'SYNTHETIC-USDproof',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()};const d=draft(),b=recordIrpc(d,{...book(r),payments:[p]});assert.equal(b.result.rows[0].settled.numerator,'640');assert.equal(b.result.rows[0].outstanding.numerator,'19360');assert.equal(b.result.paymentConversions.p.originalAmount,'10');d.modelo_a_snapshot=saveIrpcBook(d,b);assert.match(irpcReportRows(d)[0][3],/SYNTHETIC-USDproof/);assert.match(irpcReportRows(d)[0][3],/Original: 10 USD/);});
|
||||
test('foreign payment to beneficiary or purported ATremittance is never converted into taxproof',()=>{for(const kind of ['beneficiary','remittance']){const r={...row(),settlement:'complete',allocations:[{paymentId:'p',amount:'640'}]},p={id:'p',kind,reference:'SYNTHETIC-wrongpayment',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()};assert.equal(recordIrpc(draft(),{...book(r),payments:[p]}).result.rows[0].status,'indeterminate');}});
|
||||
test('converted shared proof cannot exceed availableMZN or change FX between contexts',()=>{const d=draft(),p={id:'p',kind:'withholding_proof',reference:'SYNTHETIC-sharedUSD',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()},r={...row('one'),direction:'received',settlement:'complete',allocations:[{paymentId:'p',amount:'400'}]};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,{...book(r),payments:[p]}));d.context_opportunity='other';const other={...row('two'),direction:'received',settlement:'complete',allocations:[{paymentId:'p',amount:'300'}]};assert.equal(recordIrpc(d,{...book(other),payments:[p]}).result.rows[0].status,'indeterminate');other.allocations[0].amount='200';assert.equal(recordIrpc(d,{...book(other),payments:[p]}).result.rows[0].status,'calculated');assert.equal(recordIrpc(d,{...book(other),payments:[{...p,fxSelection:fx({mean:'65'})}]}).result.rows[0].status,'indeterminate');});
|
||||
test('accepted v1MZN10% results remain current and bytepreserved without recomputation',()=>{const d=draft(),b=irpcBook(d),before=JSON.stringify(b);assert.equal(irpcCurrent(d,b),true);assert.equal(JSON.stringify(irpcBook(d)),before);assert.deepEqual(irpcBook({...d,modelo_a_snapshot:saveIrpcBook(d,b)}),b);});
|
||||
test('new normal/FX result preserves old maps and acceptedresult in history,report frozen after change',()=>{const d=draft(),b=irpcBook(d),old=structuredClone(b.result),s=JSON.parse(d.modelo_a_snapshot);const next=recordIrpc(d,{...b,rows:[...b.rows,{...row('fx'),currency:'USD',base:'1000',fxSelection:fx()}]});d.modelo_a_snapshot=saveIrpcBook(d,next);for(const[k,v]of Object.entries(s))if(k!=='irpcWithholdings')assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v);assert.deepEqual(next.history.at(-1),old);next.rows.at(-1).fxSelection.mean='65';assert.equal(irpcCurrent(d,next),false);d.modelo_a_snapshot=saveIrpcBook(d,next);const report=irpcReportRows(d).at(-1);assert.match(report[2],/12800.00/);assert.match(report[3],/Histórico/);assert.match(report[3],/taxa: 64/);});
|
||||
test('export keeps currencies,normal/special distinctions,payment refs and externalformalities separate',()=>{const d=draft(),r={...row(),currency:'USD',base:'1000',fxSelection:fx()};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const h=irpcPreparationHtml(d);assert.match(h,/Original: 1000 USD/);assert.ok(!h.includes("selectionKey"));assert.match(h,/20%/);assert.match(h,/CER/);assert.match(h,/sem submissão/);assert.match(h,/2026-10/);});
|
||||
|
||||
if(process.env.IRPC_GROUPED_FIXTURE_OUTPUT){
|
||||
const d=draft(),old=irpcBook(d),s=fx(),f={...newCurrencyEvent('retention-fx','MZN'),description:'SYNTHETIC existing BMselection for retention',fxSelection:s,sourceCurrency:'USD',fxDirection:s.direction,fxRate:s.applied.rate,fxDate:s.applied.date,fxSource:s.applied.source};const cb=currencyEventBook(d);d.modelo_a_snapshot=saveCurrencyEventBook(d,{...cb,rows:[...cb.rows,f]});const source=irpcFxSources(d).find(v=>v.id===f.id);
|
||||
const foreign={...row('foreign-royalty'),currency:'USD',base:'1000',fxSelection:source.selection,fxLink:{id:source.id,fingerprint:source.fingerprint},settlement:'complete',settlementRef:'SYNTHETIC IRPC remittance5000',allocations:[{paymentId:'p3',amount:'5000'}]},special={...row('special-telecom'),kind:'telecom',ordinary:{...row().ordinary,specialFacts:'yes'}},resident={...row('resident-royalty'),residence:'resident',base:'50000'},received={...row('received-foreign-royalty'),direction:'received',currency:'USD',base:'100',fxSelection:s,settlement:'complete',settlementRef:'SYNTHETIC proof10USD converted640MZN',allocations:[{paymentId:'p4',amount:'640'}]};
|
||||
const b=recordIrpc(d,{...old,rows:[...old.rows,foreign,special,resident,received],payments:[...old.payments,{id:'p3',kind:'remittance',reference:'SYNTHETIC-IRPC-AT5000',date:'2026-10-20',amount:'5000',currency:'MZN'},{id:'p4',kind:'withholding_proof',reference:'SYNTHETIC-IRPC-USD10',date:'2026-10-20',amount:'10',currency:'USD',fxSelection:s}]});d.modelo_a_snapshot=saveIrpcBook(d,b);fs.writeFileSync(process.env.IRPC_GROUPED_FIXTURE_OUTPUT,JSON.stringify(d,null,2)+'\n');
|
||||
}
|
||||
|
||||
test("62(3) definitive capital needs exact seat/management facts and is not an annual credit",()=>{const r={...row(),residence:"resident",kind:"capital"};r.ordinary.capitalType="listed_debt_mz";assert.equal(calc(r).tax,null);r.ordinary.capitalSeatAndManagement="yes";assert.equal(calc(r).nature,"definitive");assert.equal(calc(r).rate,20);assert.equal("annualCredit"in calc(r),false);});
|
||||
|
||||
test('confirming unchanged FX preserves source link;deliberate input change detaches only that selection',()=>{const input={currency:'USD',fxSelection:fx(),fxLink:{id:'saved',fingerprint:'original'}};const confirmed={...input.fxSelection,applied:selectFx(input.fxSelection)};assert.deepEqual(updateIrpcFx(input,confirmed).fxLink,input.fxLink);assert.equal(updateIrpcFx(input,{...input.fxSelection,mean:'65'}).fxLink,undefined);assert.equal(input.fxSelection.mean,'64');assert.equal(input.fxLink.id,'saved');});
|
||||
Reference in New Issue
Block a user