import { inputText } from './input-text.ts'; import { validateMrfta, mrftaReportRows, type MrftaStore, } from './irpc-mrfta.ts'; import { irpcReliefVersion, evaluateIrpcRelief, irpcReliefEvidenceLabel, type IrpcReliefPreparation, } from './irpc-relief-preparation.ts'; import { convertIrpcAmount, irpcFxDependency, irpcFxEvidenceLabel, type IrpcFxAmount, type IrpcFxEvidence, } from './irpc-fx.ts'; import { acquisitionBook, acquisitionScope } from './iva-acquisitions.ts'; import { salesBook } from './iva-operations.ts'; import { closureFingerprint } from './iva-closure.ts'; import { addFractions, decimalFraction, fraction, fractionPresentation, subtractFractions, type FiscalFraction, } from './fiscal-rational.ts'; export const irpcWithholdingVersion = 'irpc-L5/1'; export const irpcWithholdingSource = 'Lei12/2025,BR248/2025,PDF19/p2180(19):arts5(5/7),20(2)c,62(5)a/b e67(5/7);CIRPC Lei34/2007 arts2,62/67/68,PDF17–19/p768(71–73);Lei19/2013 PDF2/p682(22),62(3/4);Lei20/2022 BR252 p2731/PDF1,62(1–5). Art67(7)remete62(6),enquanto taxa nova consta62(5):divergência formal preservada,sem renumerar o original. MRFTA19/39AT,p1–2. Código2002 revogado não usado. Outros regimes/taxas dependem da correspondência específica,sem zero presumido.'; type Choice = '' | 'yes' | 'no'; export type IrpcKind = | '' | 'digital_goods' | 'digital_services' | 'emoney_commission' | 'royalty' | 'equipment' | 'rent' | 'capital' | 'services' | 'telecom' | 'international_transport' | 'connected_installation' | 'aircraft_maintenance_charter' | 'rural_public_electrification' | 'maritime_fishing_cabotage' | 'other'; export const irpcKinds: Array<[IrpcKind, string]> = [ ['', 'Por identificar'], ['digital_goods', 'Transmissão de bens digitais'], ['digital_services', 'Prestação de serviços digitais'], ['emoney_commission', 'Comissão de agente de moeda electrónica'], ['royalty', 'Propriedade intelectual/industrial'], ['equipment', 'Uso de equipamento'], ['rent', 'Rendimento predial'], ['capital', 'Aplicação de capitais'], ['services', 'Outras prestações/intermediação'], ['telecom', 'Serviços de telecomunicações — artigo62(4)a'], ['international_transport', 'Transportes internacionais — artigo62(4)a'], [ 'connected_installation', 'Montagem/instalação de equipamentos conexos a telecomunicações/transportes —62(4)a', ], [ 'aircraft_maintenance_charter', 'Manutenção e fretamento de aeronaves —62(4)a', ], [ 'rural_public_electrification', 'Construção/reabilitação de infraestruturas de produção,transporte e distribuição de energia eléctrica,nas zonas rurais,em projectos públicos de electrificação rural —62(4)b', ], [ 'maritime_fishing_cabotage', 'Afretamento de embarcações marítimas para pesca/cabotagem —62(4)c', ], ['other', 'Outro enquadramento'], ]; export type IrpcEvent = { kind: | 'cost' | 'payment' | 'maturity' | 'availability' | 'liquidation' | 'quantification'; date: string; reference: string; }; export type IrpcPayment = IrpcFxAmount & { id: string; kind: 'beneficiary' | 'remittance' | 'withholding_proof'; reference: string; date: string; amount: string; currency: string; }; export type IrpcOrdinaryFacts = { nonDigital: Choice; capitalType: | '' | 'unlisted_interest' | 'unlisted_dividend' | 'listed_debt_mz' | 'listed_other_mz' | 'liquidity_repo' | 'other'; rentOwnPropertyManagement: Choice; specialFacts: Choice; capitalSeatAndManagement?: Choice; reference: string; }; export const emptyIrpcOrdinaryFacts = (): IrpcOrdinaryFacts => ({ nonDigital: '', capitalType: '', rentOwnPropertyManagement: '', specialFacts: '', capitalSeatAndManagement: '', reference: '', }); export type IrpcRow = IrpcFxAmount & { reliefPreparation?: IrpcReliefPreparation; ordinary?: IrpcOrdinaryFacts; id: string; direction: 'paid' | 'received'; counterparty: string; documentRef: string; description: string; base: string; currency: string; origin: 'projected' | 'effective'; kind: IrpcKind; recipientIrpc: Choice; payerIrpc: Choice; residence: '' | 'resident' | 'nonresident'; permanentEstablishment: Choice; attributable: Choice; mozambique: Choice; qualificationRef: string; specialRelief: '' | 'none' | 'claimed'; reliefRef: string; eventsComplete: Choice; events: IrpcEvent[]; settlement: '' | 'none' | 'complete'; settlementRef: string; allocations: Array<{ paymentId: string; amount: string }>; source?: { id: string; key: string; fingerprint: string }; }; export type IrpcResult = { treatment?: 'dispensed' | 'treaty'; reliefRule?: string; id: string; status: 'calculated' | 'indeterminate' | 'outside_scope'; reason: string; period: string; eventDate: string; nature: 'definitive' | 'on_account' | null; rate: number | null; base: FiscalFraction | null; tax: FiscalFraction | null; settled: FiscalFraction | null; outstanding: FiscalFraction | null; direction: IrpcRow['direction']; conversion?: IrpcFxEvidence | null; }; export type IrpcBook = MrftaStore & { version: 1; rows: IrpcRow[]; payments: IrpcPayment[]; result?: { version: string; key: string; calculatedAt: string; inputs: IrpcRow[]; payments: IrpcPayment[]; rows: IrpcResult[]; paymentConversions?: Record; }; history?: Array>; }; export const newIrpcRow = (id: string): IrpcRow => ({ id, direction: 'paid', counterparty: '', documentRef: '', description: '', base: '', currency: 'MZN', origin: 'projected', kind: '', recipientIrpc: '', payerIrpc: '', residence: '', permanentEstablishment: '', attributable: '', mozambique: '', qualificationRef: '', specialRelief: '', reliefRef: '', eventsComplete: '', events: [], settlement: '', settlementRef: '', allocations: [], }); function store(d: Record) { const s = JSON.parse( inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'), ); if ( s.version !== 1 || !s.books || typeof s.books !== 'object' || Array.isArray(s.books) ) throw Error('Livros fiscais não recuperáveis;originais preservados.'); return s; } function map(d: Record): Record { const m = store(d).irpcWithholdings; if (m === undefined) return {}; if (!m || typeof m !== 'object' || Array.isArray(m)) throw Error('Mapa IRPC não recuperável;originais preservados.'); return m; } function validate(b: IrpcBook) { if ( !b || b.version !== 1 || !Array.isArray(b.rows) || !Array.isArray(b.payments) || (b.history !== undefined && !Array.isArray(b.history)) ) throw Error('Livro IRPC não recuperável.'); validateMrfta(b); for (const r of b.rows) { const template = newIrpcRow(''); if ( !r || Object.keys(template) .filter((k) => typeof template[k as keyof IrpcRow] === 'string') .some((k) => typeof r[k as keyof IrpcRow] !== 'string') || !['paid', 'received'].includes(r.direction) || !['projected', 'effective'].includes(r.origin) || !irpcKinds.some(([k]) => k === r.kind) || !['', 'resident', 'nonresident'].includes(r.residence) || !['', 'none', 'claimed'].includes(r.specialRelief) || !['', 'none', 'complete'].includes(r.settlement) || [ 'recipientIrpc', 'payerIrpc', 'permanentEstablishment', 'attributable', 'mozambique', 'eventsComplete', ].some( (k) => !['', 'yes', 'no'].includes( r[k as keyof IrpcRow] as string, ), ) || !Array.isArray(r.events) || !Array.isArray(r.allocations) || r.events.some( (e) => !e || ![ 'cost', 'payment', 'maturity', 'availability', 'liquidation', 'quantification', ].includes(e.kind) || typeof e.date !== 'string' || typeof e.reference !== 'string', ) || r.allocations.some( (a) => !a || typeof a.paymentId !== 'string' || typeof a.amount !== 'string', ) || (r.source !== undefined && (!r.source || !['id', 'key', 'fingerprint'].every( (k) => typeof r.source![ k as keyof NonNullable ] === 'string', ))) ) throw Error( 'Entradas IRPC não recuperáveis;sem apagar ou repor vazios.', ); } for (const r of b.rows) if ( r.reliefPreparation && ((r.reliefPreparation.rule !== undefined && !['', 'cirpc68d', 'cirpc68f', 'ptMz12'].includes( r.reliefPreparation.rule, )) || (r.reliefPreparation.facts !== undefined && (!r.reliefPreparation.facts || typeof r.reliefPreparation.facts !== 'object' || Array.isArray(r.reliefPreparation.facts) || Object.values(r.reliefPreparation.facts).some( (v) => !['', 'yes', 'no'].includes(v), )))) ) throw Error('Condições da dispensa/convenção não recuperáveis.'); for (const r of b.rows) if ( r.reliefPreparation && (!['dispensation', 'exemption', 'treaty'].includes( r.reliefPreparation.kind, ) || [ 'instrument', 'article', 'country', 'residenceDocument', 'validFrom', 'validTo', 'conditions', 'evidence', ].some( (k) => typeof r.reliefPreparation![ k as keyof IrpcReliefPreparation ] !== 'string', )) ) throw Error( 'Preparação de dispensa/convenção não recuperável;original preservado.', ); for (const r of b.rows) if ( r.ordinary && (['nonDigital', 'rentOwnPropertyManagement', 'specialFacts'].some( (k) => !['', 'yes', 'no'].includes( r.ordinary![ k as | 'nonDigital' | 'rentOwnPropertyManagement' | 'specialFacts' ], ), ) || typeof r.ordinary.reference !== 'string' || ![ '', 'unlisted_interest', 'unlisted_dividend', 'listed_debt_mz', 'listed_other_mz', 'liquidity_repo', 'other', ].includes(r.ordinary.capitalType)) ) throw Error('Factos específicos IRPC não recuperáveis.'); for (const r of b.rows) if ( r.ordinary?.capitalSeatAndManagement !== undefined && !['', 'yes', 'no'].includes(r.ordinary.capitalSeatAndManagement) ) throw Error('Factos de sede/direcção não recuperáveis.'); for (const v of [...b.rows, ...b.payments]) if ( (v.fxSelection !== undefined && (!v.fxSelection || typeof v.fxSelection !== 'object' || Array.isArray(v.fxSelection))) || (v.fxLink !== undefined && (!v.fxLink || typeof v.fxLink.id !== 'string' || typeof v.fxLink.fingerprint !== 'string')) ) throw Error( 'Vínculo/selecção cambial não recuperável;original preservado.', ); for (const p of b.payments) if ( !p || !['id', 'reference', 'date', 'amount', 'currency'].every( (k) => typeof p[k as keyof IrpcPayment] === 'string', ) || !['beneficiary', 'remittance', 'withholding_proof'].includes(p.kind) ) throw Error('Pagamento IRPC inválido;originais preservados.'); if ( b.result && (!Array.isArray(b.result.rows) || !Array.isArray(b.result.inputs) || typeof b.result.key !== 'string' || b.result.version !== irpcWithholdingVersion) ) throw Error('Resultado IRPC não recuperável.'); if ( b.payments.some( (p, i) => p.reference.trim() && b.payments.some( (q, j) => i !== j && q.kind === p.kind && q.reference.trim() === p.reference.trim() && q.date === p.date && q.currency === p.currency, ), ) ) throw Error( 'Pagamento/prova já registado:reutilize a referência existente em vez de duplicar.', ); if ( new Set(b.rows.map((r) => r.id)).size !== b.rows.length || new Set(b.payments.map((p) => p.id)).size !== b.payments.length ) throw Error('Identificadores IRPC repetidos.'); } export function irpcBook(d: Record): IrpcBook { const b = map(d)[acquisitionScope(d)] ?? { version: 1, rows: [], payments: [], }; validate(b); return b; } export function saveIrpcBook(d: Record, b: IrpcBook): string { validate(b); return JSON.stringify({ ...store(d), irpcWithholdings: { ...map(d), [acquisitionScope(d)]: b }, }); } export function irpcSources(d: Record) { return [ ...acquisitionBook(d).rows.map((r) => ({ id: 'acquisition:' + r.id, key: closureFingerprint(r), counterparty: r.supplier, documentRef: r.documentRef + (r.documentLine ? ' / linha ' + r.documentLine : ''), description: r.description, amount: r.net, currency: acquisitionBook(d).currency, direction: 'paid' as const, origin: r.origin, })), ...salesBook(d).rows.map((r) => ({ id: 'sale:' + r.id, key: closureFingerprint(r), counterparty: '', documentRef: r.evidence, description: r.description, amount: r.basis === 'net' ? r.amount : '', currency: r.sourceCurrency, direction: 'received' as const, origin: 'projected' as const, })), ]; } export function reuseIrpcSources( d: Record, b: IrpcBook, ): IrpcBook { validate(b); const rows = [...b.rows]; for (const s of irpcSources(d)) if (!rows.some((r) => r.source?.id === s.id)) { const r = { ...newIrpcRow('irpc:' + s.id), counterparty: s.counterparty, documentRef: s.documentRef, description: s.description, base: s.amount, currency: s.currency, direction: s.direction, origin: s.origin, }; rows.push({ ...r, source: { id: s.id, key: s.key, fingerprint: closureFingerprint(r), }, }); } return { ...b, rows }; } function sourceIssue(d: Record, r: IrpcRow) { if (!r.source) return ''; const s = irpcSources(d).find((s) => s.id === r.source!.id); return !s ? 'Operação de origem não está disponível neste contexto;vínculo e edições conservados.' : s.key !== r.source.key ? 'Documento/operação de origem alterado;compare o histórico e reconcilie a divergência antes de calcular.' : ''; } const date = (s: string) => /^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(s) && !Number.isNaN(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; const positive = (f: FiscalFraction) => BigInt(f.numerator) > 0n; export function evaluateIrpc( d: Record, r: IrpcRow, ): IrpcResult { const v: IrpcResult = { id: r.id, status: 'indeterminate', reason: 'Identifique o rendimento e os intervenientes.', period: '', eventDate: '', nature: null, rate: null, base: null, tax: null, settled: null, outstanding: null, direction: r.direction, }; const issue = sourceIssue(d, r) || irpcFxDependency(d, r); if (issue) return { ...v, reason: issue }; if (r.recipientIrpc === 'no') return { ...v, status: 'outside_scope', reason: 'Titular fora de IRPC;sem converter em IRPS ou em isenção.', }; if ( !r.counterparty.trim() || !r.documentRef.trim() || r.recipientIrpc !== 'yes' || r.payerIrpc !== 'yes' || !r.residence || !r.kind || !r.qualificationRef.trim() ) return { ...v, reason: 'Faltam contraparte,documento,qualificação do rendimento,titular IRPC,devedor IRPC ou residência. A via devedor IRPS exige âmbito próprio.', }; if (r.mozambique !== 'yes') return { ...v, reason: 'Falta correspondência territorial aos arts5/67;ser estrangeiro não resolve incidência nem isenção.', }; if (r.specialRelief !== 'none' && r.specialRelief !== 'claimed') return { ...v, reason: 'Convenção,isenção ou dispensa invocada/desconhecida:identificar artigo,período,condições e prova;sem taxa reduzida ou zero automáticos.', }; if ( !r.eventsComplete || r.eventsComplete !== 'yes' || !r.events.length || r.events.some((e) => !date(e.date) || !e.reference.trim()) ) return { ...v, reason: 'Identifique os eventos aplicáveis67(5),incluindo reconhecimento do custo,e confirme a sua completude com suporte;uma factura ou pagamento isolado não fixa o momento.', }; v.eventDate = r.events.map((e) => e.date).sort()[0]; v.period = v.eventDate.slice(0, 7); if (!v.eventDate.startsWith('2026-')) return { ...v, reason: 'Primeiro evento aplicável fora de2026;corresponder versão/período próprio,sem deslocar para o pagamento posterior.', }; try { const converted = convertIrpcAmount(d, r.base, r); v.base = converted.value; v.conversion = converted.evidence; } catch (e) { return { ...v, reason: 'Base do rendimento/conversão por resolver: ' + String(e), }; } if (r.specialRelief === 'claimed') { const relief = evaluateIrpcRelief(r, v.eventDate); if (!relief.applied) return { ...v, reason: relief.reason }; v.treatment = relief.treatment; v.reliefRule = relief.rule; v.rate = relief.rate; v.nature = relief.treatment === 'treaty' ? 'definitive' : 'on_account'; v.reason = relief.reason; v.tax = fraction( BigInt(v.base.numerator) * BigInt(relief.rate), BigInt(v.base.denominator) * 100n, ); v.status = 'calculated'; return v; } const digital = [ 'digital_goods', 'digital_services', 'emoney_commission', ].includes(r.kind); if (digital) { if ( (r.residence === 'nonresident' && r.permanentEstablishment === 'no') || (r.residence === 'nonresident' && r.permanentEstablishment === 'yes' && r.attributable === 'no') ) v.nature = 'definitive'; else return { ...v, reason: 'Para residente ou rendimento imputável aEP,corresponder concretamente a taxa62(5) com as dispensas68,em especial68(d),antes da liquidação. EP desconhecido permanece indeterminado;90dias/serviço digital não criam ausência deEP automática.', }; v.rate = 10; v.reason = 'Taxa10%62(5) no âmbito digital/moeda electrónica demonstrado;retenção definitiva67(3). Procedimento67(7)/remissão62(6) e CER permanecem separados.'; } else { const f = r.ordinary; if (!f || f.nonDigital !== 'yes' || !f.reference.trim()) return { ...v, reason: 'Classifique o rendimento não abrangido pelo ramo digital/moeda electrónica e identifique os factos específicos. Cotejo62/67:Lei34/2007,Lei19/2013 eLei20/2022 agora disponível;sem taxa genérica.', }; const nonresident = r.residence === 'nonresident'; if ( nonresident && r.permanentEstablishment !== 'no' && !( r.permanentEstablishment === 'yes' && ['yes', 'no'].includes(r.attributable) ) ) return { ...v, reason: 'Falta identificar EP e imputação;ausência física ou nacionalidade não resolvem este facto.', }; const attributable = !nonresident || (r.permanentEstablishment === 'yes' && r.attributable === 'yes'); v.nature = r.kind === 'rent' || attributable ? 'on_account' : 'definitive'; if ( ['equipment', 'services'].includes(r.kind) && v.nature === 'on_account' ) return { ...v, reason: 'Rendimento67(1)b/g com imposto por conta:dispensa68(d) identificada,por corresponder no ramo de dispensas;não reter20% nem declarar isenção do rendimento.', }; if (r.kind === 'rent' && f.rentOwnPropertyManagement !== 'no') return { ...v, reason: 'Rendimento predial mantém natureza por conta67(3). Identifique a gestão de imóveis próprios e eventual dispensa68(f)/transparência;apenas este caso depende dessa correspondência.', }; if (r.kind === 'capital') { if (!f.capitalType || f.capitalType === 'other') return { ...v, reason: 'Identifique juros/dividendos não cotados,títulos cotadosBM ou permutas de liquidez;62(3/4) não permite taxa única para capitais.', }; if ( r.residence === 'resident' && ['listed_debt_mz', 'liquidity_repo'].includes(f.capitalType) ) { if (f.capitalSeatAndManagement !== 'yes') return { ...v, reason: 'Art62(3):identifique sede e direcção efectiva nacionais do titular,além da categoria exacta dos títulos/permuta;residência genérica não substitui estes factos.', }; v.nature = 'definitive'; } if (v.nature === 'on_account' && f.capitalType !== 'listed_debt_mz') return { ...v, reason: 'Capitais com imposto por conta:correspondência específica das dispensas68(a/b/c/g),incluindo titular e operação,permanece separada;sem bloquear capitais definitivos independentes.', }; if ( f.capitalType === 'liquidity_repo' && r.residence !== 'resident' ) return { ...v, reason: 'Permutas de liquidez62(3)b exigem entidade com sede/direcção efectiva nacional;caso não residente mantém correspondência própria.', }; } const special = [ 'telecom', 'international_transport', 'connected_installation', 'aircraft_maintenance_charter', 'rural_public_electrification', 'maritime_fishing_cabotage', ].includes(r.kind) || (r.kind === 'capital' && f.capitalType === 'listed_other_mz'); if (special) { if (!nonresident || r.permanentEstablishment !== 'no') return { ...v, reason: 'Taxa especial62(4) refere entidades62(2),não residentes semEP. O caso residente/EP tem correspondência própria;sem copiar10%.', }; if (f.specialFacts !== 'yes') return { ...v, reason: 'Confirme os factos exactos62(4)a–d:conexão dos equipamentos,projecto público rural,pesca/cabotagem ou título cotadoBM que não é dívida/bilhete62(3)a,conforme categoria. Sem taxa especial por descrição genérica.', }; v.rate = 10; v.reason = 'Taxa especial10%62(4),redacçãoLei19/2013,conservada emLei20/2022 eLei12/2025;entidade não residente semEP e categoria específica identificadas. Não é a taxa digital62(5).'; } else { if ( ![ 'royalty', 'equipment', 'rent', 'capital', 'services', ].includes(r.kind) ) return { ...v, reason: 'Natureza fora dos ramos agrupados;identificar artigo e condições próprios,sem taxa por defeito.', }; v.rate = 20; v.reason = (r.kind === 'capital' && r.residence === 'resident' && ['listed_debt_mz', 'liquidity_repo'].includes(f.capitalType) ? 'Retenção liberatória20%62(3)a/b:juros de dívida/bilhetes cotados ou permuta de liquidez qualificados,com sede e direcção efectiva nacionais. ' : '') + 'Taxa20%62(1),com62(2/3) quando aplicável;incidência67(1),natureza67(3). Lei20/2022,p2731,confrontada:as medidas agrícolas terminam31/12/2025 e não são transportadas para2026. Sem aplicar10% digital a rendimentos não digitais.'; } } v.tax = fraction( BigInt(v.base.numerator) * BigInt(v.rate!), BigInt(v.base.denominator) * 100n, ); v.status = 'calculated'; v.reason += ' Base separada do IVA;entrega/prova e liquidação anual mantêm reconciliação própria.'; return v; } function sharedPayments(d: Record, b: IrpcBook) { const used = b.payments.filter((p) => b.rows.some((r) => r.allocations.some((a) => a.paymentId === p.id)), ); return Object.entries(map(d)) .filter(([ctx]) => ctx !== acquisitionScope(d)) .flatMap(([ctx, other]) => { validate(other); const payments = other.payments.filter((q) => used.some( (p) => p.reference.trim() === q.reference.trim() && p.date === q.date && p.currency === q.currency && p.kind === q.kind, ), ); return payments.length ? [ [ ctx, payments, other.rows.flatMap((r) => r.allocations.filter((a) => payments.some((p) => p.id === a.paymentId), ), ), ], ] : []; }); } function key(d: Record, b: IrpcBook) { const fx = [...b.rows, ...b.payments] .filter((r) => r.currency !== 'MZN' && r.fxLink) .map((r) => irpcFxDependency(d, r)); return JSON.stringify([ irpcWithholdingVersion, acquisitionScope(d), b.rows, b.payments, b.rows.map((r) => sourceIssue(d, r)), sharedPayments(d, b), ...(fx.length ? [fx] : []), ...(b.rows.some((r) => r.specialRelief === 'claimed') ? [irpcReliefVersion] : []), ]); } export function irpcCurrent(d: Record, b: IrpcBook) { return !!b.result && b.result.key === key(d, b); } export function recordIrpc( d: Record, b: IrpcBook, now = new Date().toISOString(), ): IrpcBook { validate(b); const rows = b.rows.map((r) => evaluateIrpc(d, r)); const paymentConversions: Record = {}; const allocations = new Map(); const fail = (i: number, reason: string) => { rows[i] = { ...rows[i], status: 'indeterminate', reason, settled: null, outstanding: null, }; }; b.rows.forEach((r, i) => { if (rows[i].status !== 'calculated') return; if (rows[i].treatment === 'dispensed') { if (r.allocations.length) { fail( i, 'Dispensa com entrega/prova associada: reconcilie sem apagar o pagamento nem gerar crédito automático.', ); return; } rows[i].settled = fraction(0n, 1n); rows[i].outstanding = fraction(0n, 1n); return; } if (r.settlement === 'none' && r.allocations.length) { fail( i, 'Ausência de entrega/prova contradiz pagamentos associados;conserve e reconcilie.', ); return; } if (!r.settlement || !r.settlementRef.trim()) { rows[i].reason += ' Entrega à AT/retenção sofrida ainda não reconciliada;não presumir zero pago.'; return; } const parts: FiscalFraction[] = []; const seen = new Set(); for (const a of r.allocations) { const p = b.payments.find((p) => p.id === a.paymentId); try { if ( !p || seen.has(p.id) || (p.kind === 'remittance' && p.currency !== 'MZN') || !date(p.date) || p.date < rows[i].eventDate || !p.reference.trim() || p.kind !== (r.direction === 'paid' ? 'remittance' : 'withholding_proof') ) throw Error(); seen.add(p.id); const amount = decimalFraction(a.amount); paymentConversions[p.id] = convertIrpcAmount( d, p.amount, p, ).evidence; parts.push(amount); allocations.set(p.id, [ ...(allocations.get(p.id) ?? []), amount, ]); } catch (e) { fail( i, 'Pagamento/prova ausente,incoerente,repetido ou de outra natureza:pagamento ao fornecedor e imposto IVA não são entrega IRPC. ' + String(e), ); return; } } const settled = addFractions(parts); if (positive(subtractFractions(settled, rows[i].tax!))) { fail( i, 'Valor associado excede a retenção calculada;reconcilie excesso sem gerar crédito automático.', ); return; } rows[i].settled = settled; rows[i].outstanding = subtractFractions(rows[i].tax!, settled); }); for (const [id, parts] of allocations) { const p = b.payments.find((p) => p.id === id)!; let excess = false; try { const elsewhere: FiscalFraction[] = []; for (const [ctx, other] of Object.entries(map(d))) { if (ctx === acquisitionScope(d)) continue; validate(other); for (const q of other.payments.filter( (q) => q.reference.trim() === p.reference.trim() && q.date === p.date && q.kind === p.kind && q.currency === p.currency, )) { if ( q.amount !== p.amount || (q.currency !== 'MZN' && JSON.stringify(q.fxSelection) !== JSON.stringify(p.fxSelection)) ) throw Error('Montantes/conversões divergentes'); for (const r of other.rows) for (const a of r.allocations.filter( (a) => a.paymentId === q.id, )) elsewhere.push(decimalFraction(a.amount)); } } excess = positive( subtractFractions( addFractions([...parts, ...elsewhere]), convertIrpcAmount(d, p.amount, p).value, ), ); } catch { excess = true; } if (excess) b.rows.forEach((r, i) => { if (r.allocations.some((a) => a.paymentId === id)) fail( i, 'Mesmo pagamento utilizado além do montante disponível;reparta sem duplicação.', ); }); } // An identical source document/income cannot become a second tax just by adding a row. b.rows.forEach((r, i) => { if ( b.rows.some( (s, j) => j !== i && ((r.source && s.source?.id === r.source.id) || (r.documentRef.trim() && s.documentRef.trim() === r.documentRef.trim() && s.counterparty.trim() === r.counterparty.trim() && s.direction === r.direction)), ) ) { fail( i, 'Documento/rendimento repetido neste livro;identifique parcelas próprias antes de somar,sem duplicação.', ); } }); return { ...b, history: b.result ? [...(b.history ?? []), structuredClone(b.result)] : b.history, result: { version: irpcWithholdingVersion, key: key(d, b), calculatedAt: now, inputs: structuredClone(b.rows), payments: structuredClone(b.payments), rows, paymentConversions, }, }; } const amount = (v: FiscalFraction | null) => { if (!v) return 'Indeterminado'; const p = fractionPresentation(v); return (p.approximate ? '≈' : '') + p.decimal + ' MZN'; }; export function irpcReportRows( d: Record, ): Array<[string, string, string, string]> { try { const b = irpcBook(d), r = b.result; if (!r) return mrftaReportRows(b, false); return [ ...r.rows.map((v): [string, string, string, string] => { const i = r.inputs.find((i) => i.id === v.id)!; return [ 'IRPC — retenções', `${i.documentRef} — ${i.counterparty}`, `${{ calculated: 'Calculado', indeterminate: 'Indeterminado', outside_scope: 'Fora deste âmbito' }[v.status]};tratamento:${v.treatment === 'dispensed' ? 'Dispensa de retenção' : v.treatment === 'treaty' ? 'Limite convencional' : 'Retenção interna'};enquadramento: ${irpcKinds.find(([k]) => k === i.kind)?.[1] ?? i.kind};${i.direction === 'paid' ? 'retida a entregar' : 'sofrida'};base:${amount(v.base)};taxa:${v.treatment === 'dispensed' ? 'Dispensa (sem retenção)' : (v.rate ?? 'por corresponder') + '%'};retenção:${amount(v.tax)};entrega/prova:${amount(v.settled)};saldo:${amount(v.outstanding)};${v.reason}`, `${acquisitionScope(d)};${i.origin};${v.period};evento:${v.eventDate};${v.nature === 'on_account' ? 'Imposto por conta (sem crédito anual automático)' : v.nature === 'definitive' ? 'Retenção definitiva' : 'Natureza por determinar'};${irpcCurrent(d, b) ? 'Entradas correspondentes' : 'Histórico;entradas/origem divergentes'};dispensa/convenção:${irpcReliefEvidenceLabel(i)};reconciliação:${i.settlementRef};comprovativos:${i.allocations .map((a) => { const p = r.payments.find( (p) => p.id === a.paymentId, ); return p ? `${p.reference}/${p.date}:parcela${a.amount}MZN de${p.amount}${p.currency};${r.paymentConversions?.[p.id] ? irpcFxEvidenceLabel(r.paymentConversions[p.id]!) : 'sem conversão usada'}` : 'referência por resolver'; }) .join( ' | ', )};${r.calculatedAt};${v.conversion ? 'Câmbio:' + irpcFxEvidenceLabel(v.conversion) + ';' : ''}${irpcWithholdingSource}`, ]; }), ...mrftaReportRows(b, irpcCurrent(d, b)), ]; } catch (e) { return [ [ 'IRPC — retenções', 'Livro não recuperável', String(e), 'Originais e histórico preservados.', ], ]; } } export function irpcPreparationHtml(d: Record): string { const b = irpcBook(d); if (!b.result || !irpcCurrent(d, b)) throw Error('Calcule e guarde as entradas actuais antes de preparar.'); const esc = (v: unknown) => String(v).replace( /[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[c]!, ); return ( 'IRPC — preparação das retenções

Preparação IRPC — L5

Mapa de trabalho;sem submissão,recepção ou aceitação AT. MRFTA19/39:quadro1 retenção;quadro2 mês/ano único por declaração;quadro7 base/taxa/imposto. CER e correspondência das rubricas digitais/moeda electrónica por esclarecer;67(7)remete62(6),mas taxa nova em62(5). Não preencher assinatura ou uso dos serviços. Não consolidar períodos diferentes nem transformar retenções sofridas em imposto a entregar.

' + irpcReportRows(d) .map( (r) => '

' + esc(r[1]) + '

' + esc(r[2]) + '

' + esc(r[3]) + '
', ) .join('') + '

Instruções do exemplar:prazo nominal dia20 do mês seguinte à dedução,sem ajuste de calendário/prorrogação ou procedimento2026 presumidos. Juros não calculados. GuiaM/39 anterior preservada,sem actualizar remissões do original.

' ); }