import { canonicalParcel } from './fiscal-proof-parcel.ts'; import type { NormalBook } from './irpc-normal.ts'; import { economicTaxProof, economicDistributionIdentity, economicIncome, validateEconomic, type EconomicPosition, } from './irpc-economic-credit.ts'; import { creditMoney, creditLimit, creditCompare, } from './irpc-international-credit.ts'; import { addFractions, fractionPresentation, subtractFractions, fraction, type FiscalFraction, } from './fiscal-rational.ts'; export type EconomicInclusion = { source: EconomicPosition; profitAlreadyIncluded: string; creditAlreadyIncluded: string; reference: string; }; export type EconomicInclusions = { version: 1; rows: EconomicInclusion[] }; export const economicInclusionVersion = 'IRPC-economic-inclusion/1'; export function validateEconomicInclusions(input: EconomicInclusions) { if ( input?.version !== 1 || !Array.isArray(input.rows) || input.rows.some( (r) => !r || typeof r.profitAlreadyIncluded !== 'string' || typeof r.creditAlreadyIncluded !== 'string' || typeof r.reference !== 'string', ) ) throw Error('Inclusões económicas não recuperáveis.'); validateEconomic({ version: 1, state: 'known', reference: economicInclusionVersion, rows: input.rows.map((r) => r.source), }); } export function economicInclusionMoney(value: FiscalFraction): string { const presentation = fractionPresentation(value); return presentation.approximate ? value.numerator + '/' + value.denominator : presentation.decimal; } export function calculateEconomicInclusions( input: EconomicInclusions, subject: string, year: string, ) { validateEconomicInclusions(input); const pending: string[] = []; const effects: { id: string; topic: string; amount: FiscalFraction | null; effect: FiscalFraction | null; reason: string; }[] = []; const seen = new Set(); for (const row of input.rows) { const r = row.source; try { if ( !r || !['ordinary_profit', 'liquidation', 'association'].includes( r.kind, ) || !r.id.trim() || !/^\d{9}$/.test(r.payerNuit) || r.subjectNuit !== subject || r.year !== year || year !== '2026' || r.recipientResident !== 'yes' || r.payerResident !== 'yes' || r.payerIrpc !== 'yes' || r.payerExempt !== 'no' || r.article40 !== 'no' || !r.qualificationReference?.trim() || !r.article40Reference?.trim() || !row.reference?.trim() || !r.distributionDocument?.trim() || !r.distributionParcel?.trim() || !/^2026-\d{2}-\d{2}$/.test(r.distributionDate) || new Date(r.distributionDate).toISOString().slice(0, 10) !== r.distributionDate ) throw Error( 'Falta qualificação, origem, titular/exercício ou correspondência da inclusão; art40 e outros âmbitos separados.', ); const identity = r.distributionDocument.trim().toUpperCase() + '|' + r.distributionParcel.trim().toUpperCase(); const taxIdentity = JSON.stringify([ canonicalParcel(r.taxIssuer), canonicalParcel(r.taxDocument), canonicalParcel(r.taxParcel), ]); const distributionIdentity = economicDistributionIdentity(r); if ( seen.has(distributionIdentity) || seen.has('tax|' + taxIdentity) ) throw Error( 'Origem/parcela económica repetida; consolide antes de corrigir a base.', ); seen.add(distributionIdentity); seen.add('tax|' + taxIdentity); const qualification = economicIncome(r); const tax = economicTaxProof(r).value; const nominal = fraction( BigInt(tax.numerator) * 3n, BigInt(tax.denominator) * 5n, ); if ( creditCompare(qualification.income, fraction(0n)) === 0n && creditCompare(nominal, fraction(0n)) > 0n ) throw Error( 'Imposto subjacente positivo sem rendimento elegível para crédito económico.', ); const specialReference = qualification.liquidation ? `${qualification.liquidation.version}; rendimento elegível ${economicInclusionMoney(qualification.liquidation.eligibleIncome)}; excesso com natureza de mais-valia ${economicInclusionMoney(qualification.liquidation.excessCapitalGain)} incluído no rendimento total a reconciliar. ${qualification.liquidation.reference}` : qualification.association ? `${qualification.association.version}; ${qualification.association.kind}; ${qualification.association.reference}` : ''; const targets = [creditMoney(r.income), nominal]; const included = [ creditMoney(row.profitAlreadyIncluded), creditLimit(row.creditAlreadyIncluded), ]; if ( r.incomeIncluded === 'yes' && creditCompare(included[0], targets[0]) < 0n ) throw Error( 'A origem L8 declara os lucros já incluídos; o montante anterior indicado contradiz essa declaração. Compare e corrija a origem antes de acrescentar novamente.', ); if ( r.grossUpIncluded.trim() && creditCompare(creditLimit(r.grossUpIncluded), included[1]) !== 0n ) throw Error( 'O crédito já acrescido indicado não corresponde à origem L8; reconcilie a diferença antes de alterar a base.', ); targets.forEach((target, index) => { const effect = subtractFractions(target, included[index]); economicInclusionMoney(effect); effects.push({ id: identity + ':' + index, topic: 'incomeCorrections', amount: fraction( BigInt(effect.numerator) < 0n ? -BigInt(effect.numerator) : BigInt(effect.numerator), BigInt(effect.denominator), ), effect, reason: `${economicInclusionVersion}${[target, included[index], effect].some((v) => (BigInt(v.numerator) * 100n) % BigInt(v.denominator) !== 0n) ? '; IRPC-economic-inclusion-exact/1: fracções conservadas, sem arredondamento fiscal' : ''}; CIRPC64(1–2)/53(1)a; ${index ? 'acréscimo nominal' : 'lucros'}: alvo ${economicInclusionMoney(target)}; já considerado ${economicInclusionMoney(included[index])}; diferença ${economicInclusionMoney(effect)}. ${row.reference}${specialReference ? ' ' + specialReference : ''}`, }); }); } catch (e) { pending.push(String(e)); } } return { version: economicInclusionVersion, input: structuredClone(input), effects, pending, net: addFractions(effects.flatMap((e) => (e.effect ? [e.effect] : []))), }; } export function prepareEconomicInclusion( book: NormalBook, row: EconomicInclusion, ): NormalBook { if ( book.special || book.periodsFx || (book.schedules && book.schedules.period.kind !== 'calendar') || book.scope !== 'resident_general' || book.year !== '2026' || !book.base || !book.profit ) throw Error( 'Prepare primeiro o lucro e a base L6 ordinária do sujeito/ano civil2026/MZN; períodos especiais e conversões têm correspondência própria.', ); const old = book.economicInclusions ?? { version: 1 as const, rows: [] }; const rows = old.rows.filter( (p) => economicDistributionIdentity(p.source) !== economicDistributionIdentity(row.source), ); const next: EconomicInclusions = { version: 1, rows: [...rows, structuredClone(row)], }; const previous = calculateEconomicInclusions( old, book.base.subjectNuit, book.year, ), prepared = calculateEconomicInclusions( next, book.base.subjectNuit, book.year, ); if (previous.pending.length || prepared.pending.length) throw Error([...previous.pending, ...prepared.pending].join(' ')); const topic = book.profit.topics.incomeCorrections; if ( !topic.reference.trim() || !['known', 'absent'].includes(topic.state) || (topic.state === 'absent' && topic.amount.trim()) ) throw Error( 'Reconcilie as outras correcções aos proveitos antes de substituir parcelas.', ); const current = topic.state === 'absent' ? fraction(0n) : old.rows.length ? creditLimit(topic.amount) : creditMoney(topic.amount); const remaining = subtractFractions( current, addFractions( previous.effects.flatMap((e) => (e.amount ? [e.amount] : [])), ), ); if (BigInt(remaining.numerator) < 0n) throw Error( 'Montante de correcções inferior às parcelas anteriores; compare antes de substituir.', ); const amount = addFractions([ remaining, addFractions( prepared.effects.flatMap((e) => (e.amount ? [e.amount] : [])), ), ]); return { ...book, economicInclusions: next, profit: { ...book.profit, topics: { ...book.profit.topics, incomeCorrections: { state: 'known', amount: economicInclusionMoney(amount), reference: topic.reference.includes(row.reference) ? topic.reference : topic.reference + '\n' + row.reference, }, }, }, }; }