import { addFractions, decimalFraction, fraction, subtractFractions, type FiscalFraction, } from './fiscal-rational.ts'; import type { ProfitEvidence } from './irpc-normal-profit.ts'; export const normalBaseVersion = 'IRPC-normal-adjustments/3'; type Choice = '' | 'yes' | 'no'; export type NormalLoss = { id: string; year: string; subjectNuit: string; assessed: string; previouslyUsed: string; requested: string; reference: string; kind: '' | 'ordinary' | 'transfer' | 'other'; annualPeriods: Choice; generalActivity: Choice; balanceConfirmed: Choice; }; export type NormalBaseInput = { version: 1; subjectNuit: string; identityReference: string; method: '' | 'direct' | 'indirect'; methodReference: string; lossesState: '' | 'known' | 'absent'; lossesReference: string; unchangedObject: Choice; objectReference: string; losses: NormalLoss[]; benefits: ProfitEvidence; }; export const newNormalBaseInput = (): NormalBaseInput => ({ version: 1, subjectNuit: '', identityReference: '', method: '', methodReference: '', lossesState: '', lossesReference: '', unchangedObject: '', objectReference: '', losses: [], benefits: { state: '', amount: '', reference: '' }, }); export const newNormalLoss = (id: string): NormalLoss => ({ id, year: '', subjectNuit: '', assessed: '', previouslyUsed: '', requested: '', reference: '', kind: '', annualPeriods: '', generalActivity: '', balanceConfirmed: '', }); export function validateNormalBase(i: NormalBaseInput) { if ( !i || i.version !== 1 || [ 'subjectNuit', 'identityReference', 'methodReference', 'lossesReference', 'objectReference', ].some((k) => typeof i[k as 'subjectNuit'] !== 'string') || !['', 'direct', 'indirect'].includes(i.method) || !['', 'known', 'absent'].includes(i.lossesState) || !['', 'yes', 'no'].includes(i.unchangedObject) || !Array.isArray(i.losses) || new Set(i.losses.map((l) => l.id)).size !== i.losses.length || !i.benefits || !['', 'known', 'absent'].includes(i.benefits.state) || typeof i.benefits.amount !== 'string' || typeof i.benefits.reference !== 'string' ) throw Error('Preparação da matéria colectável não recuperável.'); for (const l of i.losses) if ( !l || Object.keys(newNormalLoss('')).some( (k) => typeof l[k as keyof NormalLoss] !== 'string', ) || !['', 'ordinary', 'transfer', 'other'].includes(l.kind) || ['annualPeriods', 'generalActivity', 'balanceConfirmed'].some( (k) => !['', 'yes', 'no'].includes(l[k as 'annualPeriods']), ) ) throw Error('Perda fiscal não recuperável;originais conservados.'); } export type NormalBaseModel = { input: NormalBaseInput; taxableProfit: FiscalFraction | null; requestedLosses: FiscalFraction; deductedLosses: FiscalFraction | null; benefitDeduction: FiscalFraction | null; taxableBase: FiscalFraction | null; losses: Array< NormalLoss & { available: FiscalFraction | null; requestedAmount: FiscalFraction | null; deducted: FiscalFraction | null; remaining: FiscalFraction | null; lastOrdinaryYear: number | null; status: 'eligible' | 'excluded' | 'indeterminate'; reason: string; } >; pending: string[]; source: string; }; const zero = () => fraction(0n); function amount(s: string) { if (!/^\d{1,18}(?:\.\d{1,2})?$/.test(s)) throw Error(); return decimalFraction(s); } const units = (f: FiscalFraction) => (BigInt(f.numerator) * 100n) / BigInt(f.denominator); export function calculateNormalBase( input: NormalBaseInput, profit: FiscalFraction | null, year: string, currentLossReference?: string, short = false, ): NormalBaseModel { validateNormalBase(input); const i = structuredClone(input), pending: string[] = [], losses: NormalBaseModel['losses'] = []; if (year !== '2026') pending.push('Matéria colectável delimitada ao exercício2026.'); if (!/^\d{9}$/.test(i.subjectNuit) || !i.identityReference.trim()) pending.push( 'Identifique NUIT e suporte do sujeito passivo do lucro e das perdas.', ); if (i.method !== 'direct' || !i.methodReference.trim()) pending.push( 'Método declarativo/directo e suporte por identificar;apuramento indirecto fora deste recorte (15(2),41(2)).', ); if (!profit) pending.push( 'Lucro tributável actual indeterminado;nenhuma dedução transforma informação em falta em zero.', ); else if (BigInt(profit.numerator) < 0n && !currentLossReference?.trim()) pending.push( 'Resultado fiscal negativo conhecido;tratamento da perda do próprio exercício fora deste recorte,sem matéria colectável zero ou reporte automático.', ); if (!i.lossesState || !i.lossesReference.trim()) pending.push( 'Situação e completude das perdas anteriores em falta;sem ausência presumida.', ); if (i.lossesState === 'absent' && i.losses.length) pending.push( 'Ausência declarada de perdas contradiz os registos existentes;reconcilie sem apagar histórico.', ); if (i.lossesState === 'known' && !i.losses.length) pending.push( 'Perdas declaradas conhecidas sem discriminação por exercício.', ); if ( i.losses.some( (l) => !/^20\d{2}$/.test(l.year) || Number(l.year) + 5 >= Number(year), ) && (!i.unchangedObject || !i.objectReference.trim()) ) pending.push( 'Documente manutenção ou alteração do objecto/natureza da actividade no termo do exercício (41(7)).', ); for (const l of i.losses) { const r: NormalBaseModel['losses'][number] = { ...l, available: null, requestedAmount: null, deducted: null, remaining: null, lastOrdinaryYear: null, status: 'indeterminate', reason: '', }; const issues: string[] = []; if (!l.reference.trim()) issues.push( 'Documento de apuramento/saldo fiscal por identificar.', ); if (l.subjectNuit !== i.subjectNuit || !/^\d{9}$/.test(l.subjectNuit)) issues.push('Perda não reconciliada com o mesmo sujeito passivo.'); if (!/^20\d{2}$/.test(l.year) || Number(l.year) >= Number(year)) issues.push( 'Exercício da perda deve ser anterior ao exercício de utilização;perda actual não é dedução a si própria.', ); if ( i.losses.some( (q) => q.id !== l.id && q.year === l.year && q.subjectNuit === l.subjectNuit, ) ) issues.push( 'Saldo anual duplicado;reconcilie um saldo por sujeito/exercício.', ); if (l.kind !== 'ordinary') issues.push( 'Transmissão/fusão ou outra origem exige correspondência própria;sem aplicação automática de41-A.', ); if (l.annualPeriods !== 'yes') issues.push( 'Contagem por exercícios anuais ordinários não demonstrada;períodos especiais/curtos (41(5)) fora deste recorte.', ); if (l.generalActivity !== 'yes') issues.push( 'Origem no regime geral,fora de transparência/isenção parcial ou actividade com redução,por demonstrar (41(4/6)).', ); if (l.balanceConfirmed !== 'yes') issues.push( 'Saldo fiscal corrigido e utilizações anteriores por reconciliar (41(3));não usar perda contabilística como fiscal.', ); try { const assessed = amount(l.assessed), used = amount(l.previouslyUsed); if (units(used) > units(assessed)) throw Error(); r.available = subtractFractions(assessed, used); } catch { issues.push( 'Apuramento fiscal e utilização anterior devem ser montantes conhecidos não negativos e coerentes.', ); } try { r.requestedAmount = amount(l.requested); if (r.available && units(r.requestedAmount) > units(r.available)) issues.push( 'Utilização proposta excede saldo fiscal disponível.', ); } catch { issues.push( 'Identifique utilização proposta,não negativa,mesmo quando zero.', ); } if (l.annualPeriods === 'yes' && /^20\d{2}$/.test(l.year)) r.lastOrdinaryYear = Number(l.year) + 5; if (!issues.length) { if ( r.lastOrdinaryYear! < Number(year) || i.unchangedObject === 'no' ) { r.status = 'excluded'; r.reason = r.lastOrdinaryYear! < Number(year) ? 'Prazo dos cinco exercícios ordinários posteriores expirado (41(1));saldo não utilizável neste exercício.' : 'Objecto/natureza alterados conforme declaração;dedução41(1)não aplicável (41(7)).'; if (units(r.requestedAmount!) !== 0n) issues.push( 'Perda excluída com utilização positiva proposta;corrija a proposta sem dedução presumida.', ); } else { r.status = 'eligible'; r.reason = 'Saldo fiscal ordinário documentado,mesmo sujeito,cinco exercícios posteriores;utilização escolhida explicitamente,sem ordemFIFO ou percentagem presumida.'; } } if (issues.length) { r.status = 'indeterminate'; r.reason = issues.join(' '); pending.push(l.year + ': ' + r.reason); } losses.push(r); } if ( short && losses.some( (l) => l.requestedAmount && BigInt(l.requestedAmount.numerator) > 0n, ) ) pending.push( 'Período curto:sequência dos períodos posteriores por reconciliar;41(5) exclui da contagem o período de transição7(4)d inferior a6meses,sem agregar todos os períodos curtos ou presumir prazo por ano civil.', ); const requestedLosses = addFractions( losses .filter((l) => l.status === 'eligible') .map((l) => l.requestedAmount!), ); if ( profit && BigInt(requestedLosses.numerator) * BigInt(profit.denominator) > (BigInt(profit.numerator) > 0n ? BigInt(profit.numerator) * BigInt(requestedLosses.denominator) : 0n) ) pending.push( 'Utilizações propostas excedem o lucro tributável disponível;reparta explicitamente sem criar base negativa ou escolher uma ordem de perdas.', ); let benefitDeduction: FiscalFraction | null = null; const benefit = i.benefits; if (!benefit.state || !benefit.reference.trim()) pending.push( 'Benefícios/deduções à matéria colectável por identificar (15(4));créditos à colecta e retenções não são esta dedução.', ); else if (benefit.state === 'absent') { if (benefit.amount.trim()) pending.push( 'Ausência de benefícios contradiz montante preenchido.', ); else benefitDeduction = zero(); } else { try { const value = amount(benefit.amount); if (units(value) !== 0n) pending.push( 'Benefício de montante conhecido:regra,condições,limite e ordem próprios por implementar;sem subtracção livre do valor.', ); else benefitDeduction = value; } catch { pending.push( 'Montante conhecido das deduções por preencher ou inválido.', ); } } const ready = pending.length === 0 && profit !== null, deductedLosses = ready ? requestedLosses : null, taxableBase = ready ? subtractFractions( subtractFractions( BigInt(profit!.numerator) < 0n ? zero() : profit!, requestedLosses, ), benefitDeduction!, ) : null; for (const l of losses) { if (l.available) { l.deducted = ready ? l.status === 'eligible' ? l.requestedAmount! : zero() : null; l.remaining = l.deducted ? subtractFractions(l.available, l.deducted) : null; } } return { input: i, taxableProfit: profit, requestedLosses, deductedLosses, benefitDeduction, taxableBase, losses, pending, source: 'CIRPC15(1)a/15(2–4),41(1–7),Lei34/2007 PDF5/p768(59),PDF12/p768(66). Recorte2026,residente geral,método declarativo,perdas próprias em exercícios anuais ordinários;cinco exercícios posteriores,sem transferência41-A,regimes especiais ou benefícios positivos implementados. Pedido de utilização expresso e limitado ao saldo/lucro;semFIFO obrigatório ou limite percentual inventado. Matéria colectável simulada,não colecta/imposto a pagar,semM20/M22/submissão ou consumo persistente do saldo oficial.', }; }