feat(irpc): demonstrate documented prior credit and PEC balances in QA

This commit is contained in:
2026-10-28 14:01:07 +02:00
parent cf4e0638c1
commit d134a92d16
17 changed files with 692 additions and 15 deletions
@@ -1578,3 +1578,16 @@ L8 permanece parcial. A avaliação real do percurso proactivo continua a aguard
**SaldoL8 preservado:** derivação automática da fracção nacional/CDT específicas;créditos económicos/CBF;utilização interanual/transporte/caducidade próprios;obrigação/calendárioPPC/PEC;autónomos,períodos especiais,moedas eM20/M22 conforme dependências já discriminadas. Excedente/transportePEC fora do âmbito entregue. Informação de inclusão/CDT/prova/titular/uso bloqueia apenas o caso dependente. As condições concretas de retoma mantêm-se no saldo curtoQA anterior;sem repetir pesquisas ou exigir nova autenticação de fontes oficiais aceites.
Recibo:`docs/investmoz/simulations/irpc-l8-international-credit-delivery-20261027.json`. Cópia remota:`/var/backups/investmoz-irpc-l8-international-20261027/installed-receipt.json`,SHA256`d80324b898fd160ab3e7a2fd0b11ea2decd0b99a9324255850062ee4a13e0f0f`. Provas locais:`outputs/irpc-l8-international-delivery-20261027/`. Resolvida a colisão local`inspect.py`→`remote_delivery_inspection.py`;módulo padrão/Paramiko verificados no mesmoPython,guardaLinux/geteuid mantida,sem alteração de credenciais ou do servidor por essa correcção. A avaliação real do percurso proactivo continua dependente do retorno da equipa. **L8 permanece parcial.**
## 28/10/2026 — saldos anteriores internacionais e PEC: delta QA concluído
Implementado e demonstrado em QA isolada, **sem instalação**. Base `cf4e0638`, com internacional `838ad941`, PEC `e182d1d7` e sectores `669cb897` preservados. A pasta original estava noutro ramo; isso não foi tratado como eliminação de funcionalidades. Recibo: `docs/investmoz/simulations/irpc-l8-carry-qa-20261028.json`.
Extensão opcional do livro anual existente: uma origem consolidada por natureza/país/exercício, apuramento e regra de origem documentados, elegível menos dedução inicial e utilizações efectivas subsequentes. Internacional: cinco exercícios seguintes; PEC: três. Origem2021 internacional e PEC2023 chegam a2026; origem2020 internacional/PEC2022 ficam fora do prazo ordinário, conservadas com explicação e saldo histórico. Não se converte imposto acima do limite em crédito transportável. Situações transitórias/especiais não são qualificadas pela regra ordinária.
Escolha expressa das parcelas correntes e anteriores, vinculada à colecta seleccionada, sem ordem FIFO imposta nem redução automática. A soma não ultrapassa a colecta e os créditos internacionais precedem o PEC. Valores não escolhidos ficam separados da insuficiência de colecta; simulações não criam nem consomem saldos oficiais. Exemplo sintético:1408−8corrente internacional−50anterior internacional−200PECcorrente−100PECanterior−500PPC=**550MZN a pagar, simulado**.
**Provas:**16novos+49afectados=65testes aprovados; API1teste/119asserções,6casos POST/PUT/GET na base guardada `proactive_investment_20261027_test`,loopback55441,com rollback. Seis recuperações recalculadas integralmente,HTML/dossier concordantes e resultados anteriores congelados.55oportunidades,64documentos,1vínculo,19rascunhos,40utilizadores,3organizações e409eventos anteriores preservados.31ficheiros do acervo byte a byte iguais,sem nova importação/publicação. TypeScript,build QA com exit0 ePint aprovados. Não foi feita nova prova manual de interface ou avaliação pela equipa. Sem migrations,grants,contas,alteração activa ou operações de servidor.
**Saldo concreto:**L8 continua parcial. Próximo incremento independente:crédito económico64,com gross-up53(1)a/exclusão40,qualificação dos lucros/pagador/beneficiário/imposto e limite proporcional correspondidos; fonte aceite já disponível. Permanecem fracção nacional/CDT automática,créditos/benefícios próprios,obrigação/calendárioPPC/PEC,autónomos,períodos especiais,outras moedas eM20/M22. Não declarar IRPC concluído nem repetir a validação das entregas independentes aceites.
@@ -0,0 +1,183 @@
{
"date": "2026-10-28",
"status": "implemented and demonstrated in isolated QA; not installed",
"branch": "codex/irpc-l8-carry-qa-20261028",
"base": "cf4e0638",
"installed_reference_unchanged": "838ad941a24ae76ec4bf91ccc3f92012668fe8cf",
"scope": {
"year": "2026",
"subject": "same subject, resident general",
"period": "annual ordinary origins; calendar-year current close",
"currency": "documented MZN; exact foreign-credit fractions supported",
"international": "eligible origin credit minus origin deduction and documented actual later uses; five subsequent exercises, art65(3)",
"pec": "actual PEC eligible origin minus origin deduction and actual later uses; three subsequent exercises, art66(1–2)",
"allocation": "explicit documented selection between current and prior exercises, confirmed against current assessment; no presumed FIFO or automatic reduction",
"expiry": "ordinary window ended excludes current deduction but keeps source and arithmetic historical remainder",
"origin_rules": "origin assessment, inclusion/limits/insufficient collection and applicable period documented; exceptional/transitional rules outside scope"
},
"restriction_origins": {
"Tempo": "preserve approved sources, accepted deliveries, data/history, QA isolation; continue L8",
"accepted_requirement": "CIRPC65(3),66(1–2), source already accepted; no new provenance approval",
"technical_choices": "optional versioned snapshot extension; consolidated nature/country/origin-year balance; explicit selection avoids invented interyear priority"
},
"verification": {
"new_frontend_tests": 16,
"affected_previously_accepted_tests": 49,
"total_frontend_pass": 65,
"frontend_fail": 0,
"API_tests": 1,
"API_assertions": 119,
"API_cases": 6,
"API_database": "proactive_investment_20261027_test",
"API_port": 55441,
"API_transactions_rolled_back": true,
"recovered_recalculated_HTML_dossier": 6,
"frozen_prior_results_preserved": true,
"typescript": "passed",
"build": "passed, actual subprocess exit0",
"PHP_formatting": "pint passed",
"browser_manual": "not performed; no manual UI acceptance claimed"
},
"preservation": {
"previous_deliveries": [
"international838ad941",
"PECe182d1d7",
"sectors669cb897"
],
"source_files_byte_identical": 31,
"existing_QA_records": {
"database": "proactive_investment_20261027_test",
"tables": {
"opportunities": {
"existing_rows": 55,
"unchanged": true
},
"documents": {
"existing_rows": 64,
"unchanged": true
},
"opportunity_documents": {
"existing_rows": 1,
"unchanged": true
},
"simulation_form_drafts": {
"existing_rows": 19,
"unchanged": true
},
"users": {
"existing_rows": 40,
"unchanged": true
},
"organizations": {
"existing_rows": 3,
"unchanged": true
},
"audit_events": {
"existing_rows": 409,
"unchanged": true
}
},
"transaction_rollback": true
},
"matrix": "existing AUD-SIM-INT02 updated; all other rows semantically unchanged",
"new_accounts_permissions_migrations": false,
"active_data_or_configuration_written": false,
"remote_deployment_or_publication": false,
"Songo_Gurue": "no pilot changes; remain private",
"other_dossiers": "same-snapshot origins checked, no global official account reconciliation"
},
"cases": [
{
"name": "payable",
"status": "payable",
"balance": {
"numerator": "550",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "expired",
"status": "indeterminate",
"balance": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "missing-use",
"status": "indeterminate",
"balance": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "overallocated",
"status": "indeterminate",
"balance": null,
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "fractional",
"status": "payable",
"balance": {
"numerator": "1822",
"denominator": "3"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
},
{
"name": "recoverable",
"status": "recoverable",
"balance": {
"numerator": "-500",
"denominator": "1"
},
"apiRecovered": true,
"recalculationEqual": true,
"html": true,
"dossier": true,
"frozenHistoryPreserved": true
}
],
"remaining_L8": [
"automatic national-fraction derivation and case-specific treaty correspondence",
"economic credit art64/53 and benefit-specific credits with own qualification/limits",
"PPC/PEC obligation and calendar, interest/penalties and payment guides with applicable edition",
"autonomous taxation, special periods, other currencies, M20/M22 transport/submission",
"exceptional or transitional interyear cases outside ordinary source window; no official ledger created"
],
"next_independent_increment": {
"item": "economic double-tax credit",
"source": "existing accepted CIRPC64,53(1)a and exclusion40; BR52 original",
"required_correspondence": "distributed profits included, payer/recipient residence and IRPC nonexemption, art40 distinction, source tax, gross-up53, proportional cap64, actual use and nonduplication; do not infer from declared dividend alone"
},
"L8_complete": false,
"evidence": {
"api.log": "ef604fbe7a289930cb0b6dfa35407e0f115092894d32fd98c04a9e1f3b732415",
"build-result.json": "80bb1b6bb0ccadaf94f241ace9b0b19f77461b193b8b7522afa655d414d36198",
"build.log": "d93839f94f37e181acafe3f6295c10982e036475b1f5186aedba9a9ae3cd6bd9",
"existing-records-preservation.json": "b45b3306d3c9dee63fbbaf9a04bba31008019e65892bd16322460b5ef53eda7d",
"frontend-tests.log": "86a878a794a8f1ed7db56d689f0b9d5a2bdbc6079aa66e3cfe4b65ae64e656f6",
"protected-consumers.json": "9f147ea688a79a77a8d7507cf1c322a6a3c03b959be8fc5ed114db63dac05761",
"recovered-report-receipt.json": "82df86eaabbb79d760e4f2f1efb0f900d9e9607e11ea49502746dbafc46fed03",
"source-preservation.json": "c28df37db83989fe5746dcb5382a75c4f39471a16a7cbe64670260209cf6ad79"
}
}
@@ -209,3 +209,8 @@ CIRPC65(1–2), Lei34/2007, exemplarBR52, PDF18/p768-(72), e ordem face aoPEC em
### 27/10/2026 — Crédito internacional entregue
A implementaçãoQA documentada acima foi instalada em`838ad941`,sem alterar fontes ou concederCDT automaticamente. CIRPC65(1–2)/66/RIRPC21 reutilizados no recorte demonstrado;65(3) e transportePEC continuam separados. Ver `irpc-l8-international-credit-delivery-20261027.json`.
## 28/10/2026 — aplicação de65(3)/66 aos saldos anteriores
Reutilizados os31ficheiros do acervo, todos iguais por bytes a `cf4e0638`. Lei34/2007,BR52,PDF18/p768-(72),SHA256`cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e`: página conferida visualmente;65(3)cinco exercícios seguintes,66(1–2)três paraPEC,sem reembolso. Referência à regra/apuramento aplicáveis na origem é correspondência de período, não nova validação de autenticidade da fonte. Leitura remota do URL arquivado falhou por timeout; usado o exemplar oficial exacto já aceite. Nenhum original alterado,scan declarado ou novo cálculo activado por publicação. Recorte:origens anuais ordinárias documentadas,mesmo sujeito,fecho2026/MZN;sem situações transitórias/especiais presumidas. Recibo `irpc-l8-carry-qa-20261028.json`.
@@ -7120,3 +7120,23 @@ L8 permanece parcial. A avaliação real do percurso proactivo continua a aguard
**SaldoL8 preservado:** derivação automática da fracção nacional/CDT específicas;créditos económicos/CBF;utilização interanual/transporte/caducidade próprios;obrigação/calendárioPPC/PEC;autónomos,períodos especiais,moedas eM20/M22 conforme dependências já discriminadas. Excedente/transportePEC fora do âmbito entregue. Informação de inclusão/CDT/prova/titular/uso bloqueia apenas o caso dependente. As condições concretas de retoma mantêm-se no saldo curtoQA anterior;sem repetir pesquisas ou exigir nova autenticação de fontes oficiais aceites.
Recibo:`docs/investmoz/simulations/irpc-l8-international-credit-delivery-20261027.json`. Cópia remota:`/var/backups/investmoz-irpc-l8-international-20261027/installed-receipt.json`,SHA256`d80324b898fd160ab3e7a2fd0b11ea2decd0b99a9324255850062ee4a13e0f0f`. Provas locais:`outputs/irpc-l8-international-delivery-20261027/`. Resolvida a colisão local`inspect.py`→`remote_delivery_inspection.py`;módulo padrão/Paramiko verificados no mesmoPython,guardaLinux/geteuid mantida,sem alteração de credenciais ou do servidor por essa correcção. A avaliação real do percurso proactivo continua dependente do retorno da equipa. **L8 permanece parcial.**
## 28/10/2026 — retoma de L8 e preservação antes do novo delta
28/10/2026: inventário prévio à QA dos saldos anteriores: internacional corrente838ad941, PECe182d1d7 e sectores669cb897 preservados na basecf4e0638; todos os ficheiros do acervo iguais por bytes. Diferença da pasta original é de ramo/ambiente, não perda confirmada. Sem leitura ou escrita de dados activos; recibos aceites reutilizados. Continuação emQA de65(3)/66, sem declarar L8 concluído.
Escolha técnica: novo worktree `portal-investidor-irpc-carry-qa-20261028`, ramo `codex/irpc-l8-carry-qa-20261028`, a partir de `cf4e0638`. Sem migrations, novas permissões ou alterações no activo. Nenhuma perda funcional confirmada nesta comparação de fontes; não se declara nova comparação de dados/schema/permissões no servidor. Songo/Gurué e a avaliação pela equipa conservam os estados anteriores.
## 28/10/2026 — saldos anteriores internacionais e PEC: delta QA concluído
Implementado e demonstrado em QA isolada, **sem instalação**. Base `cf4e0638`, com internacional `838ad941`, PEC `e182d1d7` e sectores `669cb897` preservados. A pasta original estava noutro ramo; isso não foi tratado como eliminação de funcionalidades. Recibo: `docs/investmoz/simulations/irpc-l8-carry-qa-20261028.json`.
Extensão opcional do livro anual existente: uma origem consolidada por natureza/país/exercício, apuramento e regra de origem documentados, elegível menos dedução inicial e utilizações efectivas subsequentes. Internacional: cinco exercícios seguintes; PEC: três. Origem2021 internacional e PEC2023 chegam a2026; origem2020 internacional/PEC2022 ficam fora do prazo ordinário, conservadas com explicação e saldo histórico. Não se converte imposto acima do limite em crédito transportável. Situações transitórias/especiais não são qualificadas pela regra ordinária.
Escolha expressa das parcelas correntes e anteriores, vinculada à colecta seleccionada, sem ordem FIFO imposta nem redução automática. A soma não ultrapassa a colecta e os créditos internacionais precedem o PEC. Valores não escolhidos ficam separados da insuficiência de colecta; simulações não criam nem consomem saldos oficiais. Exemplo sintético:1408−8corrente internacional−50anterior internacional−200PECcorrente−100PECanterior−500PPC=**550MZN a pagar, simulado**.
**Provas:**16novos+49afectados=65testes aprovados; API1teste/119asserções,6casos POST/PUT/GET na base guardada `proactive_investment_20261027_test`,loopback55441,com rollback. Seis recuperações recalculadas integralmente,HTML/dossier concordantes e resultados anteriores congelados.55oportunidades,64documentos,1vínculo,19rascunhos,40utilizadores,3organizações e409eventos anteriores preservados.31ficheiros do acervo byte a byte iguais,sem nova importação/publicação. TypeScript,build QA com exit0 ePint aprovados. Não foi feita nova prova manual de interface ou avaliação pela equipa. Sem migrations,grants,contas,alteração activa ou operações de servidor.
**Saldo concreto:**L8 continua parcial. Próximo incremento independente:crédito económico64,com gross-up53(1)a/exclusão40,qualificação dos lucros/pagador/beneficiário/imposto e limite proporcional correspondidos; fonte aceite já disponível. Permanecem fracção nacional/CDT automática,créditos/benefícios próprios,obrigação/calendárioPPC/PEC,autónomos,períodos especiais,outras moedas eM20/M22. Não declarar IRPC concluído nem repetir a validação das entregas independentes aceites.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import {SimulationIrpcCarry} from './simulation-irpc-carry';
import {SimulationIrpcInternational} from './simulation-irpc-international';
import {SimulationIrpcPec} from './simulation-irpc-pec';
import {useState} from 'react';
@@ -20,7 +21,7 @@ export function SimulationIrpcAnnual({draft,onChange}:Props){
{b.withholdings.rows.map((r,index)=>{const change=(p:Partial<typeof r>)=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>Retenção seleccionada {index+1}</legend><p>Rendimento:{r.rowId};prova:{r.paymentId};NUIT:{r.subjectNuit};exercício:{r.year}. Fonte conservada. Se mudou, retire esta selecção e seleccione a prova actual. O histórico mantém-se.</p><label className="block">Parcela a abater<input aria-label={'Parcela da retenção L8 '+(index+1)} value={r.amount} onChange={e=>change({amount:e.target.value})}/></label><label className="block">O próprio sujeito é beneficiário e o rendimento está incluído na base deste exercício<select aria-label={'Rendimento incluído L8 '+(index+1)} value={r.incomeIncluded} onChange={e=>change({incomeIncluded:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — crédito não aplicável neste fecho</option></select></label><label className="block">Suporte da titularidade e inclusão<input aria-label={'Suporte da retenção L8 '+(index+1)} value={r.reference} onChange={e=>change({reference:e.target.value})}/></label><button type="button" onClick={()=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.filter(x=>x.id!==r.id)}})}>Retirar selecção L8 da retenção {index+1}</button></fieldset>;})}</fieldset>
<fieldset className="space-y-2 rounded border p-3"><legend>PPC efectivamente pagos</legend>{group('ppc','PPC efectuados')}<p>Regista comprovativos de pagamentos por conta; não calcula automaticamente a obrigação PPC nem usa pagamentos previstos no plano.</p><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:[...b.ppc.rows,{...newAnnualPpc(crypto.randomUUID()),subjectNuit:b.subjectNuit,year:b.year}]}})}>Registar comprovativo PPC</button>
{b.ppc.rows.map((r,index)=>{const change=(p:Partial<AnnualPpc>)=>patch({ppc:{...b.ppc,rows:b.ppc.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>PPC {index+1}</legend>{([['document','Comprovativo'],['issuer','Emitente'],['date','Data (AAAA-MM-DD)'],['subjectNuit','NUIT beneficiário'],['year','Exercício do comprovativo'],['currency','Moeda do comprovativo'],['proofAmount','Montante comprovado'],['amount','Parcela a abater'],['reference','Suporte do pagamento e afectação']]as const).map(([key,label])=><label key={key} className="block">{label}<input aria-label={'PPC L8 '+(index+1)+' — '+label} value={r[key]} onChange={e=>change({[key]:e.target.value})}/></label>)}<label className="block">Pagamento efectivamente comprovado<select aria-label={'PPC L8 '+(index+1)+' — Efectivo'} value={r.effective} onChange={e=>change({effective:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — comprovativo reconciliado</option><option value="no">Não — apenas previsão/outro estado</option></select></label><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:b.ppc.rows.filter(x=>x.id!==r.id)}})}>Retirar PPC {index+1}</button></fieldset>;})}</fieldset>
<SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/>{([...(b.pecPayments?[]:[['pec','PEC']as const]),...(b.internationalCredits?[]:[['credits','Créditos com regras próprias']as const])]).map(([key,label])=><fieldset key={key} className="rounded border p-3"><legend>{label} — tratamento separado</legend><label className="block">Estado — {label}<select aria-label={'Estado L8 — '+label} value={b[key].state} onChange={e=>patch({[key]:{...b[key],state:e.target.value as ''|'known'|'absent'}})}><option value="">Informação em falta</option><option value="absent">Ausência declarada com suporte</option><option value="known">Valor conhecido — positivo exige regra própria</option></select></label>{(b[key].state==='known'||b[key].amount)&&<label className="block">Montante declarado<input aria-label={'Montante L8 — '+label} value={b[key].amount} onChange={e=>patch({[key]:{...b[key],amount:e.target.value}})}/></label>}<label className="block">Suporte<input aria-label={'Suporte L8 — '+label} value={b[key].reference} onChange={e=>patch({[key]:{...b[key],reference:e.target.value}})}/></label></fieldset>)}
<SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/><SimulationIrpcCarry book={b} save={save}/>{([...(b.pecPayments?[]:[['pec','PEC']as const]),...(b.internationalCredits?[]:[['credits','Créditos com regras próprias']as const])]).map(([key,label])=><fieldset key={key} className="rounded border p-3"><legend>{label} — tratamento separado</legend><label className="block">Estado — {label}<select aria-label={'Estado L8 — '+label} value={b[key].state} onChange={e=>patch({[key]:{...b[key],state:e.target.value as ''|'known'|'absent'}})}><option value="">Informação em falta</option><option value="absent">Ausência declarada com suporte</option><option value="known">Valor conhecido — positivo exige regra própria</option></select></label>{(b[key].state==='known'||b[key].amount)&&<label className="block">Montante declarado<input aria-label={'Montante L8 — '+label} value={b[key].amount} onChange={e=>patch({[key]:{...b[key],amount:e.target.value}})}/></label>}<label className="block">Suporte<input aria-label={'Suporte L8 — '+label} value={b[key].reference} onChange={e=>patch({[key]:{...b[key],reference:e.target.value}})}/></label></fieldset>)}
<button type="button" onClick={()=>{try{save(recordAnnual(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar saldo anual L8</button><button type="button" onClick={()=>{try{const html=annualHtml(draft),url=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),link=document.createElement('a');link.href=url;link.download='IRPC-L8-saldo-simulado.html';link.click();URL.revokeObjectURL(url);setError('');}catch(e){setError(String(e));}}}>Exportar saldo anual L8</button>
{annualReportRows(draft).map((r,n)=><p key={n}>{r[1]} — {r[2]} — {r[3]}</p>)}</section>;
}catch(e){return<p role="alert">PreparaçãoL8 não recuperável:{String(e)}. Originais e históricos conservados.</p>;}
@@ -0,0 +1,53 @@
import {prepareCreditCarry, type AnnualBook} from '@/lib/irpc-annual-settlement';
import {carrySource, newCarryLot, newCarryUse, type CarryLot, type CreditCarry} from '@/lib/irpc-credit-carry';
import CountrySelect, {type Country} from '@/components/country-select';
import {countryIsoCodes} from '@/lib/country-iso-codes';
const pt = new Intl.DisplayNames(['pt'], {type: 'region'});
const en = new Intl.DisplayNames(['en'], {type: 'region'});
const countries: Country[] = countryIsoCodes.filter(code => code !== 'MZ').map(code => ({code, name: pt.of(code) ?? code, name_en: en.of(code) ?? code})).sort((a, b) => a.name.localeCompare(b.name, 'pt'));
export function SimulationIrpcCarry({book, save}: {book: AnnualBook; save: (book: AnnualBook) => void}) {
const c = book.creditCarry;
if (!c) return <fieldset className="rounded border p-3"><legend>Saldos de exercícios anteriores</legend><p>Se dispõe de remanescentes documentados de crédito internacional ou PEC, prepare a origem e as utilizações efectivas. Os resultados anteriores ficam conservados.</p><button type="button" onClick={() => save(prepareCreditCarry(book))}>Preparar saldos anteriores</button></fieldset>;
const update = (value: CreditCarry) => save({...book, creditCarry: value});
const lotChange = (r: CarryLot) => update({...c, lots: c.lots.map(x => x.id === r.id ? r : x)});
return <fieldset className="space-y-3 rounded border p-3"><legend>Crédito internacional e PEC — exercícios anteriores</legend>
<p>{carrySource}</p>
<label className="block">Inventário de saldos anteriores<select aria-label="Estado dos saldos anteriores L8" value={c.state} onChange={e => update({...c, state: e.target.value as CreditCarry['state']})}><option value="">Informação em falta</option><option value="known">Saldos conhecidos</option><option value="absent">Ausência documentada</option></select></label>
<label className="block">Suporte do inventário<input aria-label="Suporte dos saldos anteriores L8" value={c.reference} onChange={e => update({...c, reference: e.target.value})}/></label>
{(c.state === 'known' || c.lots.length > 0) && <>
<p>Consolide uma origem por natureza, país e exercício. Transcreva o montante elegível apurado e o deduzido na origem, com o suporte da regra desse exercício. Imposto estrangeiro acima dos limites de elegibilidade não é remanescente por insuficiência de colecta. Só se aplica aqui o prazo ordinário a exercícios anuais; situações transitórias ou especiais exigem correspondência própria.</p>
<button type="button" onClick={() => update({...c, lots: [...c.lots, {...newCarryLot(crypto.randomUUID()), subjectNuit: book.subjectNuit}]})}>Registar origem de saldo anterior</button>
</>}
{c.lots.map((r, i) => {
const patch = (v: Partial<CarryLot>) => lotChange({...r, ...v});
const prefix = 'Saldo anterior ' + (i + 1) + ' — ';
return <fieldset key={r.id} className="space-y-2 rounded border p-3"><legend>Origem {i + 1}</legend>
<label className="block">Natureza<select aria-label={prefix + 'Natureza'} value={r.kind} onChange={e => patch({kind: e.target.value as CarryLot['kind']})}><option value="">Por identificar</option><option value="international">Crédito por dupla tributação internacional</option><option value="pec">PEC efectivamente pago</option></select></label>
{(r.kind === 'international' || r.country) && <CountrySelect countries={countries} label={prefix + 'País estrangeiro'} value={r.country} onChange={country => patch({country})} allowLegacyValue/>}
{r.kind === 'pec' && <p>O PEC não tem país estrangeiro. A escolha da natureza conserva os valores introduzidos; reconcilie qualquer país anterior.</p>}
{([['subjectNuit', 'NUIT titular'], ['year', 'Exercício de origem'], ['currency', 'Moeda comprovada'], ['originDocument', 'Declaração/apuramento de origem'], ['originReference', 'Suporte da origem e insuficiência de colecta'], ['ruleReference', 'Regra e período aplicáveis na origem'], ['eligible', 'Montante elegível na origem — MZN'], ['originDeducted', 'Deduzido na origem — MZN'], ['selected', 'Parcela escolhida para este fecho — MZN']] as const).map(([key, label]) => <label key={key} className="block">{label}<input aria-label={prefix + label} value={r[key]} onChange={e => patch({[key]: e.target.value})}/></label>)}
<label className="block">Exercício de origem anual ordinário<select aria-label={prefix + 'Período ordinário'} value={r.ordinary} onChange={e => patch({ordinary: e.target.value as CarryLot['ordinary']})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — regra específica por corresponder</option></select></label>
<label className="block">Remanescente por insuficiência de colecta<select aria-label={prefix + 'Insuficiência de colecta'} value={r.insufficient} onChange={e => patch({insufficient: e.target.value as CarryLot['insufficient']})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — não é saldo elegível neste percurso</option></select></label>
<label className="block">Utilizações efectivas posteriores à origem<select aria-label={prefix + 'Estado dos usos'} value={r.usesState} onChange={e => patch({usesState: e.target.value as CarryLot['usesState']})}><option value="">Informação em falta</option><option value="known">Utilizações conhecidas</option><option value="absent">Ausência documentada</option></select></label>
<label className="block">Suporte do inventário de utilizações<input aria-label={prefix + 'Suporte dos usos'} value={r.usesReference} onChange={e => patch({usesReference: e.target.value})}/></label>
<p>Inclua utilizações efectivas já ocorridas neste exercício antes desta parcela. Não registe simulações históricas como utilizações fiscais. Crédito internacional admite fracção exacta n/d; não é aplicado arredondamento fiscal.</p>
{r.usesState === 'known' && <button type="button" onClick={() => patch({uses: [...r.uses, newCarryUse(crypto.randomUUID())]})}>Registar utilização efectiva da origem {i + 1}</button>}
{r.uses.map((u, j) => <fieldset key={u.id} className="rounded border p-2"><legend>Utilização {i + 1}.{j + 1}</legend>
{([['year', 'Exercício'], ['amount', 'Montante MZN'], ['document', 'Documento de utilização'], ['reference', 'Suporte']] as const).map(([key, label]) => <label key={key} className="block">{label}<input aria-label={prefix + 'Uso ' + (j + 1) + ' — ' + label} value={u[key]} onChange={e => patch({uses: r.uses.map(x => x.id === u.id ? {...x, [key]: e.target.value} : x)})}/></label>)}
<button type="button" onClick={() => patch({uses: r.uses.filter(x => x.id !== u.id)})}>Retirar utilização {i + 1}.{j + 1}</button>
</fieldset>)}
<button type="button" onClick={() => update({...c, lots: c.lots.filter(x => x.id !== r.id)})}>Retirar origem {i + 1}</button>
</fieldset>;
})}
{(c.state === 'known' || c.currentInternational || c.currentPec || c.allocationReference) && <fieldset className="space-y-2 rounded border p-3"><legend>Escolha das parcelas entre exercícios</legend>
<p>Escolha quanto deduzir dos créditos correntes e de cada origem anterior. Os créditos internacionais precedem o PEC; a soma fica limitada à colecta. Não se presume prioridade entre exercícios e nenhuma parcela é reduzida automaticamente. Valores não escolhidos ficam identificados, sem criação de transporte oficial.</p>
<label className="block">Crédito internacional corrente a deduzir — MZN<input aria-label="Parcela corrente internacional L8" value={c.currentInternational} onChange={e => update({...c, currentInternational: e.target.value})}/></label>
<label className="block">PEC corrente a deduzir — MZN<input aria-label="Parcela corrente PEC L8" value={c.currentPec} onChange={e => update({...c, currentPec: e.target.value})}/></label>
<label className="block">Suporte da escolha e seus impactos<input aria-label="Suporte da afectação entre exercícios L8" value={c.allocationReference} onChange={e => update({...c, allocationReference: e.target.value})}/></label>
<button type="button" onClick={() => update({...c, assessmentKey: book.assessmentKey})}>Confirmar afectação à colecta seleccionada</button>
<p>{c.assessmentKey && c.assessmentKey === book.assessmentKey ? 'Escolha vinculada à colecta seleccionada; o cálculo verifica disponibilidade e limites.' : 'Falta confirmar a escolha na colecta seleccionada. Valores conservados.'}</p>
</fieldset>}
</fieldset>;
}
+30 -7
View File
@@ -1,3 +1,4 @@
import {carryPlan,carryIdentity,carrySource,newCreditCarry,validateCreditCarry,type CreditCarry} from './irpc-credit-carry.ts';
import {validateInternational,internationalProofValue,internationalGroupPending,internationalPosition,internationalDeduction,internationalCreditSource,newInternationalCredits,type InternationalCredits,type InternationalTaxProof} from './irpc-international-credit.ts';
import {validatePec,pecPaymentValue,pecDeduction,pecSource,type PecPayments} from './irpc-pec.ts';
import {normalBook,normalCurrent} from './irpc-normal.ts';
@@ -14,11 +15,12 @@ type Group={state:State;reference:string};
export type AnnualWithholding={id:string;rowId:string;paymentId:string;sourceKey:string;subjectNuit:string;year:string;amount:string;incomeIncluded:''|'yes'|'no';reference:string};
export type AnnualPpc={id:string;document:string;issuer:string;date:string;subjectNuit:string;year:string;currency:string;proofAmount:string;amount:string;effective:''|'yes'|'no';reference:string};
type AnnualRecord={version:typeof annualSettlementVersion;key:string;createdAt:string;model:AnnualModel};
export type AnnualBook={version:1;subjectNuit:string;year:string;reference:string;assessment:''|'normal'|'special';assessmentKey:string;assessmentReference:string;withholdings:Group&{rows:AnnualWithholding[]};ppc:Group&{rows:AnnualPpc[]};pec:Evidence;pecPayments?:PecPayments;credits:Evidence;internationalCredits?:InternationalCredits;result?:AnnualRecord;history:AnnualRecord[]};
export type AnnualBook={version:1;subjectNuit:string;year:string;reference:string;assessment:''|'normal'|'special';assessmentKey:string;assessmentReference:string;withholdings:Group&{rows:AnnualWithholding[]};ppc:Group&{rows:AnnualPpc[]};pec:Evidence;pecPayments?:PecPayments;credits:Evidence;internationalCredits?:InternationalCredits;creditCarry?:CreditCarry;result?:AnnualRecord;history:AnnualRecord[]};
export const annualSource=(b:AnnualBook)=>{
const excluded=b.internationalCredits?(b.pecPayments?'Créditos económicos/benefícios positivos':'PEC/créditos económicos/benefícios positivos'):(b.pecPayments?'Créditos próprios positivos':'PEC/créditos positivos');
return annualSettlementSource.replace('PEC/créditos positivos',excluded)+(b.pecPayments?' '+pecSource:'')+(b.internationalCredits?' '+internationalCreditSource:'');
return annualSettlementSource.replace('PEC/créditos positivos',excluded)+(b.pecPayments?' '+pecSource:'')+(b.internationalCredits?' '+internationalCreditSource:'')+(b.creditCarry?' '+carrySource:'');
};
export function prepareCreditCarry(b:AnnualBook):AnnualBook{return b.creditCarry?b:{...b,creditCarry:newCreditCarry()};}
export function prepareInternationalBook(b:AnnualBook):AnnualBook{
if(b.internationalCredits)return b;
const c=newInternationalCredits(),old=b.credits;
@@ -41,6 +43,7 @@ const proofIdentity=(issuer:string,document:string,_date:string)=>JSON.stringify
function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros fiscais não recuperáveis;originais conservados.');return s;}
function books(d:Record<string,unknown>):Record<string,AnnualBook>{const m=store(d).irpcAnnual;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('MapaL8 não recuperável;originais conservados.');return m;}
export function validateAnnual(b:AnnualBook){
if(b?.creditCarry!==undefined)validateCreditCarry(b.creditCarry);
if(b?.pecPayments!==undefined)validatePec(b.pecPayments);
if(b?.internationalCredits!==undefined)validateInternational(b.internationalCredits);
if(!b||b.version!==1||['subjectNuit','year','reference','assessmentKey','assessmentReference'].some(k=>typeof b[k as 'year']!=='string')||!['','normal','special'].includes(b.assessment)||!Array.isArray(b.history))throw Error('PreparaçãoL8 não recuperável.');
@@ -72,7 +75,7 @@ export function reuseAnnualWithholding(d:Record<string,unknown>,b:AnnualBook,row
if(b.withholdings.rows.some(r=>r.rowId===rowId&&r.paymentId===paymentId))throw Error('Retenção/prova já seleccionada;altere a parcela existente sem duplicar.');
return{...b,withholdings:{...b.withholdings,rows:[...b.withholdings.rows,{id,rowId,paymentId,sourceKey:o.key,subjectNuit:b.subjectNuit,year:o.year,amount:o.amount,incomeIncluded:'',reference:''}]}};
}
function dependencies(d:Record<string,unknown>){let normal:unknown,L5:unknown;try{const a=annualAssessment(d);normal=[a.record,a.current];}catch(e){normal=String(e);}try{const b=irpcBook(d);L5=[b.rows,b.payments,b.result,irpcCurrent(d,b)];}catch(e){L5=String(e);}return[normal,L5,Object.entries(books(d)).filter(([k])=>k!==acquisitionScope(d)).map(([k,b])=>[k,b.subjectNuit,b.year,b.withholdings,b.ppc,...(b.pecPayments?[b.pecPayments]:[]),...(b.internationalCredits?[b.internationalCredits]:[])])];}
function dependencies(d:Record<string,unknown>){let normal:unknown,L5:unknown;try{const a=annualAssessment(d);normal=[a.record,a.current];}catch(e){normal=String(e);}try{const b=irpcBook(d);L5=[b.rows,b.payments,b.result,irpcCurrent(d,b)];}catch(e){L5=String(e);}return[normal,L5,Object.entries(books(d)).filter(([k])=>k!==acquisitionScope(d)).map(([k,b])=>[k,b.subjectNuit,b.year,b.withholdings,b.ppc,...(b.pecPayments?[b.pecPayments]:[]),...(b.internationalCredits?[b.internationalCredits]:[]),...(b.creditCarry?[b.creditCarry]:[])])];}
function key(d:Record<string,unknown>,b:AnnualBook){const{result:_,history:__,...input}=b;return closureFingerprint([annualSettlementVersion,acquisitionScope(d),input,dependencies(d)]);}
export function annualCurrent(d:Record<string,unknown>,b:AnnualBook){return !!b.result&&b.result.key===key(d,b);}
export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
@@ -120,12 +123,24 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
for(const c of claims)if(c.reason)pending.push((c.document||c.id)+': '+c.reason);
const knownWithholdings=addFractions(claims.filter(c=>c.kind==='withholding'&&!c.reason&&c.value).map(c=>c.value!)),knownPpc=addFractions(claims.filter(c=>c.kind==='ppc'&&!c.reason&&c.value).map(c=>c.value!));
const positions=b.internationalCredits?.positions.map(p=>{const own=claims.filter(c=>c.kind==='foreign_credit'&&c.positionId===p.id);const m=internationalPosition(p,addFractions(own.filter(c=>!c.reason&&c.value).map(c=>c.value!)),own.filter(c=>c.reason).map(c=>c.reason),b.subjectNuit,b.year,b.assessmentKey);pending.push(...m.pending.map(e=>'País '+p.country+': '+e));return m;})??[];
const ready=pending.length===0&&colecta!==null,international=b.internationalCredits?internationalDeduction(positions,colecta,ready):null,creditCollecta=international?.afterCredit??colecta;
const pec=b.pecPayments?pecDeduction(addFractions(claims.filter(c=>c.kind==='pec'&&!c.reason&&c.value).map(c=>c.value!)),creditCollecta,ready):null,arithmeticBalance=ready?subtractFractions(subtractFractions(pec?.afterPec??creditCollecta!,knownWithholdings),knownPpc):null;
const pecAvailable=addFractions(claims.filter(c=>c.kind==='pec'&&!c.reason&&c.value).map(c=>c.value!));
const carryDuplicates:string[]=[];
if(b.creditCarry)for(const[scope,other]of Object.entries(books(d))){
if(scope===acquisitionScope(d)||other.subjectNuit!==b.subjectNuit)continue;
try{validateAnnual(other);if(other.creditCarry?.lots.some(r=>b.creditCarry!.lots.some(x=>carryIdentity(x)===carryIdentity(r))))carryDuplicates.push('Saldo anterior já preparado noutro mapa da entidade;consolide a origem e usos sem duplicar: '+scope);}
catch(e){carryDuplicates.push('Outro mapa da entidade não recuperável para conferir saldos anteriores: '+scope+';'+String(e));}
}
const carry=b.creditCarry?carryPlan(b.creditCarry,b.subjectNuit,b.year,b.assessmentKey,addFractions(positions.flatMap(p=>p.available&&!p.pending.length?[p.available]:[])),pecAvailable,colecta,carryDuplicates):null;
if(carry)pending.push(...carry.pending);
const ready=pending.length===0&&colecta!==null,international=b.internationalCredits?internationalDeduction(positions,colecta,ready,carry?.international??undefined):null;
const creditCollecta=ready&&carry?subtractFractions(international?.afterCredit??colecta!,carry.priorInternational):international?.afterCredit??colecta;
const pec=b.pecPayments?pecDeduction(pecAvailable,creditCollecta,ready,carry?.pec??undefined):null;
const afterPec=ready&&carry?subtractFractions(pec?.afterPec??creditCollecta!,carry.priorPec):pec?.afterPec??creditCollecta;
const arithmeticBalance=ready?subtractFractions(subtractFractions(afterPec!,knownWithholdings),knownPpc):null;
if(international&&international.insufficientCollecta&&cmp(international.insufficientCollecta,zero())>0n)warn.push('Crédito elegível sem colecta suficiente:sem reembolso,transporte ou escolha automática da afectação por país.');
let payable:FiscalFraction|null=null,recoverable:FiscalFraction|null=null,belowMinimum:FiscalFraction|null=null;let status:'payable'|'recoverable'|'balanced'|'below_minimum'|'indeterminate'='indeterminate';
if(arithmeticBalance){const n=BigInt(arithmeticBalance.numerator),abs=fraction(n<0n?-n:n,BigInt(arithmeticBalance.denominator));payable=zero();recoverable=zero();belowMinimum=zero();if(!n)status='balanced';else if(cmp(abs,fraction(100n))<0n){status='below_minimum';belowMinimum=abs;warn.push('Art27(6):saldo inferior a100MZN,sem cobrança/reembolso neste recorte;aritmética conservada,não é transporte de crédito.');}else if(n>0n){status='payable';payable=abs;}else{status='recoverable';recoverable=abs;}}
return{version:annualSettlementVersion,input:structuredClone(input),subjectNuit:b.subjectNuit,year:b.year,assessment:assessment?.record?{key:b.assessmentKey,kind:b.assessment,version:assessment.record.model.version,createdAt:assessment.record.createdAt,colecta,period:assessment.period,origin:assessment.record.model.origin}:null,claims,internationalCredit:international,pecDeduction:pec,knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.internationalCredits?['Crédito internacional do próprio exercício:fracção nacional,convenção e montantes MZN são documentados;sem cálculo presumido,transporte ou consumo oficial. Créditos económicos/benefícios positivos ficam separados.']:[]),b.internationalCredits?'L8 parcial:créditos económicos/benefícios positivos,transporte/caducidade,obrigações,tributação autónoma,períodos especiais e outras moedas separados. PEC sem reembolso ou transporte automático.':(b.pecPayments?'L8 parcial:transporte/caducidade de PEC, cálculo da obrigação, créditos próprios positivos, tributação autónoma, períodos especiais e outras moedas separados. Excedente PEC não é reembolso nem saldo transportado oficialmente.':'L8 parcial:PEC e créditos positivos,tributação autónoma,períodos especiais e pagamentos noutra moeda exigem regras próprias.'),'PPC aqui é pagamento comprovado;não cálculo automático da obrigação ou calendário.','Retenção sofrida não é retenção entregue pela empresa por conta de terceiros.','Valores racionais exactos;camposM20/M22 e representação não presumidos.','Saldo simulado por colecta projectada;sem pagamento,submissão ou reembolso automático.']};
return{version:annualSettlementVersion,input:structuredClone(input),subjectNuit:b.subjectNuit,year:b.year,assessment:assessment?.record?{key:b.assessmentKey,kind:b.assessment,version:assessment.record.model.version,createdAt:assessment.record.createdAt,colecta,period:assessment.period,origin:assessment.record.model.origin}:null,claims,internationalCredit:international,pecDeduction:pec,...(carry?{creditCarry:{...carry,afterInternational:ready?creditCollecta:null,afterPec:ready?afterPec:null}}:{}),knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.creditCarry?['Saldos anteriores documentados: escolha explícita das parcelas correntes e anteriores, sem prioridade presumida entre exercícios, reembolso ou criação/consumo de saldos oficiais.']:[]),...(b.internationalCredits?['Crédito internacional do próprio exercício:fracção nacional,convenção e montantes MZN são documentados;sem cálculo presumido,transporte ou consumo oficial. Créditos económicos/benefícios positivos ficam separados.']:[]),b.creditCarry?'L8 parcial:créditos económicos/benefícios,obrigações,tributação autónoma,períodos especiais,outras moedas e correspondência M20/M22 separados. Prazos dos saldos anteriores demonstrados apenas para origens anuais ordinárias documentadas;sem criação automática de transporte.':b.internationalCredits?'L8 parcial:créditos económicos/benefícios positivos,transporte/caducidade,obrigações,tributação autónoma,períodos especiais e outras moedas separados. PEC sem reembolso ou transporte automático.':(b.pecPayments?'L8 parcial:transporte/caducidade de PEC, cálculo da obrigação, créditos próprios positivos, tributação autónoma, períodos especiais e outras moedas separados. Excedente PEC não é reembolso nem saldo transportado oficialmente.':'L8 parcial:PEC e créditos positivos,tributação autónoma,períodos especiais e pagamentos noutra moeda exigem regras próprias.'),'PPC aqui é pagamento comprovado;não cálculo automático da obrigação ou calendário.','Retenção sofrida não é retenção entregue pela empresa por conta de terceiros.','Valores racionais exactos;camposM20/M22 e representação não presumidos.','Saldo simulado por colecta projectada;sem pagamento,submissão ou reembolso automático.']};
}
export type AnnualModel=ReturnType<typeof calculateAnnual>;
export function recordAnnual(d:Record<string,unknown>,b:AnnualBook,now=new Date().toISOString()):AnnualBook{return{...b,history:b.result?[...b.history,structuredClone(b.result)]:b.history,result:{version:annualSettlementVersion,key:key(d,b),createdAt:now,model:calculateAnnual(d,b)}};}
@@ -146,7 +161,15 @@ export function annualReportRows(d:Record<string,unknown>):Array<[string,string,
]as[string,string,string,string]),
...(m.internationalCredit?[
...m.internationalCredit.positions.map(p=>['IRPC — L8','Crédito internacional — '+p.country,'Imposto pago seleccionado: '+show(p.paid)+'; fracção nacional: '+show(p.nationalLimit)+'; limite CDT: '+(p.input.treaty==='no'?'Não aplicável documentado':show(p.treatyLimit))+'; elegível: '+show(p.eligible)+'; utilização anterior: '+show(p.previousUse)+'; disponível: '+show(p.available)+'; imposto acima dos limites: '+show(p.aboveLimit)+'. '+p.pending.join(' | '),p.input.incomeReference+'; '+p.input.nationalLimitReference+'; '+p.input.treatyReference+'; '+p.input.previousUse.reference]as[string,string,string,string]),
['IRPC — L8','Crédito internacional — antes do PEC','Disponível conhecido: '+show(m.internationalCredit.available)+'; dedução: '+show(m.internationalCredit.deducted)+'; colecta após crédito: '+show(m.internationalCredit.afterCredit)+'; elegível não deduzido por insuficiência de colecta: '+show(m.internationalCredit.insufficientCollecta)+'. Sem reembolso,transporte ou afectação oficial por país. Estado: '+stateLabel(m.input.internationalCredits?.state??'')+'. Créditos económicos/benefícios: '+stateLabel(m.input.internationalCredits?.economic.state??'')+' / '+stateLabel(m.input.internationalCredits?.benefits.state??'')+'.',m.internationalCredit.source+'; '+(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.economic.reference??'')+'; '+(m.input.internationalCredits?.benefits.reference??'')]as[string,string,string,string]
['IRPC — L8','Crédito internacional — antes do PEC','Disponível conhecido: '+show(m.internationalCredit.available)+'; dedução: '+show(m.internationalCredit.deducted)+'; colecta após crédito: '+show(m.internationalCredit.afterCredit)+'; elegível não deduzido por insuficiência de colecta: '+show(m.internationalCredit.insufficientCollecta)+(m.creditCarry?'; crédito corrente não escolhido: '+show(m.internationalCredit.notSelected??null):'')+'. Sem reembolso,transporte ou afectação oficial por país. Estado: '+stateLabel(m.input.internationalCredits?.state??'')+'. Créditos económicos/benefícios: '+stateLabel(m.input.internationalCredits?.economic.state??'')+' / '+stateLabel(m.input.internationalCredits?.benefits.state??'')+'.',m.internationalCredit.source+'; '+(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.economic.reference??'')+'; '+(m.input.internationalCredits?.benefits.reference??'')]as[string,string,string,string]
]:[]),
...(m.creditCarry?[
...m.creditCarry.lots.map(l=>['IRPC — L8','Saldo anterior — '+(l.kind==='pec'?'PEC':'Crédito internacional '+l.country)+' — '+l.year,
'Remanescente da origem: '+show(l.opening)+'; usos efectivos: '+show(l.used)+'; disponível: '+show(l.available)+'; parcela escolhida: '+show(l.selected)+'; saldo aritmético posterior: '+show(l.remaining)+'; último exercício ordinário: '+(l.lastYear??'Indeterminado')+'; prazo terminado: '+(l.expired?'Sim':'Não')+'. '+l.pending.join(' | '),
l.input.originDocument+'; '+l.input.originReference+'; '+l.input.ruleReference+'; '+l.input.usesReference+'; '+l.input.uses.map(u=>u.year+': '+u.amount+' MZN; '+u.document+'; '+u.reference).join(' | ')]as[string,string,string,string]),
['IRPC — L8','Afectação escolhida entre exercícios',
'Corrente internacional: '+show(m.creditCarry.international)+'; anterior internacional: '+show(m.creditCarry.priorInternational)+'; PEC corrente: '+show(m.creditCarry.pec)+'; PEC anterior: '+show(m.creditCarry.priorPec)+'; colecta após créditos internacionais: '+show(m.creditCarry.afterInternational)+'; colecta após todos os PEC: '+show(m.creditCarry.afterPec)+'. Parcelas só deduzidas se o saldo estiver determinado. Sem criação ou consumo de saldo oficial. '+m.creditCarry.pending.join(' | '),
(m.input.creditCarry?.reference??'')+'; '+(m.input.creditCarry?.allocationReference??'')+'; '+m.creditCarry.source]as[string,string,string,string]
]:[]),
...(m.pecDeduction?[[
'IRPC — L8','PEC — dedução limitada à colecta',
+94
View File
@@ -0,0 +1,94 @@
import {countryIsoCodes} from './country-iso-codes.ts';
import {creditCompare, creditMoney, creditLimit} from './irpc-international-credit.ts';
import {addFractions, fraction, subtractFractions, type FiscalFraction} from './fiscal-rational.ts';
export const carryVersion = 'IRPC-credit-carry/1';
export const carrySource = 'CIRPC, Lei 34/2007, art.65(3): crédito internacional elegível não deduzido por insuficiência de colecta, até ao fim dos cinco exercícios seguintes; art.66(1–2): PEC até três exercícios seguintes, depois dos créditos precedentes, sem reembolso. Exemplar BR52, PDF18/p.768-(72). Recorte: fecho2026, mesmo sujeito, exercícios anuais ordinários, montantes comprovados em MZN. A origem e os usos efectivos são documentados; simulações não criam nem consomem saldos oficiais.';
type State = '' | 'known' | 'absent';
export type CarryUse = {id: string; year: string; amount: string; document: string; reference: string};
export type CarryLot = {
id: string; kind: '' | 'international' | 'pec'; country: string; subjectNuit: string; year: string;
currency: string; ordinary: '' | 'yes' | 'no'; originDocument: string; originReference: string;
ruleReference: string; eligible: string; originDeducted: string; insufficient: '' | 'yes' | 'no';
usesState: State; usesReference: string; uses: CarryUse[]; selected: string;
};
export type CreditCarry = {
version: 1; state: State; reference: string; lots: CarryLot[];
currentInternational: string; currentPec: string; allocationReference: string; assessmentKey: string;
};
export const newCreditCarry = (): CreditCarry => ({version: 1, state: '', reference: '', lots: [], currentInternational: '', currentPec: '', allocationReference: '', assessmentKey: ''});
export const newCarryLot = (id: string): CarryLot => ({id, kind: '', country: '', subjectNuit: '', year: '', currency: 'MZN', ordinary: '', originDocument: '', originReference: '', ruleReference: '', eligible: '', originDeducted: '', insufficient: '', usesState: '', usesReference: '', uses: [], selected: ''});
export const newCarryUse = (id: string): CarryUse => ({id, year: '', amount: '', document: '', reference: ''});
const states = ['', 'known', 'absent'];
const zero = () => fraction(0n);
const canonical = (s: string) => s.normalize('NFKC').trim().toLocaleLowerCase('pt').replace(/\s+/g, ' ');
// One consolidated balance per nature/country/origin exercise; changing the document does not create a second quota.
export const carryIdentity = (r: CarryLot) => JSON.stringify([r.subjectNuit, r.kind, r.country, r.year]);
export function validateCreditCarry(c: CreditCarry) {
if (!c || c.version !== 1 || !states.includes(c.state) || ['reference', 'currentInternational', 'currentPec', 'allocationReference', 'assessmentKey'].some(k => typeof (c as unknown as Record<string, unknown>)[k] !== 'string') || !Array.isArray(c.lots)) throw Error('Saldos anteriores não recuperáveis; originais conservados.');
const keys = Object.keys(newCarryLot('')).filter(k => k !== 'uses');
const ids: string[] = [];
for (const r of c.lots) {
if (!r || keys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !['', 'international', 'pec'].includes(r.kind) || !['', 'yes', 'no'].includes(r.ordinary) || !['', 'yes', 'no'].includes(r.insufficient) || !states.includes(r.usesState) || !Array.isArray(r.uses)) throw Error('Origem do saldo anterior não recuperável.');
ids.push(r.id);
for (const u of r.uses) {
if (!u || Object.keys(newCarryUse('')).some(k => typeof (u as unknown as Record<string, unknown>)[k] !== 'string')) throw Error('Utilização anterior não recuperável.');
ids.push(u.id);
}
}
if (new Set(ids).size !== ids.length || ids.some(id => !id.trim())) throw Error('Identificador de saldo/utilização ausente ou repetido.');
}
export function calculateCarryLot(r: CarryLot, subjectNuit: string, year: string) {
let opening: FiscalFraction | null = null, used: FiscalFraction | null = null, available: FiscalFraction | null = null, selected: FiscalFraction | null = null, remaining: FiscalFraction | null = null;
let lastYear: number | null = null, expired = false;
const pending: string[] = [];
try {
if (!r.kind || r.subjectNuit !== subjectNuit || !/^\d{9}$/.test(r.subjectNuit) || year !== '2026' || !/^20\d{2}$/.test(r.year) || Number(r.year) >= Number(year) || r.ordinary !== 'yes' || r.currency !== 'MZN') throw Error('Origem exige mesmo titular, exercício anterior anual ordinário e montante documentado em MZN.');
if (r.kind === 'international' ? !countryIsoCodes.includes(r.country) || r.country === 'MZ' : r.country !== '') throw Error('País estrangeiro obrigatório para crédito internacional; PEC não tem país estrangeiro.');
if (!r.originDocument.trim() || !r.originReference.trim() || !r.ruleReference.trim()) throw Error('Faltam apuramento original e regra aplicável ao exercício de origem, com suporte.');
if (r.insufficient !== 'yes') throw Error('Falta comprovar que o remanescente resulta de insuficiência de colecta na origem; imposto acima do limite não é saldo transportável.');
lastYear = Number(r.year) + (r.kind === 'international' ? 5 : 3);
expired = Number(year) > lastYear;
const amount = r.kind === 'international' ? creditLimit : creditMoney;
const eligible = amount(r.eligible), originDeducted = amount(r.originDeducted);
if (creditCompare(originDeducted, eligible) > 0n) throw Error('Dedução na origem ultrapassa o montante elegível comprovado.');
opening = subtractFractions(eligible, originDeducted);
if (!r.usesState || !r.usesReference.trim() || r.usesState === 'absent' && r.uses.length || r.usesState === 'known' && !r.uses.length) throw Error('Utilizações efectivas posteriores à origem exigem inventário conhecido ou ausência expressa com suporte.');
const documents = new Set<string>();
const values = r.uses.map(u => {
if (!/^20\d{2}$/.test(u.year) || Number(u.year) <= Number(r.year) || Number(u.year) > Number(year) || Number(u.year) > lastYear! || !u.document.trim() || !u.reference.trim()) throw Error('Uso exige exercício elegível, posterior à origem, documento e suporte; pode incluir uso efectivo já ocorrido no exercício corrente.');
const identity = canonical(u.document);
if (documents.has(identity)) throw Error('Documento de utilização repetido; consolide a dedução efectiva.');
documents.add(identity);
return amount(u.amount);
});
used = addFractions(values);
if (creditCompare(used, opening) > 0n) throw Error('Utilizações efectivas ultrapassam o remanescente da origem.');
const balance = subtractFractions(opening, used);
selected = amount(r.selected);
available = expired ? zero() : balance;
if (creditCompare(selected, available) > 0n) throw Error(expired ? 'Prazo ordinário terminado; saldo conservado no histórico, sem dedução neste exercício.' : 'Parcela seleccionada ultrapassa o saldo documentado disponível.');
remaining = subtractFractions(balance, selected);
} catch (e) { pending.push(String(e)); selected = null; }
return {id: r.id, kind: r.kind, country: r.country, year: r.year, opening, used, available, selected, remaining, lastYear, expired, pending, input: structuredClone(r)};
}
export function carryPlan(c: CreditCarry, subjectNuit: string, year: string, assessmentKey: string, currentInternational: FiscalFraction, currentPec: FiscalFraction, colecta: FiscalFraction | null, duplicates: string[] = []) {
validateCreditCarry(c);
const pending = [...duplicates];
if (!c.state || !c.reference.trim() || c.state === 'absent' && c.lots.length || c.state === 'known' && !c.lots.length) pending.push('Inventário de saldos anteriores em falta ou contraditório; desconhecido não significa ausente.');
const ids = c.lots.map(carryIdentity);
if (new Set(ids).size !== ids.length) pending.push('Saldo de mesma natureza/país/exercício repetido; consolide a origem e os usos.');
const lots = c.lots.map(r => calculateCarryLot(r, subjectNuit, year));
pending.push(...lots.flatMap(r => r.pending.map(p => r.year + '/' + r.kind + ': ' + p)));
let international: FiscalFraction | null = null, pec: FiscalFraction | null = null;
try {
if (c.state !== 'known') return {version: carryVersion, lots, international, pec, priorInternational: zero(), priorPec: zero(), pending, source: carrySource, officialBalancesConsumed: false, carryCreated: false};
if (!c.allocationReference.trim() || !c.assessmentKey || c.assessmentKey !== assessmentKey) throw Error('Confirme a escolha das parcelas correntes e anteriores com suporte na colecta seleccionada; não se presume prioridade entre exercícios.');
international = creditLimit(c.currentInternational); pec = creditMoney(c.currentPec);
if (creditCompare(international, currentInternational) > 0n || creditCompare(pec, currentPec) > 0n) throw Error('Parcela corrente escolhida ultrapassa o respectivo crédito/PEC comprovado disponível.');
} catch (e) {pending.push(String(e));}
const priorInternational = addFractions(lots.filter(r => r.kind === 'international' && !r.pending.length && r.selected).map(r => r.selected!));
const priorPec = addFractions(lots.filter(r => r.kind === 'pec' && !r.pending.length && r.selected).map(r => r.selected!));
if (international && pec && colecta && creditCompare(addFractions([international, pec, priorInternational, priorPec]), colecta) > 0n) pending.push('As parcelas escolhidas ultrapassam a colecta; ajuste a afectação entre exercícios antes de confirmar, sem reduzir valores automaticamente.');
return {version: carryVersion, lots, international, pec, priorInternational, priorPec, pending, source: carrySource, officialBalancesConsumed: false, carryCreated: false};
}
@@ -43,7 +43,7 @@ export const creditMoney = (raw: string) => {
return decimalFraction(raw);
};
export const creditCompare = (a: FiscalFraction, b: FiscalFraction) => BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator);
const creditLimit = (raw: string) => {
export const creditLimit = (raw: string) => {
const rational = /^(\d{1,18})\/(\d{1,18})$/.exec(raw);
if (rational) return fraction(BigInt(rational[1]), BigInt(rational[2]));
if (!/^\d{1,18}(\.\d{1,6})?$/.test(raw)) throw Error('Limite documentado em falta/inválido; use decimal ou fracção exacta n/d, sem arredondamento presumido.');
@@ -101,8 +101,8 @@ export function internationalPosition(p: InternationalPosition, paid: FiscalFrac
} catch (e) {pending.push(String(e));available = null;}
return {id: p.id, country: p.country, paid, nationalLimit, treatyLimit, previousUse, eligible, available, aboveLimit, pending, input: structuredClone(p)};
}
export function internationalDeduction(positions: ReturnType<typeof internationalPosition>[], colecta: FiscalFraction | null, ready: boolean) {
export function internationalDeduction(positions: ReturnType<typeof internationalPosition>[], colecta: FiscalFraction | null, ready: boolean, selected?: FiscalFraction | null) {
const available = addFractions(positions.flatMap(p => p.available && !p.pending.length ? [p.available] : []));
const deducted = ready && colecta ? minimum(available, colecta) : null;
return {version: internationalCreditVersion, positions, available, deducted, afterCredit: deducted && colecta ? subtractFractions(colecta, deducted) : null, insufficientCollecta: deducted ? subtractFractions(available, deducted) : null, source: internationalCreditSource, countryAllocationCreated: false, carryCreated: false, officialBalancesConsumed: false};
const deducted = ready && colecta ? selected ?? minimum(available, colecta) : null;
return {version: internationalCreditVersion, positions, available, deducted, afterCredit: deducted && colecta ? subtractFractions(colecta, deducted) : null, insufficientCollecta: selected === undefined && deducted ? subtractFractions(available, deducted) : null, ...(selected !== undefined ? {notSelected: deducted ? subtractFractions(available, deducted) : null} : {}), source: internationalCreditSource, countryAllocationCreated: false, carryCreated: false, officialBalancesConsumed: false};
}
+2 -2
View File
@@ -29,7 +29,7 @@ export function pecPaymentValue(r: PecPayment, subjectNuit: string, year: string
if (pecCompare(previouslyUsed, proofAmount) > 0n || pecCompare(value, subtractFractions(proofAmount, previouslyUsed)) > 0n) throw Error('PEC: parcela e utilizações anteriores excedem o comprovativo disponível.');
return {proofAmount, value, previouslyUsed};
}
export function pecDeduction(available: FiscalFraction, colecta: FiscalFraction | null, ready: boolean) {
const deducted = ready && colecta ? (pecCompare(available, colecta) > 0n ? colecta : available) : null;
export function pecDeduction(available: FiscalFraction, colecta: FiscalFraction | null, ready: boolean, selected?: FiscalFraction | null) {
const deducted = ready && colecta ? selected ?? (pecCompare(available, colecta) > 0n ? colecta : available) : null;
return {version: pecVersion, available, deducted, remaining: deducted ? subtractFractions(available, deducted) : null, afterPec: deducted && colecta ? subtractFractions(colecta, deducted) : null, source: pecSource, officialBalancesConsumed: false, carryCreated: false};
}
@@ -0,0 +1,82 @@
<?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 IrpcCreditCarryDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_prior_credit_details_round_trip_with_frozen_history_and_preserved_records(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test', $target->name);
$this->assertSame(55441, (int) $target->port);
$snapshot = static fn (string $table): array => collect(DB::select('select id::text as id,md5(row_to_json(t)::text) as hash from public."'.$table.'" t order by id'))->pluck('hash', 'id')->all();
$protected = [];
foreach (['opportunities', 'documents', 'opportunity_documents', 'simulation_form_drafts', 'users', 'organizations', 'audit_events'] as $table) {
$protected[$table] = $snapshot($table);
}
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::findOrFail(1021));
$fixture = json_decode(file_get_contents(base_path('tests/frontend/fixtures/irpc-carry-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
$this->assertTrue($fixture['synthetic']);
$this->assertFalse($fixture['actualBalances']);
$id = null;
$etag = null;
foreach ($fixture['cases'] as $name => $draft) {
$personal = ['person_type' => $draft['person_type']];
unset($draft['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $draft];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$this->assertSame($draft['business_plan_snapshot'], $back['business_plan_snapshot']);
$original = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$recovered = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
foreach (['irpcNormal', 'irpcWithholdings', 'books', 'currencyEvents', 'ispcQuarters'] as $map) {
$this->assertSame($original[$map], $recovered[$map]);
}
$book = $recovered['irpcAnnual'][$draft['context_opportunity']];
$model = $book['result']['model'];
$this->assertSame($original['irpcAnnual'][$draft['context_opportunity']], $book);
$this->assertCount(1, $book['history']);
$this->assertSame('IRPC-credit-carry/1', $model['creditCarry']['version']);
$this->assertFalse($model['creditCarry']['carryCreated']);
$this->assertFalse($model['creditCarry']['officialBalancesConsumed']);
$this->assertFalse($model['submitted']);
if (in_array($name, ['expired', 'missing-use', 'overallocated'], true)) {
$this->assertSame('indeterminate', $model['status']);
$this->assertNull($model['arithmeticBalance']);
$this->assertNull($model['internationalCredit']['deducted']);
$this->assertNull($model['pecDeduction']['deducted']);
} else {
$expected = ['payable' => ['550', '1'], 'fractional' => ['1822', '3'], 'recoverable' => ['-500', '1']][$name];
$this->assertEmpty($model['pending']);
$this->assertSame(['numerator' => $expected[0], 'denominator' => $expected[1]], $model['arithmeticBalance']);
}
if (getenv('INVESTMOZ_CARRY_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-carry-20261028/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
$preserved = [];
foreach ($protected as $table => $before) {
$this->assertSame($before, array_intersect_key($snapshot($table), $before), 'Existing records changed: '.$table);
$preserved[$table] = ['existing_rows' => count($before), 'unchanged' => true];
}
if (getenv('INVESTMOZ_CARRY_QA_OUTPUT') === '1') {
file_put_contents(base_path('outputs/irpc-l8-carry-20261028/existing-records-preservation.json'), json_encode(['database' => $target->name, 'tables' => $preserved, 'transaction_rollback' => true], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT));
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
import {internationalCase} from './irpc-international-case.mjs';
import {prepareCreditCarry} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newCarryLot} from '../../../resources/js/lib/irpc-credit-carry.ts';
export function carryCase() {
const c = internationalCase();
c.b = prepareCreditCarry(c.b);
Object.assign(c.b.creditCarry, {
state: 'known', reference: 'SYNTHETIC QA complete prior balance inventory',
currentInternational: '8', currentPec: '200', assessmentKey: c.b.assessmentKey,
allocationReference: 'SYNTHETIC QA explicit allocation: current international8, prior international50, currentPEC200, priorPEC100',
lots: [
{...newCarryLot('international-2021'), kind: 'international', country: 'ZA', subjectNuit: c.b.subjectNuit,
year: '2021', ordinary: 'yes', originDocument: 'SYNTHETIC foreign-credit-assessment2021',
originReference: 'SYNTHETIC QA origin inclusion, eligible tax, national/CDT limits, insufficient collecta',
ruleReference: 'SYNTHETIC QA documented ordinary origin rule art65, no real foreign treaty claim',
eligible: '200', originDeducted: '100', insufficient: 'yes', usesState: 'known',
usesReference: 'SYNTHETIC QA all actual uses reconciled through current exercise',
uses: [{id: 'use-2023', year: '2023', amount: '30', document: 'SYNTHETIC use2023', reference: 'SYNTHETIC QA actual documented use, not simulated'}], selected: '50'},
{...newCarryLot('pec-2023'), kind: 'pec', subjectNuit: c.b.subjectNuit,
year: '2023', ordinary: 'yes', originDocument: 'SYNTHETIC PEC2023 assessment and receipts',
originReference: 'SYNTHETIC QA actual PEC paid, initial deduction and collecta insufficiency',
ruleReference: 'SYNTHETIC QA origin ordinary art66', eligible: '300', originDeducted: '150',
insufficient: 'yes', usesState: 'absent', usesReference: 'SYNTHETIC QA no actual subsequent use', selected: '100'},
],
});
return c;
}
+124
View File
@@ -0,0 +1,124 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {carryCase} from './helpers/irpc-carry-case.mjs';
import {internationalCase} from './helpers/irpc-international-case.mjs';
import {newCarryUse, calculateCarryLot} from '../../resources/js/lib/irpc-credit-carry.ts';
import {calculateAnnual, prepareCreditCarry, recordAnnual, annualBook, annualCurrent, saveAnnualBook, annualHtml, annualReportRows} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const number = v => v ? Number(v.numerator) / Number(v.denominator) : null;
const calc = c => calculateAnnual(c.d, c.b);
const lot = (c, i = 0) => c.b.creditCarry.lots[i];
const pending = c => {const m = calc(c); assert.equal(m.status, 'indeterminate', JSON.stringify(m.pending)); assert.equal(m.arithmeticBalance, null); assert.equal(m.internationalCredit.deducted, null); assert.equal(m.pecDeduction.deducted, null); return m;};
test('2021 international and2023 PEC remain eligible in2026; origin deductions/uses count once:1408-8-50-200-100-500=550', () => {
const m = calc(carryCase()); assert.deepEqual(m.pending, []); assert.equal(number(m.payable), 550);
assert.equal(number(m.creditCarry.lots[0].opening), 100); assert.equal(number(m.creditCarry.lots[0].used), 30);
assert.equal(number(m.creditCarry.lots[0].available), 70); assert.equal(number(m.creditCarry.lots[0].remaining), 20);
assert.equal(m.creditCarry.lots[0].lastYear, 2026); assert.equal(m.creditCarry.lots[1].lastYear, 2026);
assert.equal(number(m.creditCarry.lots[1].remaining), 50); assert.equal(m.creditCarry.carryCreated, false); assert.equal(m.creditCarry.officialBalancesConsumed, false);
});
test('five subsequent exercises differs from PEC three; expired source retained with zero selection', () => {
for (const [i, year] of [[0, '2020'], [1, '2022']]) {
const c = carryCase(); lot(c, i).year = year;
if (i === 0) lot(c).uses[0].year = '2023';
assert.match(pending(c).pending.join(' '), /Prazo ordinário terminado/);
lot(c, i).selected = '0'; const m = calc(c);
assert.deepEqual(m.pending, []); assert.equal(m.creditCarry.lots[i].expired, true);
assert.equal(number(m.creditCarry.lots[i].available), 0); assert.ok(number(m.creditCarry.lots[i].remaining) > 0);
}
});
test('future/current origin, nonordinary periods, currency and wrong subject do not create an eligible carry', () => {
for (const patch of [{year: '2026'}, {year: '2027'}, {year: ''}, {ordinary: ''}, {ordinary: 'no'}, {currency: 'USD'}, {subjectNuit: '999999999'}, {country: 'MZ'}, {country: 'ZZ'}, {kind: ''}]) {
const c = carryCase(); Object.assign(lot(c), patch); pending(c);
}
const c = carryCase(); lot(c, 1).country = 'ZA'; pending(c);
});
test('origin eligibility, insufficient collecta and applicable rule require concrete evidence', () => {
for (const patch of [{originDocument: ''}, {originReference: ''}, {ruleReference: ''}, {insufficient: ''}, {insufficient: 'no'}, {eligible: ''}, {originDeducted: ''}, {originDeducted: '201'}, {selected: ''}, {selected: '70.01'}]) {
const c = carryCase(); Object.assign(lot(c), patch); pending(c);
}
});
test('unknown uses are not zero; every actual use must fit its origin/window and documentary identity', () => {
for (const patch of [{usesState: ''}, {usesReference: ''}, {usesState: 'absent'}, {uses: []}, {uses: [{...newCarryUse('u'), year: '2021', amount: '1', document: 'D', reference: 'R'}]}]) {
const c = carryCase(); Object.assign(lot(c), patch); pending(c);
}
for (const patch of [{year: '2027'}, {amount: '101'}, {document: ''}, {reference: ''}, {amount: ''}]) {
const c = carryCase(); Object.assign(lot(c).uses[0], patch); pending(c);
}
const c = carryCase(); lot(c).uses.push({...lot(c).uses[0], id: 'copy', year: '2024', document: ' synthetic USE2023 '});
assert.match(pending(c).pending.join(' '), /repetido/);
});
test('actual current-year use is subtracted before the proposed new deduction', () => {
const c = carryCase(); lot(c).uses.push({id: 'current-actual', year: '2026', amount: '10', document: 'SYNTHETIC current actual use', reference: 'SYNTHETIC actual assessment, not this simulation'});
const m = calc(c); assert.equal(number(m.creditCarry.lots[0].available), 60); assert.equal(number(m.creditCarry.lots[0].remaining), 10); assert.equal(number(m.payable), 550);
});
test('original country/nature/year cannot be duplicated by changing document or id', () => {
const c = carryCase(); c.b.creditCarry.lots.push({...structuredClone(lot(c)), id: 'other', originDocument: 'OTHER', uses: [] , usesState: 'absent'});
assert.match(pending(c).pending.join(' '), /mesma natureza/);
});
test('cross-context duplicate origin blocks and external edits invalidate saved current result', () => {
const c = carryCase(); c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b));
const b = annualBook(c.d); assert.equal(annualCurrent(c.d, b), true);
const s = JSON.parse(c.d.modelo_a_snapshot), other = structuredClone(c.b);
other.creditCarry.lots = [{...structuredClone(lot(c)), id: 'copied-origin', originDocument: 'OTHER', uses: [], usesState: 'absent'}];
s.irpcAnnual['opportunity:other'] = other; c.d.modelo_a_snapshot = JSON.stringify(s);
assert.equal(annualCurrent(c.d, b), false); assert.match(pending(c).pending.join(' '), /noutro mapa/);
});
test('explicit allocation can favour older balances without automatic reduction or FIFO', () => {
const c = carryCase(); Object.assign(c.b.creditCarry, {currentInternational: '0', currentPec: '100'});
const m = calc(c); assert.equal(number(m.payable), 658); assert.equal(number(m.internationalCredit.deducted), 0);
assert.equal(number(m.internationalCredit.notSelected), 8); assert.equal(m.internationalCredit.insufficientCollecta, null);
assert.equal(number(m.pecDeduction.deducted), 100); assert.equal(number(m.pecDeduction.remaining), 100);
});
test('current selection cannot exceed known availability or total collecta; planned choices are kept on failure', () => {
for (const patch of [{currentInternational: '9'}, {currentPec: '201'}, {currentInternational: ''}, {currentPec: ''}, {allocationReference: ''}, {assessmentKey: 'stale'}]) {
const c = carryCase(); Object.assign(c.b.creditCarry, patch); pending(c);
}
const c = carryCase(); lot(c).eligible = '5000'; lot(c).selected = '1400';
const before = structuredClone(c.b); assert.match(pending(c).pending.join(' '), /ultrapassam a colecta/); assert.deepEqual(c.b, before);
});
test('no credit or PEC reimbursement is generated by old balances; PPC refund stays separate', () => {
const c = carryCase(); Object.assign(c.b.creditCarry, {currentInternational: '8', currentPec: '0'}); lot(c).eligible = '2000'; lot(c).selected = '1300'; lot(c, 1).selected = '100';
const m = calc(c); assert.equal(number(m.recoverable), 500); assert.equal(number(m.pecDeduction.deducted), 0);
assert.equal(m.creditCarry.carryCreated, false); assert.equal(number(m.creditCarry.lots[0].remaining), 570);
});
test('exact foreign-credit fractions are preserved; decimal presentation is never fiscal rounding', () => {
const c = carryCase(); Object.assign(lot(c), {eligible: '2/3', originDeducted: '1/3', selected: '1/3', usesState: 'absent', uses: []});
Object.assign(c.b.creditCarry, {currentInternational: '1/3'});
const m = calc(c); assert.deepEqual(m.pending, []); assert.deepEqual(m.creditCarry.lots[0].available, {numerator: '1', denominator: '3'});
assert.deepEqual(m.payable, {numerator: '1822', denominator: '3'});
lot(c).selected = '1/0'; pending(c);
});
test('missing/absent/known-zero remain distinct; absence does not demand unused allocation fields', () => {
const c = internationalCase(), baseline = calc(c); c.b = prepareCreditCarry(c.b); pending(c);
c.b.creditCarry.state = 'absent'; c.b.creditCarry.reference = 'SYNTHETIC QA documented absence';
let m = calc(c); assert.equal(number(m.payable), number(baseline.payable)); assert.deepEqual(m.internationalCredit, baseline.internationalCredit);
c.b.creditCarry.lots.push({...carryCase().b.creditCarry.lots[0]}); pending(c);
const z = carryCase(); Object.assign(lot(z), {eligible: '0', originDeducted: '0', selected: '0', usesState: 'absent', uses: []});
m = calc(z); assert.deepEqual(m.pending, []); assert.equal(number(m.creditCarry.lots[0].available), 0);
});
test('optional preparation, recording, JSON recovery, HTML and dossier preserve accepted books and frozen history', () => {
const c = internationalCase(); c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b, '2026-10-27T17:00:00+02:00'));
const old = annualBook(c.d), frozen = structuredClone(old.result), existing = JSON.parse(c.d.modelo_a_snapshot), plan = c.d.business_plan_snapshot;
const next = prepareCreditCarry(old); next.creditCarry = carryCase().b.creditCarry;
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, next, '2026-10-28T12:00:00+02:00'));
const recovered = annualBook(JSON.parse(JSON.stringify(c.d))); assert.deepEqual(recovered.history, [frozen]); assert.equal(number(recovered.result.model.payable), 550);
assert.equal(annualCurrent(c.d, recovered), true); assert.equal(c.d.business_plan_snapshot, plan);
const after = JSON.parse(c.d.modelo_a_snapshot); delete after.irpcAnnual; delete existing.irpcAnnual; assert.deepEqual(after, existing);
assert.match(annualHtml(c.d), /Saldo anterior — Crédito internacional ZA — 2021/);
assert.match(annualHtml(c.d), /550.00/); assert.match(annualHtml(c.d), /2026/);
assert.match(JSON.stringify(createProjectDossierReport(c.d, {selectedSections: new Set(['finance'])})), /Afectação escolhida entre exercícios/);
recovered.creditCarry.lots[0].selected = '49'; c.d.modelo_a_snapshot = saveAnnualBook(c.d, recovered);
assert.equal(annualCurrent(c.d, recovered), false); assert.deepEqual(recovered.result.model, annualBook(c.d).result.model);
assert.throws(() => annualHtml(c.d), /fontes actuais/); assert.match(annualReportRows(c.d).flat().join(' '), /Histórico/);
});
test('malformed detail and duplicate internal ids cannot overwrite original snapshots', () => {
for (const mutate of [c => c.b.creditCarry.lots[0].uses = null, c => c.b.creditCarry.version = 2, c => c.b.creditCarry.lots[0].uses[0].amount = null, c => c.b.creditCarry.lots[1].id = c.b.creditCarry.lots[0].id]) {
const c = carryCase(), before = c.d.modelo_a_snapshot; mutate(c);
assert.throws(() => saveAnnualBook(c.d, c.b), /recuper|repetido/); assert.equal(c.d.modelo_a_snapshot, before);
}
});
test('standalone expired PEC keeps origin and is not silently converted to international five-year window', () => {
const c = carryCase(), r = {...lot(c, 1), year: '2022', selected: '0'};
const m = calculateCarryLot(r, c.b.subjectNuit, c.b.year); assert.equal(m.expired, true); assert.equal(m.lastYear, 2025); assert.equal(number(m.remaining), 150);
});
+23
View File
@@ -0,0 +1,23 @@
import fs from 'node:fs';
import {carryCase} from '../tests/frontend/helpers/irpc-carry-case.mjs';
import {internationalCase} from '../tests/frontend/helpers/irpc-international-case.mjs';
import {recordAnnual, saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
const cases = {};
for (const name of ['payable', 'expired', 'missing-use', 'overallocated', 'fractional', 'recoverable']) {
const c = carryCase(), previous = internationalCase();
const frozen = recordAnnual(previous.d, previous.b, '2026-10-27T17:00:00+02:00').result;
c.b.history = [frozen];
const l = c.b.creditCarry.lots[0];
if (name === 'expired') l.year = '2020';
if (name === 'missing-use') l.usesState = '';
if (name === 'overallocated') {l.eligible = '5000'; l.selected = '1400';}
if (name === 'fractional') {
Object.assign(l, {eligible: '2/3', originDeducted: '1/3', selected: '1/3', usesState: 'absent', uses: []});
c.b.creditCarry.currentInternational = '1/3';
}
if (name === 'recoverable') {l.eligible = '2000'; l.selected = '1300'; c.b.creditCarry.currentPec = '0';}
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b, '2026-10-28T12:00:00+02:00'));
cases[name] = c.d;
}
fs.writeFileSync(new URL('../tests/frontend/fixtures/irpc-carry-preserved.json', import.meta.url), JSON.stringify({synthetic: true, actualBalances: false, cases}));
console.log('Prepared six synthetic carry API cases with frozen international-credit history.');
+27
View File
@@ -0,0 +1,27 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {annualBook, annualCurrent, calculateAnnual, annualHtml} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const output = new URL('../outputs/irpc-l8-carry-20261028/', import.meta.url);
const receipts = [];
for (const name of ['payable', 'expired', 'missing-use', 'overallocated', 'fractional', 'recoverable']) {
const d = JSON.parse(fs.readFileSync(new URL('recovered-' + name + '.json', output), 'utf8'));
const b = annualBook(d), m = b.result.model, fresh = calculateAnnual(d, b);
assert.equal(annualCurrent(d, b), true);
assert.deepEqual(fresh, m);
assert.equal(b.history.length, 1);
assert.equal(b.history[0].model.payable.numerator, '700');
const html = annualHtml(d);
const dossier = createProjectDossierReport(d, {selectedSections: new Set(['finance']), generatedAt: '2026-10-28T12:00:00+02:00'});
assert.match(html, /Afectação escolhida entre exercícios/);
assert.match(JSON.stringify(dossier), /Saldo anterior — PEC — 2023/);
assert.match(html, /último exercício ordinário/);
if (name === 'payable') assert.match(html, /550.00/);
if (name === 'expired') {assert.equal(m.creditCarry.lots[0].expired, true); assert.equal(m.status, 'indeterminate');}
if (name === 'fractional') assert.deepEqual(m.arithmeticBalance, {numerator: '1822', denominator: '3'});
fs.writeFileSync(new URL('report-' + name + '.html', output), html);
fs.writeFileSync(new URL('dossier-' + name + '.json', output), JSON.stringify(dossier));
receipts.push({name, status: m.status, balance: m.arithmeticBalance, apiRecovered: true, recalculationEqual: true, html: true, dossier: true, frozenHistoryPreserved: true});
}
fs.writeFileSync(new URL('recovered-report-receipt.json', output), JSON.stringify({synthetic: true, activeChanged: false, receipts}, null, 2));
console.log(JSON.stringify(receipts, null, 2));