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

This commit is contained in:
2026-10-28 14:29:55 +02:00
parent 6ededb597a
commit aae67c169d
21 changed files with 882 additions and 37 deletions
@@ -0,0 +1,10 @@
import type {FiscalProofParcel} from '@/lib/fiscal-proof-parcel';
export function FiscalProofParcelFields({value, change, label}: {value: FiscalProofParcel; change: (value: FiscalProofParcel) => void; label: string}) {
return <details className="rounded border p-2" open={!!value.parcel || !!value.parcelReference}>
<summary>Identificar parcela quando o documento suporta várias parcelas — {label}</summary>
<p>Sem identificação específica, o comprovativo existente abrange o documento inteiro. Para distinguir impostos ou parcelas, indique a linha/identificador exacto e o suporte dessa separação. Os montantes comprovado e seleccionado devem corresponder à parcela identificada. A mudança do identificador não comprova uma parcela diferente.</p>
<label className="block">Identificador da parcela<input aria-label={label + ' — Parcela documental'} value={value.parcel ?? ''} onChange={e => change({parcel: e.target.value})}/></label>
<label className="block">Suporte da separação das parcelas<input aria-label={label + ' — Suporte da parcela'} value={value.parcelReference ?? ''} onChange={e => change({parcelReference: e.target.value})}/></label>
</details>;
}
@@ -1,3 +1,5 @@
import {SimulationIrpcEconomic} from './simulation-irpc-economic';
import {FiscalProofParcelFields} from './fiscal-proof-parcel';
import {SimulationIrpcCarry} from './simulation-irpc-carry';
import {SimulationIrpcInternational} from './simulation-irpc-international';
import {SimulationIrpcPec} from './simulation-irpc-pec';
@@ -18,10 +20,10 @@ export function SimulationIrpcAnnual({draft,onChange}:Props){
<p>Colecta disponível:{a.colecta?a.colecta.numerator+'/'+a.colecta.denominator+' MZN':'Indeterminada'};origem:{a.kind==='special'?'L7 qualificado':'L6 normal'};NUIT:{a.subjectNuit||'Por identificar'};exercício:{a.year||'Por identificar'}.</p><button type="button" onClick={()=>{try{save(reuseAnnualAssessment(draft,b));}catch(e){setError(String(e));}}}>Reutilizar colecta corrente L6/L7</button>{field('assessmentReference','Suporte da colecta reutilizada L8')}
<fieldset className="space-y-2 rounded border p-3"><legend>Retenções efectivamente sofridas por conta</legend>{group('withholdings','Retenções sofridas')}
{options.length===0?<p>Não há afectações de comprovativosL5 neste contexto. Reutilize e reconcilie a retenção recebida emL5 ou declare ausência com suporte.</p>:options.map(o=><div key={o.rowId+'|'+o.paymentId} className="rounded border p-2"><p>{o.document||'Prova por identificar'};emitente:{o.issuer};data:{o.date};parcelaL5:{o.amount}MZN;{o.reason||'Retenção recebida/efectiva por conta reconciliada emL5.'}</p><button type="button" disabled={!o.ready||b.withholdings.rows.some(r=>r.rowId===o.rowId&&r.paymentId===o.paymentId)} onClick={()=>{try{save(reuseAnnualWithholding(draft,b,o.rowId,o.paymentId,crypto.randomUUID()));}catch(e){setError(String(e));}}}>Seleccionar prova L5 {o.document||o.paymentId}</button></div>)}
{b.withholdings.rows.map((r,index)=>{const change=(p:Partial<typeof r>)=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>Retenção seleccionada {index+1}</legend><p>Rendimento:{r.rowId};prova:{r.paymentId};NUIT:{r.subjectNuit};exercício:{r.year}. Fonte conservada. Se mudou, retire esta selecção e seleccione a prova actual. O histórico mantém-se.</p><label className="block">Parcela a abater<input aria-label={'Parcela da retenção L8 '+(index+1)} value={r.amount} onChange={e=>change({amount:e.target.value})}/></label><label className="block">O próprio sujeito é beneficiário e o rendimento está incluído na base deste exercício<select aria-label={'Rendimento incluído L8 '+(index+1)} value={r.incomeIncluded} onChange={e=>change({incomeIncluded:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — crédito não aplicável neste fecho</option></select></label><label className="block">Suporte da titularidade e inclusão<input aria-label={'Suporte da retenção L8 '+(index+1)} value={r.reference} onChange={e=>change({reference:e.target.value})}/></label><button type="button" onClick={()=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.filter(x=>x.id!==r.id)}})}>Retirar selecção L8 da retenção {index+1}</button></fieldset>;})}</fieldset>
{b.withholdings.rows.map((r,index)=>{const change=(p:Partial<typeof r>)=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>Retenção seleccionada {index+1}</legend><p>Rendimento:{r.rowId};prova:{r.paymentId};NUIT:{r.subjectNuit};exercício:{r.year}. Fonte conservada. Se mudou, retire esta selecção e seleccione a prova actual. O histórico mantém-se.</p><label className="block">Parcela a abater<input aria-label={'Parcela da retenção L8 '+(index+1)} value={r.amount} onChange={e=>change({amount:e.target.value})}/></label><label className="block">O próprio sujeito é beneficiário e o rendimento está incluído na base deste exercício<select aria-label={'Rendimento incluído L8 '+(index+1)} value={r.incomeIncluded} onChange={e=>change({incomeIncluded:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — crédito não aplicável neste fecho</option></select></label><label className="block">Suporte da titularidade e inclusão<input aria-label={'Suporte da retenção L8 '+(index+1)} value={r.reference} onChange={e=>change({reference:e.target.value})}/></label><FiscalProofParcelFields value={r} change={change} label={"Retenção L8 "+(index+1)}/><button type="button" onClick={()=>patch({withholdings:{...b.withholdings,rows:b.withholdings.rows.filter(x=>x.id!==r.id)}})}>Retirar selecção L8 da retenção {index+1}</button></fieldset>;})}</fieldset>
<fieldset className="space-y-2 rounded border p-3"><legend>PPC efectivamente pagos</legend>{group('ppc','PPC efectuados')}<p>Regista comprovativos de pagamentos por conta; não calcula automaticamente a obrigação PPC nem usa pagamentos previstos no plano.</p><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:[...b.ppc.rows,{...newAnnualPpc(crypto.randomUUID()),subjectNuit:b.subjectNuit,year:b.year}]}})}>Registar comprovativo PPC</button>
{b.ppc.rows.map((r,index)=>{const change=(p:Partial<AnnualPpc>)=>patch({ppc:{...b.ppc,rows:b.ppc.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>PPC {index+1}</legend>{([['document','Comprovativo'],['issuer','Emitente'],['date','Data (AAAA-MM-DD)'],['subjectNuit','NUIT beneficiário'],['year','Exercício do comprovativo'],['currency','Moeda do comprovativo'],['proofAmount','Montante comprovado'],['amount','Parcela a abater'],['reference','Suporte do pagamento e afectação']]as const).map(([key,label])=><label key={key} className="block">{label}<input aria-label={'PPC L8 '+(index+1)+' — '+label} value={r[key]} onChange={e=>change({[key]:e.target.value})}/></label>)}<label className="block">Pagamento efectivamente comprovado<select aria-label={'PPC L8 '+(index+1)+' — Efectivo'} value={r.effective} onChange={e=>change({effective:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — comprovativo reconciliado</option><option value="no">Não — apenas previsão/outro estado</option></select></label><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:b.ppc.rows.filter(x=>x.id!==r.id)}})}>Retirar PPC {index+1}</button></fieldset>;})}</fieldset>
<SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/><SimulationIrpcCarry book={b} save={save}/>{([...(b.pecPayments?[]:[['pec','PEC']as const]),...(b.internationalCredits?[]:[['credits','Créditos com regras próprias']as const])]).map(([key,label])=><fieldset key={key} className="rounded border p-3"><legend>{label} — tratamento separado</legend><label className="block">Estado — {label}<select aria-label={'Estado L8 — '+label} value={b[key].state} onChange={e=>patch({[key]:{...b[key],state:e.target.value as ''|'known'|'absent'}})}><option value="">Informação em falta</option><option value="absent">Ausência declarada com suporte</option><option value="known">Valor conhecido — positivo exige regra própria</option></select></label>{(b[key].state==='known'||b[key].amount)&&<label className="block">Montante declarado<input aria-label={'Montante L8 — '+label} value={b[key].amount} onChange={e=>patch({[key]:{...b[key],amount:e.target.value}})}/></label>}<label className="block">Suporte<input aria-label={'Suporte L8 — '+label} value={b[key].reference} onChange={e=>patch({[key]:{...b[key],reference:e.target.value}})}/></label></fieldset>)}
{b.ppc.rows.map((r,index)=>{const change=(p:Partial<AnnualPpc>)=>patch({ppc:{...b.ppc,rows:b.ppc.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>PPC {index+1}</legend>{([['document','Comprovativo'],['issuer','Emitente'],['date','Data (AAAA-MM-DD)'],['subjectNuit','NUIT beneficiário'],['year','Exercício do comprovativo'],['currency','Moeda do comprovativo'],['proofAmount','Montante comprovado'],['amount','Parcela a abater'],['reference','Suporte do pagamento e afectação']]as const).map(([key,label])=><label key={key} className="block">{label}<input aria-label={'PPC L8 '+(index+1)+' — '+label} value={r[key]} onChange={e=>change({[key]:e.target.value})}/></label>)}<label className="block">Pagamento efectivamente comprovado<select aria-label={'PPC L8 '+(index+1)+' — Efectivo'} value={r.effective} onChange={e=>change({effective:e.target.value as ''|'yes'|'no'})}><option value="">Informação em falta</option><option value="yes">Sim — comprovativo reconciliado</option><option value="no">Não — apenas previsão/outro estado</option></select></label><FiscalProofParcelFields value={r} change={change} label={"PPC L8 "+(index+1)}/><button type="button" onClick={()=>patch({ppc:{...b.ppc,rows:b.ppc.rows.filter(x=>x.id!==r.id)}})}>Retirar PPC {index+1}</button></fieldset>;})}</fieldset>
<SimulationIrpcEconomic book={b} save={save} residentConfirmed={a.current&&a.book.scope==='resident_general'&&b.subjectNuit===a.subjectNuit&&b.year===a.year&&b.assessmentKey===a.key}/><SimulationIrpcInternational book={b} save={save}/><SimulationIrpcPec book={b} save={save}/><SimulationIrpcCarry book={b} save={save}/>{([...(b.pecPayments?[]:[['pec','PEC']as const]),...(b.internationalCredits?[]:[['credits','Créditos com regras próprias']as const])]).map(([key,label])=><fieldset key={key} className="rounded border p-3"><legend>{label} — tratamento separado</legend><label className="block">Estado — {label}<select aria-label={'Estado L8 — '+label} value={b[key].state} onChange={e=>patch({[key]:{...b[key],state:e.target.value as ''|'known'|'absent'}})}><option value="">Informação em falta</option><option value="absent">Ausência declarada com suporte</option><option value="known">Valor conhecido — positivo exige regra própria</option></select></label>{(b[key].state==='known'||b[key].amount)&&<label className="block">Montante declarado<input aria-label={'Montante L8 — '+label} value={b[key].amount} onChange={e=>patch({[key]:{...b[key],amount:e.target.value}})}/></label>}<label className="block">Suporte<input aria-label={'Suporte L8 — '+label} value={b[key].reference} onChange={e=>patch({[key]:{...b[key],reference:e.target.value}})}/></label></fieldset>)}
<button type="button" onClick={()=>{try{save(recordAnnual(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar saldo anual L8</button><button type="button" onClick={()=>{try{const html=annualHtml(draft),url=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),link=document.createElement('a');link.href=url;link.download='IRPC-L8-saldo-simulado.html';link.click();URL.revokeObjectURL(url);setError('');}catch(e){setError(String(e));}}}>Exportar saldo anual L8</button>
{annualReportRows(draft).map((r,n)=><p key={n}>{r[1]} — {r[2]} — {r[3]}</p>)}</section>;
}catch(e){return<p role="alert">PreparaçãoL8 não recuperável:{String(e)}. Originais e históricos conservados.</p>;}
@@ -0,0 +1,41 @@
import {prepareEconomicBook, type AnnualBook} from '@/lib/irpc-annual-settlement';
import {economicSource, newEconomicPosition, type EconomicPosition} from '@/lib/irpc-economic-credit';
export function SimulationIrpcEconomic({book, save, residentConfirmed}: {book: AnnualBook; save: (book: AnnualBook) => void; residentConfirmed: boolean}) {
const c = book.economicCredits;
if (!c) return <fieldset className="rounded border p-3"><legend>Crédito por dupla tributação económica</legend><p>Para lucros distribuídos por entidade moçambicana sujeita e não isenta de IRPC, distinga o imposto correspondente aos lucros da retenção sofrida e do imposto estrangeiro. Reutiliza a colecta e conserva o resumo de créditos anterior.</p><button type="button" onClick={() => save(prepareEconomicBook(book))}>Preparar crédito económico</button></fieldset>;
const update = (next: typeof c) => save({...book, economicCredits: next});
const rowChange = (r: EconomicPosition) => update({...c, rows: c.rows.map(x => x.id === r.id ? r : x)});
return <fieldset className="space-y-3 rounded border p-3"><legend>Crédito económico — lucros distribuídos</legend>
<p>{economicSource}</p>
<p>O detalhe substitui nesta execução o resumo económico anterior, que permanece conservado: {book.internationalCredits?.economic.state || 'informação em falta'}; montante {book.internationalCredits?.economic.amount || 'não preenchido'}. Os benefícios e o crédito internacional mantêm os seus estados próprios.</p>
<label className="block">Estado do crédito económico<select aria-label="Estado do crédito económico L8" value={c.state} onChange={e => update({...c, state: e.target.value as typeof c.state})}><option value="">Informação em falta</option><option value="known">Lucros e IRPC conhecidos</option><option value="absent">Ausência documentada</option></select></label>
<label className="block">Suporte do inventário económico<input aria-label="Suporte económico L8" value={c.reference} onChange={e => update({...c, reference: e.target.value})}/></label>
{(c.state === 'known' || c.rows.length > 0) && <button type="button" onClick={() => update({...c, state: 'known', rows: [...c.rows, {...newEconomicPosition(crypto.randomUUID()), subjectNuit: book.subjectNuit, year: book.year, recipientResident: residentConfirmed ? 'yes' : ''}]})}>Registar lucros distribuídos</button>}
{c.rows.map((r, i) => {
const patch = (v: Partial<EconomicPosition>) => rowChange({...r, ...v});
const prefix = 'Crédito económico ' + (i + 1) + ' — ';
const field = (key: Exclude<keyof EconomicPosition, 'previousUse'>, label: string) => <label key={key} className="block">{label}<input aria-label={prefix + label} value={r[key]} onChange={e => patch({[key]: e.target.value})}/></label>;
const choice = (key: 'recipientResident' | 'payerResident' | 'payerIrpc' | 'payerExempt' | 'article40' | 'incomeIncluded', label: string) => <label key={key} className="block">{label}<select aria-label={prefix + label} value={r[key]} onChange={e => patch({[key]: e.target.value})}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — documentado</option></select></label>;
const outside = !!r.kind && r.kind !== 'ordinary_profit' || r.article40 === 'yes';
const detail = <>
{([['distributionDocument', 'Documento da distribuição'], ['distributionParcel', 'Parcela dos lucros no documento'], ['distributionDate', 'Data da distribuição (AAAA-MM-DD)'], ['income', 'Lucros incluídos antes do crédito — MZN, ilíquidos de retenção'], ['incomeReference', 'Suporte da inclusão e do valor ilíquido na base corrente'], ['taxIssuer', 'Emitente do comprovativo de IRPC correspondente'], ['taxDocument', 'Documento do IRPC correspondente aos lucros'], ['taxParcel', 'Parcela desse IRPC no documento'], ['parcelReference', 'Suporte da identificação da parcela de imposto'], ['currency', 'Moeda comprovada'], ['proofAmount', 'IRPC comprovado nesta parcela — MZN'], ['taxAmount', 'IRPC correspondente aos lucros seleccionados — MZN'], ['attributionReference', 'Suporte da correspondência entre imposto e lucros'], ['previouslyAttributed', 'IRPC desta parcela já atribuído a outros lucros — MZN'], ['priorAttributionReference', 'Suporte das atribuições anteriores, incluindo zero conhecido'], ['grossUpIncluded', 'Crédito nominal já acrescido à base (art.53) — MZN'], ['grossUpReference', 'Suporte do acréscimo art.53 já incluído em L6'], ['nationalLimit', 'Quota proporcional da colecta após acréscimo — MZN'], ['nationalLimitReference', 'Suporte da quota proporcional da colecta']] as const).map(([key, label]) => field(key, label))}
{choice('incomeIncluded', 'Lucros ilíquidos e acréscimo incluídos na base corrente')}
<p>O cálculo confere 60% do IRPC correspondente com o acréscimo já incluído em L6 e a quota proporcional documentada. Se a base estiver incompleta, reconcilie-a no percurso existente. Esta acção conserva os valores e não altera a colecta automaticamente.</p>
<button type="button" onClick={() => patch({assessmentKey: book.assessmentKey})}>Confirmar inclusão e quota na colecta seleccionada</button>
<label className="block">Utilização anterior efectiva do crédito<select aria-label={prefix + 'Estado do uso anterior'} value={r.previousUse.state} onChange={e => patch({previousUse: {...r.previousUse, state: e.target.value as EconomicPosition['previousUse']['state']}})}><option value="">Informação em falta</option><option value="known">Montante conhecido</option><option value="absent">Ausência documentada</option></select></label>
{(r.previousUse.state === 'known' || r.previousUse.amount) && <label className="block">Crédito já utilizado — MZN<input aria-label={prefix + 'Uso anterior MZN'} value={r.previousUse.amount} onChange={e => patch({previousUse: {...r.previousUse, amount: e.target.value}})}/></label>}
<label className="block">Suporte do uso anterior ou da sua ausência<input aria-label={prefix + 'Suporte do uso anterior'} value={r.previousUse.reference} onChange={e => patch({previousUse: {...r.previousUse, reference: e.target.value}})}/></label>
</>;
return <fieldset key={r.id} className="space-y-2 rounded border p-3"><legend>Lucros {i + 1}</legend>
{field('subjectNuit', 'NUIT beneficiário')}{field('payerNuit', 'NUIT distribuidor')}{field('year', 'Exercício')}
<label className="block">Natureza<select aria-label={prefix + 'Natureza'} value={r.kind} onChange={e => patch({kind: e.target.value as EconomicPosition['kind']})}><option value="">Por identificar</option><option value="ordinary_profit">Lucros ordinários — art.64(1–2)</option><option value="liquidation">Partilha em liquidação — art.64(3)</option><option value="association">Associação — art.64(4)</option></select></label>
{residentConfirmed && r.recipientResident === 'yes' ? <p>Residência do beneficiário reutilizada da colecta corrente no regime geral. NUIT e exercício conservados.</p> : choice('recipientResident', 'Beneficiário com sede/direcção efectiva em Moçambique')}{choice('payerResident', 'Distribuidor com sede/direcção efectiva em Moçambique')}{choice('payerIrpc', 'Distribuidor sujeito a IRPC')}{choice('payerExempt', 'Distribuidor isento de IRPC')}{field('qualificationReference', 'Suporte da qualificação das entidades e dos lucros')}
{choice('article40', 'Esta parcela está abrangida pela exclusão do art.40(1)')}{field('article40Reference', 'Suporte da correspondência do art.40(1) e seu período')}
<p>O art.40(1), na redacção de 2009, contempla as entidades e condições próprias, incluindo participação directa de pelo menos20% e prazo de dois anos, nos termos da fonte. Uma parcela excluída da base não recebe também o crédito64. A simples percentagem de participação não decide todas as condições.</p>
{outside ? <><p>Esta parcela exige o tratamento específico identificado. O crédito ordinário não é aplicado; os valores introduzidos ficam conservados.</p><details><summary>Consultar ou reconciliar dados conservados</summary>{detail}</details></> : detail}
<button type="button" onClick={() => update({...c, rows: c.rows.filter(x => x.id !== r.id)})}>Retirar lucros {i + 1}</button>
</fieldset>;
})}
</fieldset>;
}
@@ -1,3 +1,4 @@
import {FiscalProofParcelFields} from './fiscal-proof-parcel';
import CountrySelect, {type Country} from '@/components/country-select';
import {countryIsoCodes} from '@/lib/country-iso-codes';
import {newInternationalProof, prepareInternationalBook, type AnnualBook} from '@/lib/irpc-annual-settlement';
@@ -22,9 +23,9 @@ export function SimulationIrpcInternational({book, save}: {book: AnnualBook; sav
<p>Reúna os comprovativos de cada país numa posição por exercício. Indique a fracção nacional e a convenção conforme o suporte do caso. Limites admitem decimal ou fracção exacta n/d; a apresentação não constitui arredondamento fiscal. Valores MZN exigem correspondência documental; uma previsão de câmbio ou de pagamento não comprova o crédito.</p>
<label className="block">Estado internacional<select aria-label="Estado do crédito internacional L8" value={c.state} onChange={e => update({...c, state: e.target.value as InternationalCredits['state']})}><option value="">Informação em falta</option><option value="known">Posições conhecidas</option><option value="absent">Ausência declarada</option></select></label>
<label className="block">Suporte do inventário<input aria-label="Suporte do crédito internacional L8" value={c.reference} onChange={e => update({...c, reference: e.target.value})}/></label>
<Evidence label="Crédito económico — regra própria" value={c.economic} change={economic => update({...c, economic})}/>
{book.economicCredits ? <p>Resumo económico anterior conservado: {c.economic.state || "Informação em falta"}; {c.economic.amount || "Montante não preenchido"}. Nesta execução é usado o detalhe económico acima.</p> : <Evidence label="Crédito económico — regra própria" value={c.economic} change={economic => update({...c, economic})}/>}
<Evidence label="Benefícios/créditos próprios — regra própria" value={c.benefits} change={benefits => update({...c, benefits})}/>
<p>Créditos económicos e benefícios positivos exigem regras próprias; informação em falta mantém o saldo dependente indeterminado.</p>
<p>{book.economicCredits ? "Benefícios positivos exigem regras próprias; o crédito económico tem o seu detalhe separado. Informação em falta mantém o saldo dependente indeterminado." : "Créditos económicos e benefícios positivos exigem regras próprias; informação em falta mantém o saldo dependente indeterminado."}</p>
<button type="button" onClick={() => update({...c, state: 'known', positions: [...c.positions, {...newInternationalPosition(crypto.randomUUID()), subjectNuit: book.subjectNuit, year: book.year, assessmentKey: book.assessmentKey}]})}>Registar posição internacional</button>
{c.positions.map((p, index) => {
const patch = (v: Partial<InternationalPosition>) => positionChange({...p, ...v});
@@ -47,7 +48,7 @@ export function SimulationIrpcInternational({book, save}: {book: AnnualBook; sav
{(r.exerciseReference || r.date && !r.date.startsWith(p.year + '-')) && <label className="block">Suporte da correspondência da data ao exercício<input aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — Correspondência temporal'} value={r.exerciseReference} onChange={e => change({exerciseReference: e.target.value})}/></label>}
<label className="block">Pagamento efectivo<select aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — Efectivo'} value={r.effective} onChange={e => change({effective: e.target.value as InternationalTaxProof['effective']})}><option value="">Informação em falta</option><option value="yes">Sim — comprovado</option><option value="no">Não — previsão/outra situação</option></select></label>
<label className="block">Imposto sobre o rendimento elegível<select aria-label={'Imposto estrangeiro ' + (index + 1) + '.' + (ri + 1) + ' — Natureza'} value={r.incomeTax} onChange={e => change({incomeTax: e.target.value as InternationalTaxProof['incomeTax']})}><option value="">Informação em falta</option><option value="yes">Sim — natureza documentada</option><option value="no">Não — outra natureza, sem crédito presumido</option></select></label>
<button type="button" onClick={() => patch({payments: p.payments.filter(x => x.id !== r.id)})}>Retirar imposto estrangeiro {index + 1}.{ri + 1}</button>
<FiscalProofParcelFields value={r} change={change} label={"Imposto estrangeiro " + (index + 1) + "." + (ri + 1)}/><button type="button" onClick={() => patch({payments: p.payments.filter(x => x.id !== r.id)})}>Retirar imposto estrangeiro {index + 1}.{ri + 1}</button>
</fieldset>;
})}
<button type="button" onClick={() => update({...c, positions: c.positions.filter(x => x.id !== p.id)})}>Retirar posição internacional {index + 1}</button>
@@ -1,3 +1,4 @@
import {FiscalProofParcelFields} from './fiscal-proof-parcel';
import {newPecPayment, pecSource, type PecPayment} from '@/lib/irpc-pec';
import type {AnnualBook} from '@/lib/irpc-annual-settlement';
@@ -15,7 +16,7 @@ export function SimulationIrpcPec({book, save}: {book: AnnualBook; save: (book:
return <fieldset key={r.id} className="rounded border p-2"><legend>PEC {index + 1}</legend>
{([['document', 'Comprovativo PEC'], ['issuer', 'Emitente'], ['date', 'Data (AAAA-MM-DD)'], ['subjectNuit', 'NUIT beneficiário'], ['year', 'Exercício de origem'], ['currency', 'Moeda'], ['proofAmount', 'Montante comprovado'], ['previouslyUsed', 'Já utilizado fora destas parcelas'], ['priorUseReference', 'Suporte das utilizações anteriores ou do zero conhecido'], ['amount', 'Parcela disponível seleccionada'], ['reference', 'Suporte do pagamento PEC e afectação']] as const).map(([key, label]) => <label key={key} className="block">{label}<input aria-label={'PEC L8 ' + (index + 1) + ' — ' + label} value={r[key]} onChange={e => change({[key]: e.target.value})}/></label>)}
<label className="block">Pagamento PEC efectivamente comprovado<select aria-label={'PEC L8 ' + (index + 1) + ' — Efectivo'} value={r.effective} onChange={e => change({effective: e.target.value as PecPayment['effective']})}><option value="">Informação em falta</option><option value="yes">Sim — natureza PEC e pagamento reconciliados</option><option value="no">Não — previsão ou outra natureza</option></select></label>
<button type="button" onClick={() => update({...detail, rows: detail.rows.filter(x => x.id !== r.id)})}>Retirar selecção PEC {index + 1}</button>
<FiscalProofParcelFields value={r} change={change} label={"PEC L8 " + (index + 1)}/><button type="button" onClick={() => update({...detail, rows: detail.rows.filter(x => x.id !== r.id)})}>Retirar selecção PEC {index + 1}</button>
</fieldset>;
})}
<p>O detalhe substitui o resumo PEC apenas nesta execução. Os comprovativos e resultados de execuções anteriores permanecem no histórico; não há consumo automático de saldos oficiais.</p>
+12
View File
@@ -0,0 +1,12 @@
export type FiscalProofParcel = {parcel?: string; parcelReference?: string};
export const canonicalParcel = (s: string) => s.normalize('NFKC').trim().toLocaleLowerCase('pt').replace(/\s+/g, ' ');
export function validateProofParcel(r: FiscalProofParcel) {
if (r.parcel !== undefined && typeof r.parcel !== 'string' || r.parcelReference !== undefined && typeof r.parcelReference !== 'string') throw Error('Identificação da parcela documental não recuperável; original conservado.');
}
export function requireProofParcel(r: FiscalProofParcel) {
validateProofParcel(r);
if (!!r.parcel?.trim() !== !!r.parcelReference?.trim()) throw Error('Parcela documental e suporte devem corresponder; não se presume que linhas distintas representam impostos diferentes.');
}
export const parcelProjection = (r: FiscalProofParcel): FiscalProofParcel => ({...(r.parcel === undefined ? {} : {parcel: r.parcel}), ...(r.parcelReference === undefined ? {} : {parcelReference: r.parcelReference})});
// Old proofs without a parcel cover the physical document. They overlap any newly identified parcel until reconciled.
export const overlappingProofParcels = (a: FiscalProofParcel & {identity: string}, b: FiscalProofParcel & {identity: string}) => a.identity === b.identity && (!a.parcel?.trim() || !b.parcel?.trim() || canonicalParcel(a.parcel) === canonicalParcel(b.parcel));
+61 -26
View File
@@ -1,4 +1,6 @@
import {carryPlan,carryIdentity,carrySource,newCreditCarry,validateCreditCarry,type CreditCarry} from './irpc-credit-carry.ts';
import {economicTaxProof,economicGroupPending,economicDistributionIdentity,economicPosition,economicDeduction,economicSource,newEconomicCredits,validateEconomic,type EconomicCredits} from './irpc-economic-credit.ts';
import {validateProofParcel,requireProofParcel,parcelProjection,overlappingProofParcels,type FiscalProofParcel} from './fiscal-proof-parcel.ts';
import {validateInternational,internationalProofValue,internationalGroupPending,internationalPosition,internationalDeduction,internationalCreditSource,newInternationalCredits,type InternationalCredits,type InternationalTaxProof} from './irpc-international-credit.ts';
import {validatePec,pecPaymentValue,pecDeduction,pecSource,type PecPayments} from './irpc-pec.ts';
import {normalBook,normalCurrent} from './irpc-normal.ts';
@@ -12,14 +14,20 @@ export const annualSettlementSource='CIRPC, arts. 63 e 67 (Lei 34/2007 e naturez
type State=''|'known'|'absent';
type Evidence={state:State;amount:string;reference:string};
type Group={state:State;reference:string};
export type AnnualWithholding={id:string;rowId:string;paymentId:string;sourceKey:string;subjectNuit:string;year:string;amount:string;incomeIncluded:''|'yes'|'no';reference:string};
export type AnnualPpc={id:string;document:string;issuer:string;date:string;subjectNuit:string;year:string;currency:string;proofAmount:string;amount:string;effective:''|'yes'|'no';reference:string};
export type AnnualWithholding=FiscalProofParcel&{id:string;rowId:string;paymentId:string;sourceKey:string;subjectNuit:string;year:string;amount:string;incomeIncluded:''|'yes'|'no';reference:string};
export type AnnualPpc=FiscalProofParcel&{id:string;document:string;issuer:string;date:string;subjectNuit:string;year:string;currency:string;proofAmount:string;amount:string;effective:''|'yes'|'no';reference:string};
type AnnualRecord={version:typeof annualSettlementVersion;key:string;createdAt:string;model:AnnualModel};
export type AnnualBook={version:1;subjectNuit:string;year:string;reference:string;assessment:''|'normal'|'special';assessmentKey:string;assessmentReference:string;withholdings:Group&{rows:AnnualWithholding[]};ppc:Group&{rows:AnnualPpc[]};pec:Evidence;pecPayments?:PecPayments;credits:Evidence;internationalCredits?:InternationalCredits;creditCarry?:CreditCarry;result?:AnnualRecord;history:AnnualRecord[]};
export type AnnualBook={version:1;subjectNuit:string;year:string;reference:string;assessment:''|'normal'|'special';assessmentKey:string;assessmentReference:string;withholdings:Group&{rows:AnnualWithholding[]};ppc:Group&{rows:AnnualPpc[]};pec:Evidence;pecPayments?:PecPayments;credits:Evidence;internationalCredits?:InternationalCredits;creditCarry?:CreditCarry;economicCredits?:EconomicCredits;result?:AnnualRecord;history:AnnualRecord[]};
export const annualSource=(b:AnnualBook)=>{
const excluded=b.internationalCredits?(b.pecPayments?'Créditos económicos/benefícios positivos':'PEC/créditos económicos/benefícios positivos'):(b.pecPayments?'Créditos próprios positivos':'PEC/créditos positivos');
return annualSettlementSource.replace('PEC/créditos positivos',excluded)+(b.pecPayments?' '+pecSource:'')+(b.internationalCredits?' '+internationalCreditSource:'')+(b.creditCarry?' '+carrySource:'');
return annualSettlementSource.replace('PEC/créditos positivos',b.economicCredits?(b.pecPayments?'Benefícios positivos':'PEC/benefícios positivos'):excluded)+(b.pecPayments?' '+pecSource:'')+(b.internationalCredits?' '+internationalCreditSource:'')+(b.creditCarry?' '+carrySource:'')+(b.economicCredits?' '+economicSource:'');
};
export function prepareEconomicBook(b:AnnualBook):AnnualBook{
if(b.economicCredits)return b;
const next=prepareInternationalBook(b),e=newEconomicCredits(),old=next.internationalCredits!.economic;
if(old.state==='absent'){e.state='absent';e.reference=old.reference;}
return{...next,economicCredits:e};
}
export function prepareCreditCarry(b:AnnualBook):AnnualBook{return b.creditCarry?b:{...b,creditCarry:newCreditCarry()};}
export function prepareInternationalBook(b:AnnualBook):AnnualBook{
if(b.internationalCredits)return b;
@@ -43,6 +51,7 @@ const proofIdentity=(issuer:string,document:string,_date:string)=>JSON.stringify
function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros fiscais não recuperáveis;originais conservados.');return s;}
function books(d:Record<string,unknown>):Record<string,AnnualBook>{const m=store(d).irpcAnnual;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('MapaL8 não recuperável;originais conservados.');return m;}
export function validateAnnual(b:AnnualBook){
if(b?.economicCredits!==undefined)validateEconomic(b.economicCredits);
if(b?.creditCarry!==undefined)validateCreditCarry(b.creditCarry);
if(b?.pecPayments!==undefined)validatePec(b.pecPayments);
if(b?.internationalCredits!==undefined)validateInternational(b.internationalCredits);
@@ -51,6 +60,7 @@ export function validateAnnual(b:AnnualBook){
for(const g of[b.pec,b.credits])if(typeof g.amount!=='string')throw Error('CréditoL8 não recuperável.');
const w=['id','rowId','paymentId','sourceKey','subjectNuit','year','amount','incomeIncluded','reference'],p=Object.keys(newAnnualPpc(''));
for(const[rows,keys]of[[b.withholdings.rows,w],[b.ppc.rows,p]]as const)if(!Array.isArray(rows)||new Set(rows.map(r=>r?.id)).size!==rows.length||rows.some(r=>!r||keys.some(k=>typeof(r as unknown as Record<string,unknown>)[k]!=='string')))throw Error('ComprovativosL8 não recuperáveis ou identificador repetido.');
for(const r of[...b.withholdings.rows,...b.ppc.rows])validateProofParcel(r);
if(b.withholdings.rows.some(r=>!['','yes','no'].includes(r.incomeIncluded))||b.ppc.rows.some(r=>!['','yes','no'].includes(r.effective)))throw Error('QualificaçãoL8 não recuperável.');
for(const r of[b.result,...b.history])if(r&&(r.version!==annualSettlementVersion||typeof r.key!=='string'||typeof r.createdAt!=='string'||!r.model||!Array.isArray(r.model.pending)))throw Error('HistóricoL8 não recuperável.');
}
@@ -75,7 +85,7 @@ export function reuseAnnualWithholding(d:Record<string,unknown>,b:AnnualBook,row
if(b.withholdings.rows.some(r=>r.rowId===rowId&&r.paymentId===paymentId))throw Error('Retenção/prova já seleccionada;altere a parcela existente sem duplicar.');
return{...b,withholdings:{...b.withholdings,rows:[...b.withholdings.rows,{id,rowId,paymentId,sourceKey:o.key,subjectNuit:b.subjectNuit,year:o.year,amount:o.amount,incomeIncluded:'',reference:''}]}};
}
function dependencies(d:Record<string,unknown>){let normal:unknown,L5:unknown;try{const a=annualAssessment(d);normal=[a.record,a.current];}catch(e){normal=String(e);}try{const b=irpcBook(d);L5=[b.rows,b.payments,b.result,irpcCurrent(d,b)];}catch(e){L5=String(e);}return[normal,L5,Object.entries(books(d)).filter(([k])=>k!==acquisitionScope(d)).map(([k,b])=>[k,b.subjectNuit,b.year,b.withholdings,b.ppc,...(b.pecPayments?[b.pecPayments]:[]),...(b.internationalCredits?[b.internationalCredits]:[]),...(b.creditCarry?[b.creditCarry]:[])])];}
function dependencies(d:Record<string,unknown>){let normal:unknown,L5:unknown;try{const a=annualAssessment(d);normal=[a.record,a.current];}catch(e){normal=String(e);}try{const b=irpcBook(d);L5=[b.rows,b.payments,b.result,irpcCurrent(d,b)];}catch(e){L5=String(e);}return[normal,L5,Object.entries(books(d)).filter(([k])=>k!==acquisitionScope(d)).map(([k,b])=>[k,b.subjectNuit,b.year,b.withholdings,b.ppc,...(b.pecPayments?[b.pecPayments]:[]),...(b.internationalCredits?[b.internationalCredits]:[]),...(b.creditCarry?[b.creditCarry]:[]),...(b.economicCredits?[b.economicCredits]:[])])];}
function key(d:Record<string,unknown>,b:AnnualBook){const{result:_,history:__,...input}=b;return closureFingerprint([annualSettlementVersion,acquisitionScope(d),input,dependencies(d)]);}
export function annualCurrent(d:Record<string,unknown>,b:AnnualBook){return !!b.result&&b.result.key===key(d,b);}
export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
@@ -92,37 +102,55 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
group(b.withholdings,b.withholdings.rows,'Retenções sofridas por conta');group(b.ppc,b.ppc.rows,'PPC efectuados');
try{if(b.withholdings.state==='absent'&&options.some(o=>o.ready&&o.year===b.year&&cmp(moneyInput(o.amount),zero())>0n))pending.push('Ausência de retenções sofridas contradiz provaL5 efectiva por conta no exercício;reconcilie sem a ignorar.');}catch(e){pending.push('Reconciliação da ausência de retenções: '+String(e));}
for(const[label,e]of[...(b.pecPayments?[]:[['PEC',b.pec]as const]),...(b.internationalCredits?[]:[['Créditos com regras próprias',b.credits]as const])]){try{if(!e.state||!e.reference.trim())throw Error('presença/ausência e suporte em falta');if(e.state==='absent'){if(e.amount.trim())throw Error('ausência contradiz montante preenchido');}else if(cmp(moneyInput(e.amount),zero())!==0n)throw Error('montante positivo fora deste recorte:corresponder ordem,limite e transporte próprios;sem abatimento ou reembolso automático');}catch(err){pending.push(label+': '+String(err));}}
const claims:Array<{id:string;kind:'withholding'|'ppc'|'pec'|'foreign_credit';country?:string;positionId?:string;previouslyUsed?:FiscalFraction;identity:string;proofAmount:FiscalFraction|null;value:FiscalFraction|null;sourceKey:string;allocationAmount:FiscalFraction|null;document:string;issuer:string;date:string;subjectNuit:string;year:string;reason:string}>=[];
const claims:Array<FiscalProofParcel&{id:string;kind:'withholding'|'ppc'|'pec'|'foreign_credit'|'economic_credit';country?:string;positionId?:string;previouslyUsed?:FiscalFraction;identity:string;proofAmount:FiscalFraction|null;value:FiscalFraction|null;sourceKey:string;allocationAmount:FiscalFraction|null;document:string;issuer:string;date:string;subjectNuit:string;year:string;reason:string}>=[];
for(const r of b.withholdings.rows){const o=options.find(o=>o.rowId===r.rowId&&o.paymentId===r.paymentId),issues:string[]=[];let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null;
try{if(!o||!o.ready||r.sourceKey!==o.key)throw Error(o?.reason||'provaL5 ausente ou alterada;reutilize sem duplicar');if(r.subjectNuit!==b.subjectNuit||r.year!==b.year||o.year!==b.year||!o.date.startsWith(b.year+'-'))throw Error('entidade/exercício da retenção não correspondem ao fecho');if(r.incomeIncluded!=='yes'||!r.reference.trim())throw Error('confirme o próprio beneficiário/NUIT e inclusão do rendimento na base do exercício com suporte');value=moneyInput(r.amount);proofAmount=moneyInput(o.proofAmount);if(cmp(value,moneyInput(o.amount))>0n)throw Error('parcela ultrapassa a retenção efectivamente comprovada e afectada emL5');if(b.withholdings.rows.filter(x=>x.rowId===r.rowId&&x.paymentId===r.paymentId).length!==1)throw Error('retenção/prova repetida;uma afectação por linhaL5');if(assessment?.record?.model.special?.cooperative.status==='calculated'&&cmp(value,zero())>0n)throw Error('cooperativa integralmente elegível declara ausência de rendimentos retidos;segregue o casoL7 antes do crédito anual');}catch(e){issues.push(String(e));value=null;}
claims.push({id:r.id,kind:'withholding',identity:o?.identity??'',proofAmount,value,sourceKey:r.sourceKey,allocationAmount:o&&value?moneyInput(o.amount):null,document:o?.document??r.paymentId,issuer:o?.issuer??'',date:o?.date??'',subjectNuit:r.subjectNuit,year:r.year,reason:issues.join(' ')});
try{requireProofParcel(r);if(!o||!o.ready||r.sourceKey!==o.key)throw Error(o?.reason||'provaL5 ausente ou alterada;reutilize sem duplicar');if(r.subjectNuit!==b.subjectNuit||r.year!==b.year||o.year!==b.year||!o.date.startsWith(b.year+'-'))throw Error('entidade/exercício da retenção não correspondem ao fecho');if(r.incomeIncluded!=='yes'||!r.reference.trim())throw Error('confirme o próprio beneficiário/NUIT e inclusão do rendimento na base do exercício com suporte');value=moneyInput(r.amount);proofAmount=moneyInput(o.proofAmount);if(cmp(value,moneyInput(o.amount))>0n)throw Error('parcela ultrapassa a retenção efectivamente comprovada e afectada emL5');if(b.withholdings.rows.filter(x=>x.rowId===r.rowId&&x.paymentId===r.paymentId).length!==1)throw Error('retenção/prova repetida;uma afectação por linhaL5');if(assessment?.record?.model.special?.cooperative.status==='calculated'&&cmp(value,zero())>0n)throw Error('cooperativa integralmente elegível declara ausência de rendimentos retidos;segregue o casoL7 antes do crédito anual');}catch(e){issues.push(String(e));value=null;}
claims.push({...parcelProjection(r),id:r.id,kind:'withholding',identity:o?.identity??'',proofAmount,value,sourceKey:r.sourceKey,allocationAmount:o&&value?moneyInput(o.amount):null,document:o?.document??r.paymentId,issuer:o?.issuer??'',date:o?.date??'',subjectNuit:r.subjectNuit,year:r.year,reason:issues.join(' ')});
}
for(const r of b.ppc.rows){const issues:string[]=[];let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null;
try{if(!r.document.trim()||!r.issuer.trim()||!r.reference.trim()||r.effective!=='yes'||r.currency!=='MZN'||!iso(r.date)||!r.date.startsWith(b.year+'-')||r.subjectNuit!==b.subjectNuit||r.year!==b.year)throw Error('PPC exige comprovativo efectivo,MZN,data,entidade e exercício correspondentes;projecção/tesouraria não é pagamento');value=moneyInput(r.amount);proofAmount=moneyInput(r.proofAmount);if(cmp(value,proofAmount)>0n)throw Error('parcelaPPC ultrapassa o montante comprovado');}catch(e){issues.push(String(e));value=null;}
claims.push({id:r.id,kind:'ppc',identity:proofIdentity(r.issuer,r.document,r.date),proofAmount,value,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason:issues.join(' ')});
try{requireProofParcel(r);if(!r.document.trim()||!r.issuer.trim()||!r.reference.trim()||r.effective!=='yes'||r.currency!=='MZN'||!iso(r.date)||!r.date.startsWith(b.year+'-')||r.subjectNuit!==b.subjectNuit||r.year!==b.year)throw Error('PPC exige comprovativo efectivo,MZN,data,entidade e exercício correspondentes;projecção/tesouraria não é pagamento');value=moneyInput(r.amount);proofAmount=moneyInput(r.proofAmount);if(cmp(value,proofAmount)>0n)throw Error('parcelaPPC ultrapassa o montante comprovado');}catch(e){issues.push(String(e));value=null;}
claims.push({...parcelProjection(r),id:r.id,kind:'ppc',identity:proofIdentity(r.issuer,r.document,r.date),proofAmount,value,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason:issues.join(' ')});
}
if(b.pecPayments){group(b.pecPayments,b.pecPayments.rows,'PEC pagos no próprio exercício');for(const r of b.pecPayments.rows){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,previouslyUsed:FiscalFraction|undefined;let reason='';try{({value,proofAmount,previouslyUsed}=pecPaymentValue(r,b.subjectNuit,b.year));}catch(e){reason=String(e);}claims.push({id:r.id,kind:'pec',identity:proofIdentity(r.issuer,r.document,r.date),value,proofAmount,previouslyUsed,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason});}}
if(b.pecPayments){group(b.pecPayments,b.pecPayments.rows,'PEC pagos no próprio exercício');for(const r of b.pecPayments.rows){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,previouslyUsed:FiscalFraction|undefined;let reason='';try{({value,proofAmount,previouslyUsed}=pecPaymentValue(r,b.subjectNuit,b.year));}catch(e){reason=String(e);}claims.push({...parcelProjection(r),id:r.id,kind:'pec',identity:proofIdentity(r.issuer,r.document,r.date),value,proofAmount,previouslyUsed,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason});}}
if(b.internationalCredits){
pending.push(...internationalGroupPending(b.internationalCredits));
pending.push(...internationalGroupPending(b.internationalCredits,!!b.economicCredits));
const countries=b.internationalCredits.positions.map(p=>p.country);
if(new Set(countries).size!==countries.length)pending.push('Um país/exercício tem várias posições;consolide rendimentos e comprovativos sob uma única fracção nacional,sem repetir o limite.');
for(const p of b.internationalCredits.positions)for(const r of p.payments){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,reason='';try{({value,proofAmount}=internationalProofValue(r,b.subjectNuit,b.year));}catch(e){reason=String(e);}claims.push({id:r.id,kind:'foreign_credit',country:p.country,positionId:p.id,identity:proofIdentity(r.issuer,r.document,r.date),proofAmount,value,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason});}
for(const p of b.internationalCredits.positions)for(const r of p.payments){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,reason='';try{({value,proofAmount}=internationalProofValue(r,b.subjectNuit,b.year));}catch(e){reason=String(e);}claims.push({...parcelProjection(r),id:r.id,kind:'foreign_credit',country:p.country,positionId:p.id,identity:proofIdentity(r.issuer,r.document,r.date),proofAmount,value,sourceKey:'',allocationAmount:null,document:r.document,issuer:r.issuer,date:r.date,subjectNuit:r.subjectNuit,year:r.year,reason});}
}
const elsewhere:Array<{identity:string;subjectNuit:string;year:string;value:FiscalFraction;proofAmount:FiscalFraction;sourceKey:string;kind:'ppc'|'pec'|'withholding'|'foreign_credit';country?:string;previouslyUsed?:FiscalFraction}>=[];
for(const[scope,other]of Object.entries(books(d))){if(scope===acquisitionScope(d))continue;try{validateAnnual(other);for(const p of other.internationalCredits?.positions??[]){if(other.subjectNuit===b.subjectNuit&&other.year===b.year&&b.internationalCredits?.positions.some(x=>x.country===p.country))pending.push('País '+p.country+' já preparado noutro mapa da entidade/exercício;consolide a fracção nacional e usos sem duplicar quotas.');for(const r of p.payments){const v=internationalProofValue(r,other.subjectNuit,other.year);elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...v,sourceKey:'',kind:'foreign_credit',country:p.country});}}for(const r of other.pecPayments?.rows??[]){const p=pecPaymentValue(r,other.subjectNuit,other.year);elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...p,sourceKey:'',kind:'pec'});}for(const r of other.ppc.rows)elsewhere.push({identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(r.proofAmount),sourceKey:'',kind:'ppc'});for(const r of other.withholdings.rows){const source=(store(d).irpcWithholdings??{})[scope];const row=source?.rows?.find((x:{id:string})=>x.id===r.rowId),p=source?.payments?.find((x:{id:string})=>x.id===r.paymentId);if(row&&p)elsewhere.push({identity:proofIdentity(row.counterparty,p.reference,p.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(p.amount),sourceKey:r.sourceKey,kind:'withholding'});}}catch(e){if(other?.subjectNuit===b.subjectNuit)pending.push('Outro mapa da mesma entidade não recuperável para conferir duplicações: '+scope+';'+String(e));}}
for(const c of claims){if(!c.value||!c.proofAmount)continue;const same=claims.filter(x=>x.identity===c.identity&&x.value),other=elsewhere.filter(x=>x.identity===c.identity);let issue='';
if(same.filter(x=>x.kind!=='withholding').length>1||new Set(same.map(x=>x.kind)).size>1)issue='Mesmo comprovativo repetido ou usado com naturezas diferentes (retenção/PPC/PEC/crédito internacional);reconcilie sem dedução dupla.';
if(b.economicCredits){
pending.push(...economicGroupPending(b.economicCredits));
if(!b.internationalCredits)pending.push('Distinguir o detalhe económico do inventário internacional e dos benefícios; use a preparação existente sem apagar o resumo anterior.');
for(const r of b.economicCredits.rows){let value:FiscalFraction|null=null,proofAmount:FiscalFraction|null=null,previouslyUsed:FiscalFraction|undefined,reason='';try{({value,proofAmount,previouslyUsed}=economicTaxProof(r));}catch(e){reason=String(e);}claims.push({id:r.id,kind:'economic_credit',positionId:r.id,identity:proofIdentity(r.taxIssuer,r.taxDocument,r.distributionDate),parcel:r.taxParcel,parcelReference:r.parcelReference,proofAmount,value,previouslyUsed,sourceKey:'',allocationAmount:null,document:r.taxDocument,issuer:r.taxIssuer,date:r.distributionDate,subjectNuit:r.subjectNuit,year:r.year,reason});}
}
const elsewhere:Array<FiscalProofParcel&{identity:string;subjectNuit:string;year:string;value:FiscalFraction;proofAmount:FiscalFraction;sourceKey:string;kind:'ppc'|'pec'|'withholding'|'foreign_credit'|'economic_credit';country?:string;previouslyUsed?:FiscalFraction}>=[];
for(const[scope,other]of Object.entries(books(d))){if(scope===acquisitionScope(d))continue;try{validateAnnual(other);for(const r of other.economicCredits?.rows??[]){
if(b.economicCredits?.rows.some(x=>economicDistributionIdentity(x)===economicDistributionIdentity(r)))pending.push('Parcela de lucros económicos já preparada noutro mapa;consolide sem duplicar o crédito, mesmo com documentos de imposto diferentes.');
const v=economicTaxProof(r);elsewhere.push({identity:proofIdentity(r.taxIssuer,r.taxDocument,r.distributionDate),parcel:r.taxParcel,parcelReference:r.parcelReference,subjectNuit:r.subjectNuit,year:r.year,...v,sourceKey:'',kind:'economic_credit'});
}for(const p of other.internationalCredits?.positions??[]){if(other.subjectNuit===b.subjectNuit&&other.year===b.year&&b.internationalCredits?.positions.some(x=>x.country===p.country))pending.push('País '+p.country+' já preparado noutro mapa da entidade/exercício;consolide a fracção nacional e usos sem duplicar quotas.');for(const r of p.payments){const v=internationalProofValue(r,other.subjectNuit,other.year);elsewhere.push({...parcelProjection(r),identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...v,sourceKey:'',kind:'foreign_credit',country:p.country});}}for(const r of other.pecPayments?.rows??[]){const p=pecPaymentValue(r,other.subjectNuit,other.year);elsewhere.push({...parcelProjection(r),identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,...p,sourceKey:'',kind:'pec'});}for(const r of other.ppc.rows)elsewhere.push({...parcelProjection(r),identity:proofIdentity(r.issuer,r.document,r.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(r.proofAmount),sourceKey:'',kind:'ppc'});for(const r of other.withholdings.rows){const source=(store(d).irpcWithholdings??{})[scope];const row=source?.rows?.find((x:{id:string})=>x.id===r.rowId),p=source?.payments?.find((x:{id:string})=>x.id===r.paymentId);if(row&&p)elsewhere.push({...parcelProjection(r),identity:proofIdentity(row.counterparty,p.reference,p.date),subjectNuit:r.subjectNuit,year:r.year,value:moneyInput(r.amount),proofAmount:moneyInput(p.amount),sourceKey:r.sourceKey,kind:'withholding'});}}catch(e){if(other?.subjectNuit===b.subjectNuit)pending.push('Outro mapa da mesma entidade não recuperável para conferir duplicações: '+scope+';'+String(e));}}
for(const c of claims){if(!c.value||!c.proofAmount)continue;const same=claims.filter(x=>overlappingProofParcels(x,c)&&x.value),other=elsewhere.filter(x=>overlappingProofParcels(x,c));let issue='';
if(same.filter(x=>x.kind!=='withholding').length>1&&!same.every(x=>x.kind==='economic_credit')||new Set(same.map(x=>x.kind)).size>1)issue='Mesmo comprovativo repetido ou usado com naturezas diferentes (retenção/PPC/PEC/crédito internacional);reconcilie sem dedução dupla.';
if(other.some(x=>x.subjectNuit!==b.subjectNuit||x.year!==b.year))issue='Mesmo comprovativo já associado a outra entidade/exercício;reconcilie a titularidade e a afectação.';
if([...same,...other].some(x=>x.proofAmount&&cmp(x.proofAmount,c.proofAmount!)!==0n)||cmp(addFractions([...same.flatMap(x=>x.value?[x.value]:[]),...other.map(x=>x.value)]),c.proofAmount)>0n)issue='Comprovativo utilizado além do montante disponível ou com montantes divergentes neste/outro mapa.';
if(c.kind==='pec'&&c.previouslyUsed){if(other.some(x=>x.kind!=='pec')||other.some(x=>!x.previouslyUsed||cmp(x.previouslyUsed,c.previouslyUsed!)!==0n)||cmp(addFractions([c.previouslyUsed,...same.flatMap(x=>x.value?[x.value]:[]),...other.map(x=>x.value)]),c.proofAmount)>0n)issue='PEC com utilização anterior divergente, natureza incompatível ou parcelas acima do saldo disponível.';}
if((c.kind==='pec'||c.kind==='economic_credit')&&c.previouslyUsed){if(other.some(x=>x.kind!==c.kind)||[...same,...other].some(x=>!x.previouslyUsed||cmp(x.previouslyUsed,c.previouslyUsed!)!==0n)||cmp(addFractions([c.previouslyUsed,...same.flatMap(x=>x.value?[x.value]:[]),...other.map(x=>x.value)]),c.proofAmount)>0n)issue='PEC/IRPC económico com utilização/atribuição anterior divergente, natureza incompatível ou parcelas acima do saldo disponível.';}
if(other.some(x=>x.kind!==c.kind))issue='Comprovativo já afectado a outra natureza (retenção/PPC/PEC/crédito internacional);sem dupla dedução.';
if(c.kind==='foreign_credit'&&other.some(x=>x.country!==c.country))issue='Comprovativo estrangeiro associado a outro país/natureza;reconcilie sem duplicação.';
if(c.kind==='withholding'&&c.sourceKey&&c.allocationAmount&&cmp(addFractions([...same.filter(x=>x.sourceKey===c.sourceKey).flatMap(x=>x.value?[x.value]:[]),...other.filter(x=>x.sourceKey===c.sourceKey).map(x=>x.value)]),c.allocationAmount)>0n)issue='Mesma afectação L5 reutilizada além da parcela disponível, ainda que o comprovativo global seja maior.';
if(c.kind==='withholding'&&c.sourceKey&&c.allocationAmount&&cmp(addFractions([...claims.filter(x=>x.kind==='withholding'&&x.sourceKey===c.sourceKey).flatMap(x=>x.value?[x.value]:[]),...elsewhere.filter(x=>x.kind==='withholding'&&x.sourceKey===c.sourceKey).map(x=>x.value)]),c.allocationAmount)>0n)issue='Mesma afectação L5 reutilizada além da parcela disponível, ainda que o comprovativo global seja maior.';
if(issue)c.reason=[c.reason,issue].filter(Boolean).join(' ');
}
for(const c of claims)if(c.reason)pending.push((c.document||c.id)+': '+c.reason);
const knownWithholdings=addFractions(claims.filter(c=>c.kind==='withholding'&&!c.reason&&c.value).map(c=>c.value!)),knownPpc=addFractions(claims.filter(c=>c.kind==='ppc'&&!c.reason&&c.value).map(c=>c.value!));
const positions=b.internationalCredits?.positions.map(p=>{const own=claims.filter(c=>c.kind==='foreign_credit'&&c.positionId===p.id);const m=internationalPosition(p,addFractions(own.filter(c=>!c.reason&&c.value).map(c=>c.value!)),own.filter(c=>c.reason).map(c=>c.reason),b.subjectNuit,b.year,b.assessmentKey);pending.push(...m.pending.map(e=>'País '+p.country+': '+e));return m;})??[];
const economicRows=b.economicCredits?.rows.map(r=>{
const issues=claims.filter(c=>c.kind==='economic_credit'&&c.positionId===r.id&&c.reason).map(c=>c.reason);
const m=economicPosition(r,b.subjectNuit,b.year,b.assessmentKey,colecta,issues);
pending.push(...m.pending.map(e=>'Lucros '+r.distributionDocument+': '+e));return m;
})??[];
if(b.economicCredits){
if(b.economicCredits.rows.length&&assessment?.kind!=='normal')pending.push('Crédito económico delimitado à colecta normal corrente;efeitos L7 exigem correspondência própria.');
}
const economicPreview=b.economicCredits?economicDeduction(economicRows,colecta,true):null;
const remainingForCarry=economicPreview?.afterCredit??colecta;
const pecAvailable=addFractions(claims.filter(c=>c.kind==='pec'&&!c.reason&&c.value).map(c=>c.value!));
const carryDuplicates:string[]=[];
if(b.creditCarry)for(const[scope,other]of Object.entries(books(d))){
@@ -130,17 +158,18 @@ export function calculateAnnual(d:Record<string,unknown>,b:AnnualBook){
try{validateAnnual(other);if(other.creditCarry?.lots.some(r=>b.creditCarry!.lots.some(x=>carryIdentity(x)===carryIdentity(r))))carryDuplicates.push('Saldo anterior já preparado noutro mapa da entidade;consolide a origem e usos sem duplicar: '+scope);}
catch(e){carryDuplicates.push('Outro mapa da entidade não recuperável para conferir saldos anteriores: '+scope+';'+String(e));}
}
const carry=b.creditCarry?carryPlan(b.creditCarry,b.subjectNuit,b.year,b.assessmentKey,addFractions(positions.flatMap(p=>p.available&&!p.pending.length?[p.available]:[])),pecAvailable,colecta,carryDuplicates):null;
const carry=b.creditCarry?carryPlan(b.creditCarry,b.subjectNuit,b.year,b.assessmentKey,addFractions(positions.flatMap(p=>p.available&&!p.pending.length?[p.available]:[])),pecAvailable,remainingForCarry,carryDuplicates):null;
if(carry)pending.push(...carry.pending);
const ready=pending.length===0&&colecta!==null,international=b.internationalCredits?internationalDeduction(positions,colecta,ready,carry?.international??undefined):null;
const creditCollecta=ready&&carry?subtractFractions(international?.afterCredit??colecta!,carry.priorInternational):international?.afterCredit??colecta;
const ready=pending.length===0&&colecta!==null,economic=b.economicCredits?economicDeduction(economicRows,colecta,ready):null,afterEconomic=economic?.afterCredit??colecta;
const international=b.internationalCredits?internationalDeduction(positions,afterEconomic,ready,carry?.international??undefined):null;
const creditCollecta=ready&&carry?subtractFractions(international?.afterCredit??afterEconomic!,carry.priorInternational):international?.afterCredit??afterEconomic;
const pec=b.pecPayments?pecDeduction(pecAvailable,creditCollecta,ready,carry?.pec??undefined):null;
const afterPec=ready&&carry?subtractFractions(pec?.afterPec??creditCollecta!,carry.priorPec):pec?.afterPec??creditCollecta;
const arithmeticBalance=ready?subtractFractions(subtractFractions(afterPec!,knownWithholdings),knownPpc):null;
if(international&&international.insufficientCollecta&&cmp(international.insufficientCollecta,zero())>0n)warn.push('Crédito elegível sem colecta suficiente:sem reembolso,transporte ou escolha automática da afectação por país.');
let payable:FiscalFraction|null=null,recoverable:FiscalFraction|null=null,belowMinimum:FiscalFraction|null=null;let status:'payable'|'recoverable'|'balanced'|'below_minimum'|'indeterminate'='indeterminate';
if(arithmeticBalance){const n=BigInt(arithmeticBalance.numerator),abs=fraction(n<0n?-n:n,BigInt(arithmeticBalance.denominator));payable=zero();recoverable=zero();belowMinimum=zero();if(!n)status='balanced';else if(cmp(abs,fraction(100n))<0n){status='below_minimum';belowMinimum=abs;warn.push('Art27(6):saldo inferior a100MZN,sem cobrança/reembolso neste recorte;aritmética conservada,não é transporte de crédito.');}else if(n>0n){status='payable';payable=abs;}else{status='recoverable';recoverable=abs;}}
return{version:annualSettlementVersion,input:structuredClone(input),subjectNuit:b.subjectNuit,year:b.year,assessment:assessment?.record?{key:b.assessmentKey,kind:b.assessment,version:assessment.record.model.version,createdAt:assessment.record.createdAt,colecta,period:assessment.period,origin:assessment.record.model.origin}:null,claims,internationalCredit:international,pecDeduction:pec,...(carry?{creditCarry:{...carry,afterInternational:ready?creditCollecta:null,afterPec:ready?afterPec:null}}:{}),knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.creditCarry?['Saldos anteriores documentados: escolha explícita das parcelas correntes e anteriores, sem prioridade presumida entre exercícios, reembolso ou criação/consumo de saldos oficiais.']:[]),...(b.internationalCredits?['Crédito internacional do próprio exercício:fracção nacional,convenção e montantes MZN são documentados;sem cálculo presumido,transporte ou consumo oficial. Créditos económicos/benefícios positivos ficam separados.']:[]),b.creditCarry?'L8 parcial:créditos económicos/benefícios,obrigações,tributação autónoma,períodos especiais,outras moedas e correspondência M20/M22 separados. Prazos dos saldos anteriores demonstrados apenas para origens anuais ordinárias documentadas;sem criação automática de transporte.':b.internationalCredits?'L8 parcial:créditos económicos/benefícios positivos,transporte/caducidade,obrigações,tributação autónoma,períodos especiais e outras moedas separados. PEC sem reembolso ou transporte automático.':(b.pecPayments?'L8 parcial:transporte/caducidade de PEC, cálculo da obrigação, créditos próprios positivos, tributação autónoma, períodos especiais e outras moedas separados. Excedente PEC não é reembolso nem saldo transportado oficialmente.':'L8 parcial:PEC e créditos positivos,tributação autónoma,períodos especiais e pagamentos noutra moeda exigem regras próprias.'),'PPC aqui é pagamento comprovado;não cálculo automático da obrigação ou calendário.','Retenção sofrida não é retenção entregue pela empresa por conta de terceiros.','Valores racionais exactos;camposM20/M22 e representação não presumidos.','Saldo simulado por colecta projectada;sem pagamento,submissão ou reembolso automático.']};
return{version:annualSettlementVersion,input:structuredClone(input),subjectNuit:b.subjectNuit,year:b.year,assessment:assessment?.record?{key:b.assessmentKey,kind:b.assessment,version:assessment.record.model.version,createdAt:assessment.record.createdAt,colecta,period:assessment.period,origin:assessment.record.model.origin}:null,claims,...(economic?{economicCredit:economic}:{}),internationalCredit:international,pecDeduction:pec,...(carry?{creditCarry:{...carry,afterInternational:ready?creditCollecta:null,afterPec:ready?afterPec:null}}:{}),knownWithholdings,knownPpc,arithmeticBalance,payable,recoverable,belowMinimum,status,pending:[...new Set(pending)],warnings:warn,source:annualSource(b),officialBalancesConsumed:false,submitted:false,refundReceived:false,limitations:[...(b.economicCredits?['Crédito económico ordinário:IRPC correspondente aos lucros,acréscimo53 já incluído e quota proporcional documentados. Sem cálculo automático da inclusão ou da exclusão40;liquidação64(3),associações64(4),benefícios e regras especiais separados. Sem reembolso ou transporte económico presumidos.']:[]),...(b.creditCarry?['Saldos anteriores documentados: escolha explícita das parcelas correntes e anteriores, sem prioridade presumida entre exercícios, reembolso ou criação/consumo de saldos oficiais.']:[]),...(b.internationalCredits?['Crédito internacional do próprio exercício:fracção nacional,convenção e montantes MZN são documentados;sem cálculo presumido,transporte ou consumo oficial. '+(b.economicCredits?'Benefícios positivos ficam separados;crédito económico no detalhe próprio.':'Créditos económicos/benefícios positivos ficam separados.')]:[]),b.economicCredits?'L8 parcial:benefícios próprios,derivação automática de quotas,inclusão/correcção económica na base,liquidação/associações,obrigações,autónomos,períodos especiais,moedas eM20/M22 separados. Saldos anteriores só quando detalhados e documentados.':b.creditCarry?'L8 parcial:créditos económicos/benefícios,obrigações,tributação autónoma,períodos especiais,outras moedas e correspondência M20/M22 separados. Prazos dos saldos anteriores demonstrados apenas para origens anuais ordinárias documentadas;sem criação automática de transporte.':b.internationalCredits?'L8 parcial:créditos económicos/benefícios positivos,transporte/caducidade,obrigações,tributação autónoma,períodos especiais e outras moedas separados. PEC sem reembolso ou transporte automático.':(b.pecPayments?'L8 parcial:transporte/caducidade de PEC, cálculo da obrigação, créditos próprios positivos, tributação autónoma, períodos especiais e outras moedas separados. Excedente PEC não é reembolso nem saldo transportado oficialmente.':'L8 parcial:PEC e créditos positivos,tributação autónoma,períodos especiais e pagamentos noutra moeda exigem regras próprias.'),'PPC aqui é pagamento comprovado;não cálculo automático da obrigação ou calendário.','Retenção sofrida não é retenção entregue pela empresa por conta de terceiros.','Valores racionais exactos;camposM20/M22 e representação não presumidos.','Saldo simulado por colecta projectada;sem pagamento,submissão ou reembolso automático.']};
}
export type AnnualModel=ReturnType<typeof calculateAnnual>;
export function recordAnnual(d:Record<string,unknown>,b:AnnualBook,now=new Date().toISOString()):AnnualBook{return{...b,history:b.result?[...b.history,structuredClone(b.result)]:b.history,result:{version:annualSettlementVersion,key:key(d,b),createdAt:now,model:calculateAnnual(d,b)}};}
@@ -155,10 +184,16 @@ export function annualReportRows(d:Record<string,unknown>):Array<[string,string,
'NUIT: '+m.subjectNuit+'; exercício: '+m.year+'; colecta: '+show(m.assessment?.colecta??null)+'; origem: '+(m.assessment?.kind==='special'?'L7 qualificado':'L6 normal')+'. '+(annualCurrent(d,b)?'Entradas correspondentes.':'Histórico; origem/entradas alteradas: recalcule o saldo corrente.')+' Saldo simulado, não liquidação submetida.',
m.input.reference+'; '+m.input.assessmentReference+'; '+m.source],
...m.claims.map(c=>[
'IRPC — L8',(c.kind==='withholding'?'Retenção sofrida':c.kind==='pec'?'PEC':c.kind==='foreign_credit'?'Imposto estrangeiro':'PPC')+' — '+(c.document||c.id),
'Entidade: '+c.subjectNuit+'; exercício: '+c.year+'; emitente: '+c.issuer+'; data: '+c.date+'; comprovado: '+show(c.proofAmount)+'; parcela: '+(c.reason?'Indeterminado':show(c.value))+'. '+(c.reason||'Reconciliada sem utilização duplicada.'),
c.kind==='withholding'?m.input.withholdings.reference+'; '+(m.input.withholdings.rows.find(r=>r.id===c.id)?.reference??''):c.kind==='foreign_credit'?(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.positions.flatMap(p=>p.payments).find(r=>r.id===c.id)?.reference??'')+'; '+(m.input.internationalCredits?.positions.flatMap(p=>p.payments).find(r=>r.id===c.id)?.exerciseReference??''):c.kind==='pec'?(m.input.pecPayments?.reference??'')+'; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.reference??'')+'; utilizações anteriores: '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.previouslyUsed??'')+' MZN; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.priorUseReference??''):m.input.ppc.reference+'; '+(m.input.ppc.rows.find(r=>r.id===c.id)?.reference??'')
'IRPC — L8',(c.kind==='withholding'?'Retenção sofrida':c.kind==='pec'?'PEC':c.kind==='economic_credit'?'IRPC correspondente aos lucros':c.kind==='foreign_credit'?'Imposto estrangeiro':'PPC')+' — '+(c.document||c.id),
'Entidade: '+c.subjectNuit+'; exercício: '+c.year+'; emitente: '+c.issuer+'; data: '+c.date+'; comprovado: '+show(c.proofAmount)+'; parcela: '+(c.reason?'Indeterminado':show(c.value))+(c.parcel?'; parcela documental: '+c.parcel+'; '+(c.parcelReference??''):'')+'. '+(c.reason||'Reconciliada sem utilização duplicada.'),
c.kind==='economic_credit'?(m.input.economicCredits?.reference??'')+'; '+(m.input.economicCredits?.rows.find(r=>r.id===c.id)?.attributionReference??''):c.kind==='withholding'?m.input.withholdings.reference+'; '+(m.input.withholdings.rows.find(r=>r.id===c.id)?.reference??''):c.kind==='foreign_credit'?(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.positions.flatMap(p=>p.payments).find(r=>r.id===c.id)?.reference??'')+'; '+(m.input.internationalCredits?.positions.flatMap(p=>p.payments).find(r=>r.id===c.id)?.exerciseReference??''):c.kind==='pec'?(m.input.pecPayments?.reference??'')+'; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.reference??'')+'; utilizações anteriores: '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.previouslyUsed??'')+' MZN; '+(m.input.pecPayments?.rows.find(r=>r.id===c.id)?.priorUseReference??''):m.input.ppc.reference+'; '+(m.input.ppc.rows.find(r=>r.id===c.id)?.reference??'')
]as[string,string,string,string]),
...(m.economicCredit?[
...m.economicCredit.rows.map(r=>['IRPC — L8','Crédito económico — '+r.input.distributionDocument,
'Lucros incluídos: '+r.input.income+' MZN;IRPC correspondente: '+r.input.taxAmount+' MZN;crédito nominal60%: '+show(r.nominal)+';acréscimo53 incluído: '+r.input.grossUpIncluded+' MZN;quota proporcional: '+show(r.nationalLimit)+';elegível: '+show(r.eligible)+';uso efectivo anterior: '+show(r.previousUse)+';disponível: '+show(r.available)+';estado: '+r.status+'. '+r.pending.join(' | '),
r.input.qualificationReference+';40(1): '+r.input.article40Reference+';inclusão: '+r.input.incomeReference+';53: '+r.input.grossUpReference+';quota: '+r.input.nationalLimitReference+';uso: '+r.input.previousUse.reference]as[string,string,string,string]),
['IRPC — L8','Crédito económico — antes do internacional','Disponível: '+show(m.economicCredit.available)+';dedução: '+show(m.economicCredit.deducted)+';colecta após crédito económico: '+show(m.economicCredit.afterCredit)+'. Sem reembolso ou criação/consumo de saldo oficial.',m.economicCredit.source+'; '+(m.input.economicCredits?.reference??'')]as[string,string,string,string]
]:[]),
...(m.internationalCredit?[
...m.internationalCredit.positions.map(p=>['IRPC — L8','Crédito internacional — '+p.country,'Imposto pago seleccionado: '+show(p.paid)+'; fracção nacional: '+show(p.nationalLimit)+'; limite CDT: '+(p.input.treaty==='no'?'Não aplicável documentado':show(p.treatyLimit))+'; elegível: '+show(p.eligible)+'; utilização anterior: '+show(p.previousUse)+'; disponível: '+show(p.available)+'; imposto acima dos limites: '+show(p.aboveLimit)+'. '+p.pending.join(' | '),p.input.incomeReference+'; '+p.input.nationalLimitReference+'; '+p.input.treatyReference+'; '+p.input.previousUse.reference]as[string,string,string,string]),
['IRPC — L8','Crédito internacional — antes do PEC','Disponível conhecido: '+show(m.internationalCredit.available)+'; dedução: '+show(m.internationalCredit.deducted)+'; colecta após crédito: '+show(m.internationalCredit.afterCredit)+'; elegível não deduzido por insuficiência de colecta: '+show(m.internationalCredit.insufficientCollecta)+(m.creditCarry?'; crédito corrente não escolhido: '+show(m.internationalCredit.notSelected??null):'')+'. Sem reembolso,transporte ou afectação oficial por país. Estado: '+stateLabel(m.input.internationalCredits?.state??'')+'. Créditos económicos/benefícios: '+stateLabel(m.input.internationalCredits?.economic.state??'')+' / '+stateLabel(m.input.internationalCredits?.benefits.state??'')+'.',m.internationalCredit.source+'; '+(m.input.internationalCredits?.reference??'')+'; '+(m.input.internationalCredits?.economic.reference??'')+'; '+(m.input.internationalCredits?.benefits.reference??'')]as[string,string,string,string]
+78
View File
@@ -0,0 +1,78 @@
import {addFractions, fraction, subtractFractions, type FiscalFraction} from './fiscal-rational.ts';
import {creditCompare, creditMoney, creditLimit, type CreditEvidence, type CreditState} from './irpc-international-credit.ts';
import {canonicalParcel} from './fiscal-proof-parcel.ts';
export const economicVersion = 'IRPC-economic-credit/1';
export const economicSource = 'CIRPC64(1–2), Lei34/2007, BR52, PDF18/p768-(72):60% do IRPC correspondente aos lucros distribuídos incluídos na base, limitado à colecta proporcional após acréscimo53(1)a (PDF15/p768-(69)). Distinto de65 e da exclusão40(1), na redacçãoLei20/2009,PDF4/p272-(16). RIRPC21(2/5),PDF6/p121:económico antes do internacional/benefícios/PEC,até à colecta,sem reembolso. Recorte2026,residente geral,ano civil,MZN,lucros ordinários,qualificação e quota proporcional documentadas na colecta corrente. Sem alterar a base por presunção, transporte ou consumo de saldos oficiais.';
type Choice = '' | 'yes' | 'no';
export type EconomicPosition = {
id: string; subjectNuit: string; year: string; assessmentKey: string;
kind: '' | 'ordinary_profit' | 'liquidation' | 'association'; payerNuit: string;
recipientResident: Choice; payerResident: Choice; payerIrpc: Choice; payerExempt: Choice;
article40: Choice; qualificationReference: string; article40Reference: string;
distributionDocument: string; distributionParcel: string; distributionDate: string;
income: string; incomeIncluded: Choice; incomeReference: string;
taxIssuer: string; taxDocument: string; taxParcel: string; parcelReference: string;
currency: string; proofAmount: string; taxAmount: string; attributionReference: string;
previouslyAttributed: string; priorAttributionReference: string;
grossUpIncluded: string; grossUpReference: string; nationalLimit: string; nationalLimitReference: string;
previousUse: CreditEvidence;
};
export type EconomicCredits = {version: 1; state: CreditState; reference: string; rows: EconomicPosition[]};
export const newEconomicCredits = (): EconomicCredits => ({version: 1, state: '', reference: '', rows: []});
export const newEconomicPosition = (id: string): EconomicPosition => ({id, subjectNuit: '', year: '', assessmentKey: '', kind: '', payerNuit: '', recipientResident: '', payerResident: '', payerIrpc: '', payerExempt: '', article40: '', qualificationReference: '', article40Reference: '', distributionDocument: '', distributionParcel: '', distributionDate: '', income: '', incomeIncluded: '', incomeReference: '', taxIssuer: '', taxDocument: '', taxParcel: '', parcelReference: '', currency: 'MZN', proofAmount: '', taxAmount: '', attributionReference: '', previouslyAttributed: '', priorAttributionReference: '', grossUpIncluded: '', grossUpReference: '', nationalLimit: '', nationalLimitReference: '', previousUse: {state: '', amount: '', reference: ''}});
export function validateEconomic(c: EconomicCredits) {
if (!c || c.version !== 1 || !['', 'known', 'absent'].includes(c.state) || typeof c.reference !== 'string' || !Array.isArray(c.rows) || new Set(c.rows.map(r => r?.id)).size !== c.rows.length) throw Error('Crédito económico não recuperável; originais conservados.');
const keys = Object.keys(newEconomicPosition('')).filter(k => k !== 'previousUse');
for (const r of c.rows) if (!r || keys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !['', 'ordinary_profit', 'liquidation', 'association'].includes(r.kind) || ['recipientResident', 'payerResident', 'payerIrpc', 'payerExempt', 'article40', 'incomeIncluded'].some(k => !['', 'yes', 'no'].includes(r[k as 'payerIrpc'])) || !r.previousUse || !['', 'known', 'absent'].includes(r.previousUse.state) || typeof r.previousUse.amount !== 'string' || typeof r.previousUse.reference !== 'string') throw Error('Posição económica não recuperável; originais conservados.');
}
const zero = () => fraction(0n);
const minimum = (...v: FiscalFraction[]) => v.reduce((a, b) => creditCompare(a, b) < 0n ? a : b);
const validDate = (s: string) => /^20\d{2}-\d{2}-\d{2}$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s;
export const economicDistributionIdentity = (r: EconomicPosition) => JSON.stringify([r.payerNuit, canonicalParcel(r.distributionDocument), canonicalParcel(r.distributionParcel)]);
export function economicTaxProof(r: EconomicPosition) {
if (!r.taxIssuer.trim() || !r.taxDocument.trim() || !r.taxParcel.trim() || !r.parcelReference.trim() || !r.attributionReference.trim() || !r.priorAttributionReference.trim() || r.currency !== 'MZN') throw Error('IRPC correspondente exige documento, emitente/parcela, suporte da atribuição aos lucros e montante MZN; não é retenção do beneficiário nem imposto estrangeiro.');
const proofAmount = creditMoney(r.proofAmount), value = creditMoney(r.taxAmount), previouslyUsed = creditMoney(r.previouslyAttributed);
if (creditCompare(addFractions([value, previouslyUsed]), proofAmount) > 0n) throw Error('IRPC atribuído aos lucros mais parcelas já atribuídas ultrapassam o comprovado disponível.');
return {proofAmount, value, previouslyUsed};
}
export function economicGroupPending(c: EconomicCredits) {
const pending: string[] = [];
if (!c.state || !c.reference.trim()) pending.push('Crédito económico: inventário conhecido/ausente e suporte em falta.');
if (c.state === 'absent' && c.rows.length) pending.push('Ausência de crédito económico contradiz lucros registados; conserve e reconcilie.');
if (c.state === 'known' && !c.rows.length) pending.push('Crédito económico conhecido sem lucros/IRPC discriminados; zero não é ausência presumida.');
if (new Set(c.rows.map(economicDistributionIdentity)).size !== c.rows.length) pending.push('Mesma parcela de lucros distribuídos repetida; consolide a origem, mesmo que o documento de imposto ou a data tenham sido alterados.');
return pending;
}
export function economicPosition(r: EconomicPosition, subjectNuit: string, year: string, assessmentKey: string, colecta: FiscalFraction | null, proofIssues: string[]) {
const pending = [...proofIssues];
let nominal: FiscalFraction | null = null, nationalLimit: FiscalFraction | null = null, eligible: FiscalFraction | null = null, previousUse: FiscalFraction | null = null, available: FiscalFraction | null = null, included: FiscalFraction | null = null;
let status: 'qualified' | 'outside_scope' | 'indeterminate' = 'indeterminate';
try {
if (!r.id.trim() || !/^\d{9}$/.test(r.payerNuit) || r.subjectNuit !== subjectNuit || year !== '2026' || r.year !== year || r.currency !== 'MZN') throw Error('Titular, entidade distribuidora, exercício2026 e MZN por corresponder.');
if (r.kind !== 'ordinary_profit') {status = r.kind ? 'outside_scope' : 'indeterminate'; throw Error('Lucros ordinários64(1–2) por identificar; liquidação64(3) e associações64(4) exigem correspondência específica, sem converter a parcela em crédito internacional.');}
if (r.recipientResident !== 'yes' || r.payerResident !== 'yes' || r.payerIrpc !== 'yes' || r.payerExempt !== 'no' || !r.qualificationReference.trim()) throw Error('Exige sede/direcção efectiva em Moçambique de beneficiário e distribuidor, distribuidor sujeito a IRPC e não isento, com suporte.');
if (r.article40 !== 'no' || !r.article40Reference.trim()) {status = r.article40 === 'yes' ? 'outside_scope' : 'indeterminate'; throw Error(r.article40 === 'yes' ? 'Exclusão40(1) identificada: tratar na base; não somar crédito64 à mesma parcela excluída.' : 'Falta corresponder a não aplicação de40(1), incluindo a redacção20/2009; desconhecido não significa não aplicável.');}
if (!validDate(r.distributionDate) || !r.distributionDate.startsWith(year + '-') || !r.distributionDocument.trim() || !r.distributionParcel.trim()) throw Error('Documento/parcela e data da distribuição do exercício por identificar.');
if (!r.assessmentKey || r.assessmentKey !== assessmentKey || r.incomeIncluded !== 'yes' || !r.incomeReference.trim()) throw Error('Confirme a inclusão dos lucros ilíquidos de retenção e a correspondência com a colecta corrente; a origem pode ter mudado.');
const tax = economicTaxProof(r), income = creditMoney(r.income);
nominal = fraction(BigInt(tax.value.numerator) * 3n, BigInt(tax.value.denominator) * 5n);
if (creditCompare(income, zero()) === 0n && creditCompare(nominal, zero()) > 0n) throw Error('Lucros incluídos zero contradizem IRPC correspondente positivo.');
if (!r.grossUpReference.trim() || creditCompare(creditLimit(r.grossUpIncluded), nominal) !== 0n) throw Error('Acréscimo53(1)a incluído na base deve corresponder ao crédito nominal60%; reconcilie L6 sem alterar a colecta ou o histórico automaticamente.');
included = addFractions([income, nominal]);
nationalLimit = creditLimit(r.nationalLimit);
if (!r.nationalLimitReference.trim() || !colecta || creditCompare(nationalLimit, colecta) > 0n || creditCompare(included, zero()) === 0n && creditCompare(nationalLimit, zero()) > 0n) throw Error('Quota proporcional da colecta após acréscimo53 exige suporte e correspondência; não pode exceder a colecta nem ser positiva para rendimento zero.');
if (!r.previousUse.state || !r.previousUse.reference.trim()) throw Error('Utilização anterior efectiva do crédito em falta; simulação não é utilização fiscal.');
if (r.previousUse.state === 'absent') {if (r.previousUse.amount.trim()) throw Error('Ausência de uso anterior contradiz montante'); previousUse = zero();}
else previousUse = creditLimit(r.previousUse.amount);
eligible = minimum(nominal, nationalLimit);
if (creditCompare(previousUse, eligible) > 0n) throw Error('Uso anterior excede o crédito económico elegível.');
if (!pending.length) {available = subtractFractions(eligible, previousUse); status = 'qualified';}
} catch (e) {pending.push(String(e));}
return {id: r.id, nominal, nationalLimit, eligible, previousUse, available, included, status, pending, input: structuredClone(r)};
}
export function economicDeduction(rows: ReturnType<typeof economicPosition>[], colecta: FiscalFraction | null, ready: boolean) {
const available = addFractions(rows.flatMap(r => r.available && !r.pending.length ? [r.available] : []));
const deducted = ready && colecta ? minimum(available, colecta) : null;
return {version: economicVersion, rows, available, deducted, afterCredit: deducted && colecta ? subtractFractions(colecta, deducted) : null, source: economicSource, officialBalancesConsumed: false, carryCreated: false};
}
@@ -1,4 +1,5 @@
import type {AnnualPpc} from './irpc-annual-settlement.ts';
import {validateProofParcel, requireProofParcel} from './fiscal-proof-parcel.ts';
import {countryIsoCodes} from './country-iso-codes.ts';
import {addFractions, subtractFractions, decimalFraction, fraction, type FiscalFraction} from './fiscal-rational.ts';
@@ -32,6 +33,7 @@ export function validateInternational(c: InternationalCredits) {
for (const p of c.positions) {
if (!p || positionKeys.some(k => typeof (p as unknown as Record<string, unknown>)[k] !== 'string') || !yesNo.includes(p.incomeIncluded) || !yesNo.includes(p.treaty) || !evidenceValid(p.previousUse) || !Array.isArray(p.payments)) throw Error('Posição internacional não recuperável; originais conservados.');
for (const r of p.payments) {
validateProofParcel(r);
if (!r || proofKeys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !yesNo.includes(r.effective) || !yesNo.includes(r.incomeTax)) throw Error('Comprovativo internacional não recuperável.');
proofIds.push(r.id);
}
@@ -53,18 +55,19 @@ const zero = () => fraction(0n);
const minimum = (...values: FiscalFraction[]) => values.reduce((a, b) => creditCompare(a, b) < 0n ? a : b);
const isDate = (s: string) => /^20\d{2}-\d{2}-\d{2}$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s;
export function internationalProofValue(r: InternationalTaxProof, subjectNuit: string, year: string) {
requireProofParcel(r);
if (!r.id.trim() || !r.document.trim() || !r.issuer.trim() || !r.reference.trim() || !r.taxReference.trim() || r.incomeTax !== 'yes' || r.effective !== 'yes' || r.currency !== 'MZN' || !isDate(r.date) || r.subjectNuit !== subjectNuit || r.year !== year || year !== '2026') throw Error('Crédito internacional: exige imposto sobre o rendimento efectivamente pago, prova/titular/exercício 2026 e MZN; previsões, outras naturezas e conversões não correspondidas ficam separados.');
if (!r.date.startsWith(year + '-') && !r.exerciseReference.trim()) throw Error('Data de pagamento fora do exercício: falta correspondência documental com o rendimento e imposto deste exercício; não é transporte presumido.');
const value = creditMoney(r.amount), proofAmount = creditMoney(r.proofAmount);
if (creditCompare(value, proofAmount) > 0n) throw Error('Parcela estrangeira ultrapassa o montante comprovado.');
return {value, proofAmount};
}
export function internationalGroupPending(c: InternationalCredits) {
export function internationalGroupPending(c: InternationalCredits, economicDetailed = false) {
const pending: string[] = [];
if (!c.state || !c.reference.trim()) pending.push('Crédito internacional: declare pagamentos conhecidos, ausência ou informação em falta, com suporte.');
if (c.state === 'absent' && c.positions.length) pending.push('Ausência de crédito internacional contradiz posições presentes; conserve e reconcilie.');
if (c.state === 'known' && !c.positions.length) pending.push('Crédito internacional conhecido sem posição/comprovativo; zero não é ausência presumida.');
for (const [label, e] of [['Dupla tributação económica', c.economic], ['Benefícios/créditos próprios', c.benefits]] as const) {
for (const [label, e] of [...(economicDetailed ? [] : [['Dupla tributação económica', c.economic] as const]), ['Benefícios/créditos próprios', c.benefits] as const]) {
try {
if (!e.state || !e.reference.trim()) throw Error('estado e suporte em falta');
if (e.state === 'absent') {if (e.amount.trim()) throw Error('ausência contradiz montante');}
+4 -1
View File
@@ -1,8 +1,9 @@
import {subtractFractions, decimalFraction, type FiscalFraction} from './fiscal-rational.ts';
import {validateProofParcel, requireProofParcel, type FiscalProofParcel} from './fiscal-proof-parcel.ts';
export const pecVersion = 'IRPC-PEC/1';
export const pecSource = 'CIRPC, art. 66(1–2), Lei 34/2007, exemplar BR52, p. 768-(72): dedução após créditos anteriores, até à colecta, sem reembolso do PEC. Recorte demonstrado: pagamento efectivo do próprio exercício 2026, residente geral, ano civil, MZN. Não calcula a obrigação PEC, transporte, caducidade ou consumo de saldos oficiais.';
export type PecPayment = {
export type PecPayment = FiscalProofParcel & {
id: string; document: string; issuer: string; date: string; subjectNuit: string;
year: string; currency: string; proofAmount: string; amount: string;
effective: '' | 'yes' | 'no'; reference: string;
@@ -15,6 +16,7 @@ export function validatePec(p: PecPayments) {
if (!p || p.version !== 1 || !['', 'known', 'absent'].includes(p.state) || typeof p.reference !== 'string' || !Array.isArray(p.rows) || new Set(p.rows.map(r => r?.id)).size !== p.rows.length || p.rows.some(r => !r || keys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !['', 'yes', 'no'].includes(r.effective))) {
throw Error('Comprovativos PEC não recuperáveis; originais conservados.');
}
for (const r of p.rows) validateProofParcel(r);
}
export const pecCompare = (a: FiscalFraction, b: FiscalFraction) => BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator);
const money = (s: string) => {
@@ -22,6 +24,7 @@ const money = (s: string) => {
return decimalFraction(s);
};
export function pecPaymentValue(r: PecPayment, subjectNuit: string, year: string) {
requireProofParcel(r);
if (!r.id.trim() || !r.document.trim() || !r.issuer.trim() || !r.reference.trim() || !r.priorUseReference.trim() || r.effective !== 'yes' || r.currency !== 'MZN' || !/^20\d{2}-\d{2}-\d{2}$/.test(r.date) || !Number.isFinite(Date.parse(r.date)) || new Date(r.date).toISOString().slice(0, 10) !== r.date || !r.date.startsWith(year + '-') || r.subjectNuit !== subjectNuit || r.year !== year || year !== '2026') {
throw Error('PEC: exige prova efectiva, natureza PEC, titular, exercício 2026, data válida e MZN; saldos anteriores e previsões ficam separados.');
}