import { creditLimit, creditCompare } from './irpc-international-credit.ts'; import type { NormalDepreciationModel } from './irpc-normal-depreciation.ts'; import type { NormalCostsModel } from './irpc-normal-costs.ts'; import type { SchedulesModel } from './irpc-normal-schedules.ts'; import { addFractions, decimalFraction, fraction, type FiscalFraction, } from './fiscal-rational.ts'; export const normalProfitVersion = 'IRPC-normal-adjustments/2'; export type ProfitEvidence = { state: '' | 'known' | 'absent'; amount: string; reference: string; }; export const profitTopics = [ [ 'positivePatrimonial', 'Variações patrimoniais positivas fora do resultado', ], [ 'negativePatrimonial', 'Variações patrimoniais negativas fora do resultado', ], [ 'incomeCorrections', 'Outras correcções aos proveitos, incluindo exclusões', ], [ 'expenseCorrections', 'Outras correcções aos custos, além dos acréscimos registados', ], [ 'timing', 'Diferenças de periodização e de critérios contabilísticos/fiscais', ], ['special', 'Operações especiais, benefícios e regimes fora deste recorte'], ] as const; export type ProfitTopic = (typeof profitTopics)[number][0]; export type NormalProfitInput = { version: 1; completenessReference: string; lines: Array<{ code: string; state: '' | 'known' | 'absent'; reference: string; }>; topics: Record; }; export const newNormalProfitInput = (): NormalProfitInput => ({ version: 1, completenessReference: '', lines: [], topics: Object.fromEntries( profitTopics.map(([k]) => [ k, { state: '', amount: '', reference: '' }, ]), ) as NormalProfitInput['topics'], }); export function validateNormalProfit(i: NormalProfitInput) { if ( !i || i.version !== 1 || typeof i.completenessReference !== 'string' || !Array.isArray(i.lines) || i.lines.some( (l) => !l || typeof l.code !== 'string' || typeof l.reference !== 'string' || !['', 'known', 'absent'].includes(l.state), ) || new Set(i.lines.map((l) => l.code)).size !== i.lines.length || !i.topics || typeof i.topics !== 'object' || Array.isArray(i.topics) ) throw Error('Conferência do lucro tributável não recuperável.'); for (const [k] of profitTopics) { const t = i.topics[k]; if ( !t || !['', 'known', 'absent'].includes(t.state) || typeof t.amount !== 'string' || typeof t.reference !== 'string' ) throw Error( 'Estado documental do lucro tributável não recuperável.', ); } } type Source = { currency: string; periods: number; accountingLines: Array<{ code: string; origin: string; accountingEffect: 'income' | 'expense'; total: string; }>; }; export type NormalProfitModel = { input: NormalProfitInput; lines: Array<{ code: string; label: string; amount: string; addedBack: FiscalFraction; remainder: FiscalFraction | null; state: ProfitEvidence['state']; reference: string; reason: string; }>; topics: Array< ProfitEvidence & { key: ProfitTopic; label: string; value: FiscalFraction | null; reason: string; } >; pending: string[]; taxableProfit: FiscalFraction | null; source: string; }; const zero = () => fraction(0n, 1n); function money(s: string, exact = false) { if (exact) { const v = creditLimit(s.startsWith('-') ? s.slice(1) : s); return s.startsWith('-') ? fraction(-BigInt(v.numerator), BigInt(v.denominator)) : v; } if (!/^-?\d+(?:\.\d{1,2})?$/.test(s)) throw Error('Montante inválido'); const v = decimalFraction(s.startsWith('-') ? s.slice(1) : s); return s.startsWith('-') ? fraction(-BigInt(v.numerator), BigInt(v.denominator)) : v; } const units = (f: FiscalFraction) => (BigInt(f.numerator) * 100n) / BigInt(f.denominator); const difference = (a: FiscalFraction, b: FiscalFraction) => fraction( BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator), BigInt(a.denominator) * BigInt(b.denominator), ); export function closeNormalProfit( input: NormalProfitInput, source: Source | null, rows: Array<{ sourceCode: string; adjustment: FiscalFraction | null; status: string; }>, accounting: FiscalFraction | null, additions: FiscalFraction, contextReady: boolean, schedules?: SchedulesModel, costs?: NormalCostsModel, depreciation?: NormalDepreciationModel, economic?: { effects: { id: string; topic: string; amount: FiscalFraction | null; effect: FiscalFraction | null; reason: string; }[]; net: FiscalFraction; pending: string[]; }, ): NormalProfitModel { validateNormalProfit(input); const pending: string[] = [], lines: NormalProfitModel['lines'] = [], topics: NormalProfitModel['topics'] = []; if (!contextReady) pending.push( 'Âmbito,exercício,moeda ou correspondência anual ainda insuficientes.', ); if (!input.completenessReference.trim()) pending.push( 'Documente que o plano e os ajustamentos abrangem o sujeito passivo/exercício completo;uma oportunidade isolada não equivale automaticamente ao conjunto da empresa.', ); if (rows.some((r) => r.status !== 'calculated')) pending.push( 'Existem ajustamentos registados por resolver;acréscimos independentes conservados.', ); if (!source) pending.push('Falta resultado financeiro reconciliado.'); for (const l of source?.accountingLines ?? []) { const review = input.lines.find((r) => r.code === l.code), state = review?.state ?? '', reference = review?.reference ?? ''; const addedBack = addFractions( rows .filter((r) => r.sourceCode === l.code && r.adjustment) .map((r) => r.adjustment!), ); let remainder: FiscalFraction | null = null; const issues: string[] = []; try { const amount = money(l.total); remainder = difference(amount, addedBack); if (units(amount) < 0n || units(remainder) < 0n) issues.push( 'Valor negativo ou acréscimos superiores à origem;reconcilie a linha sem compensação presumida.', ); if (!state || !reference.trim()) issues.push('Conferência fiscal e suporte da linha em falta.'); if ( state === 'absent' && (units(amount) !== 0n || units(addedBack) !== 0n) ) issues.push( 'Ausência declarada contradiz o montante conhecido da origem.', ); if ( state === 'known' && l.code === 'depreciation' && units(amount) !== 0n && (depreciation ? depreciation.adjustment : schedules?.depreciation) == null ) issues.push( 'Depreciação contabilística conhecida;amortização fiscal por corresponder,não presumir igualdade.', ); if ( state === 'known' && l.code === 'interest_expense' && units(remainder) !== 0n && schedules?.interest == null ) issues.push( 'Juros financeiros remanescentes conhecidos;dedutibilidade e eventuais limites por corresponder. Só os juros compensatórios já acrescidos estão cobertos.', ); if ( ![ 'revenue', 'cost_of_sales', 'operating_expenses', 'personnel', 'depreciation', 'interest_expense', ].includes(l.code) ) issues.push( 'Linha financeira sem correspondência neste recorte.', ); } catch { issues.push('Montante da linha por reconciliar.'); } const reason = issues.join(' ') || (state === 'absent' ? 'Ausência expressamente declarada e compatível com a origem zero.' : units(remainder ?? zero()) === 0n ? 'Valor conhecido,integralmente corrigido ou igual a zero;sem presumir ausência.' : l.accountingEffect === 'income' ? 'Proveito ordinário reconhecido no exercício,qualificado e documentado nos termos dos artigos18/20.' : 'Parcela remanescente qualificada como custo ordinário indispensável e documentado do artigo22,sem exclusões/limites adicionais declarados.'); if (issues.length) pending.push(l.origin + ': ' + reason); lines.push({ code: l.code, label: l.origin, amount: l.total, addedBack, remainder, state, reference, reason, }); } for (const l of input.lines) if (!source?.accountingLines.some((s) => s.code === l.code)) pending.push('Conferência sem linha de origem actual: ' + l.code); for (const [key, label] of profitTopics) { const t = input.topics[key]; let value: FiscalFraction | null = null, reason = ''; if (!t.state || !t.reference.trim()) reason = 'Informação em falta;sem zero presumido.'; else if (t.state === 'absent') { if (t.amount.trim()) reason = 'Ausência declarada com montante preenchido;reconcilie os dois estados.'; else { value = zero(); reason = 'Ausência expressamente declarada com suporte;parcela zero apenas por esta declaração.'; } } else { try { value = money( t.amount, key === 'incomeCorrections' && !!economic, ); if (BigInt(value.numerator) !== 0n) reason = 'Montante conhecido,mas tratamento específico ainda por corresponder;conservado sem aplicação presumida.'; else reason = 'Valor conhecido zero com suporte;distinto de ausência declarada.'; } catch { reason = 'Montante conhecido por preencher ou inválido.'; } } const mapped = [ ...(schedules?.effects ?? []), ...(costs?.effects ?? []), ...(economic?.effects ?? []), ].filter((e) => e.topic === key); const mappedAmount = addFractions( mapped.flatMap((e) => (e.amount ? [e.amount] : [])), ); const covered = (schedules || costs || economic) && mapped.length > 0 && mapped.every((e) => e.effect !== null) && value !== null && t.state === 'known' && (economic && key === 'incomeCorrections' ? creditCompare(mappedAmount, value) === 0n : units(mappedAmount) === units(value)); if (covered) reason = 'Montante reconciliado com as regras do mapa complementar; efeito fiscal discriminado por linha.'; if ( value === null || (BigInt(value.numerator) !== 0n && !covered) || (t.state === 'absent' && !!t.amount.trim()) || (mapped.length > 0 && !covered) ) pending.push( label + ': ' + (mapped.length && !covered ? 'Mapa complementar não reconciliado com este estado/montante. ' : '') + reason, ); topics.push({ ...structuredClone(t), key, label, value, reason }); } if (schedules) pending.push(...schedules.pending); if (costs) pending.push(...costs.pending); if (economic) pending.push(...economic.pending); if (depreciation && !schedules) pending.push(...depreciation.pending); return { input: structuredClone(input), lines, topics, pending, taxableProfit: pending.length === 0 && accounting ? addFractions([ accounting, additions, ...(economic ? [economic.net] : []), ...(schedules ? [schedules.net] : []), ...(costs ? [costs.net] : []), ...(depreciation && !schedules && depreciation.adjustment ? [depreciation.adjustment] : []), ]) : null, source: schedules ? 'CIRPC17/18/20/21/22/24/36; Decreto72/2013 art3–7. Mapa complementar de efeitos com sinal reconciliado por tema, juros financeiros ordinários e quotas anuais constantes documentadas. Simulação emMZN/2026; benefícios e casos especiais separados emL7, saldo anual emL8.' : 'CIRPC17(1)/18/20/22/36(1)c;recorte anual2026,residente geral,MZN. Resultado antes de imposto + acréscimos36(1)c;demais correcções/variações sem efeito apenas quando ausência ou zero conhecidos expressamente suportados. Receita/custos ordinários classificados pelo utilizador;depreciação fiscal e juros financeiros remanescentes fora deste percurso. Lucro tributável simulado,não matéria colectável,colecta ou declaraçãoM20/22.', }; }