import { addFractions, fraction, type FiscalFraction, } from './fiscal-rational.ts'; import { convertIrpcAmount, irpcFxDependency, type IrpcFxAmount, type IrpcFxEvidence, irpcFxEvidenceLabel, } from './irpc-fx.ts'; import type { rawNormalFinancialSource } from './irpc-normal.ts'; export const normalPeriodFxVersion = 'IRPC-normal-adjustments/8'; export type NormalPeriodFxInput = { version: 1; kind: '' | 'annual' | 'start' | 'cessation' | 'same_year' | 'transition'; from: string; to: string; planStart: string; reference: string; conversionReference: string; mznMapsReference: string; months: Array; }; export const newNormalPeriodFx = (): NormalPeriodFxInput => ({ version: 1, kind: '', from: '', to: '', planStart: '', reference: '', conversionReference: '', mznMapsReference: '', months: [], }); export function validateNormalPeriodFx(i: NormalPeriodFxInput) { if ( !i || i.version !== 1 || ![ '', 'annual', 'start', 'cessation', 'same_year', 'transition', ].includes(i.kind) || [ 'from', 'to', 'planStart', 'reference', 'conversionReference', 'mznMapsReference', ].some((k) => typeof i[k as 'from'] !== 'string') || !Array.isArray(i.months) || new Set(i.months.map((m) => m.period)).size !== i.months.length || i.months.some( (m) => !Number.isSafeInteger(m.period) || m.period < 1 || typeof m.currency !== 'string' || (m.fxSelection !== undefined && (!m.fxSelection || typeof m.fxSelection !== 'object')) || (m.fxLink !== undefined && (!m.fxLink || typeof m.fxLink.id !== 'string' || typeof m.fxLink.fingerprint !== 'string')), ) ) throw Error( 'Correspondência do período/moeda não recuperável;originais preservados.', ); } const iso = (s: string) => /^20\d{2}-\d{2}-\d{2}$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; const month = (s: string) => Number(s.slice(0, 4)) * 12 + Number(s.slice(5, 7)) - 1; const monthEnd = (s: string) => { const d = new Date(s + 'T00:00:00Z'); d.setUTCMonth(d.getUTCMonth() + 1, 0); return d.toISOString().slice(0, 10); }; const amount = (v: FiscalFraction) => { const n = BigInt(v.numerator) * 100n, d = BigInt(v.denominator); if (n % d !== 0n) throw Error( 'Conversão com fracção de cêntimo: representação fiscal por corresponder,sem arredondamento novo.', ); const c = n / d, a = c < 0n ? -c : c; return ( (c < 0n ? '-' : '') + a / 100n + '.' + String(a % 100n).padStart(2, '0') ); }; type Source = NonNullable>; export function mapNormalPeriodFx( d: Record, input: NormalPeriodFxInput, raw: Source | null, ) { validateNormalPeriodFx(input); const pending: string[] = [], short = input.kind !== '' && input.kind !== 'annual'; let count = 0; if (!input.kind || !input.reference.trim()) pending.push( 'Tipo,evento e suporte da correspondência do período por identificar.', ); if ( !iso(input.from) || !iso(input.to) || input.from > input.to || !input.from.endsWith('-01') || monthEnd(input.to.slice(0, 7) + '-01') !== input.to || !input.to.startsWith('2026-') ) pending.push( 'Recorte:meses completos,termo em2026;datas parciais ou inválidas permanecem por corresponder.', ); else { count = month(input.to) - month(input.from) + 1; if ( count < 1 || count > 12 || (short && count >= 12) || (!short && count !== 12) ) pending.push('Número de meses incompatível com o tipo de período.'); } if ( short && (!input.from.startsWith('2026-') || (input.kind === 'start' && input.to !== '2026-12-31') || (['cessation', 'transition'].includes(input.kind) && input.from !== '2026-01-01')) ) pending.push( 'Datas incompatíveis com início,cessação ou transição no ano civil2026.', ); if (!iso(input.planStart) || !input.planStart.endsWith('-01')) pending.push( 'Identifique o primeiro mês civil do plano;meses orçamentais não têm data fiscal presumida.', ); if (!input.mznMapsReference.trim()) pending.push( 'Confirme que parcelas,activos,quotas,saldos e mapas fiscais existentes estão reconciliados emMZN com este período;sem converter novamente esses dados.', ); if (!raw) pending.push('Plano financeiro reconciliado em falta.'); const selected = raw?.accountingLines[0]?.monthlyAmounts.filter( (m) => iso(input.planStart) && month(input.planStart) + m.period - 1 >= month(input.from) && month(input.planStart) + m.period - 1 <= month(input.to), ) ?? []; if (selected.length !== count || !count) pending.push( 'Meses do plano não cobrem integralmente o período indicado;sem anualização ou preenchimento de meses em falta.', ); if (raw?.currency !== 'MZN' && !input.conversionReference.trim()) pending.push( 'Identifique pressuposto documentado da conversão mensal para a simulação;selecção cambial não confirma,por si,a regra fiscal de conversão anual.', ); for (const m of input.months) if (!selected.some((x) => x.period === m.period)) pending.push( 'Selecção cambial sem mês seleccionado no período: ' + m.period, ); const rows: Array<{ code: string; period: number; date: string; originalAmount: string; originalCurrency: string; value: FiscalFraction | null; evidence: IrpcFxEvidence | null; reason: string; }> = []; const lines = raw?.accountingLines.map((l) => { const monthlyAmounts = l.monthlyAmounts .filter((m) => selected.some((x) => x.period === m.period)) .map((m) => { let value: FiscalFraction | null = null, evidence: IrpcFxEvidence | null = null, reason = ''; const day = iso(input.planStart) ? new Date( Date.UTC( Number(input.planStart.slice(0, 4)), Number(input.planStart.slice(5, 7)) - 1 + m.period - 1, 1, ), ) .toISOString() .slice(0, 10) : ''; try { const sign = m.amount.startsWith('-') ? -1n : 1n, original = sign < 0n ? m.amount.slice(1) : m.amount; const selection = input.months.find( (x) => x.period === m.period, ); if ( raw.currency !== 'MZN' && (!selection || selection.currency !== raw.currency) ) throw Error( 'Selecção cambial deste mês/moeda em falta;sem usar taxa de outro mês por defeito.', ); const converted = convertIrpcAmount( d, original, selection ?? { currency: 'MZN' }, ); value = fraction( BigInt(converted.value.numerator) * sign, BigInt(converted.value.denominator), ); evidence = converted.evidence; amount(value); } catch (e) { value = null; reason = String(e); pending.push( l.code + '/mês' + m.period + ': ' + reason, ); } rows.push({ code: l.code, period: m.period, date: day, originalAmount: m.amount, originalCurrency: raw.currency, value, evidence, reason, }); return { period: m.period, amount: value ? amount(value) : '0.00', }; }); const total = addFractions( rows .filter((r) => r.code === l.code && r.value) .map((r) => r.value!), ); return { ...l, monthlyAmounts, total: amount(total) }; }) ?? []; const ready = pending.length === 0; const profit = addFractions( lines.map((l) => { const f = rows .filter((r) => r.code === l.code && r.value) .map((r) => r.value!); const sum = addFractions(f); return l.accountingEffect === 'income' ? sum : fraction(-BigInt(sum.numerator), BigInt(sum.denominator)); }), ); return { input: structuredClone(input), short, months: count, originalCurrency: raw?.currency ?? '', rows, pending, source: ready && raw ? { ...raw, currency: 'MZN', cashSchedule: raw.currency === 'MZN' ? raw.cashSchedule.filter((r) => selected.some((m) => m.period === r.period), ) : [], periods: count, accountingLines: lines, accountingProfitBeforeTax: amount(profit), } : null, rule: 'CIRPC7(4)a–d;Decreto72/2013 art9(3)a–c para os casos correspondidos. Meses completos do plano,sem anualização. Conversão para simulação reutiliza mecanismo cambial entregue e pressupostos explícitos;sem confirmação fiscal anual presumida.', }; } export type NormalPeriodFxModel = ReturnType; export function normalPeriodFxDependency( d: Record, i: NormalPeriodFxInput, ) { return i.months .map((m) => irpcFxDependency(d, m)) .filter(Boolean) .join(' | '); } export function normalPeriodFxReport( m: NormalPeriodFxModel, ): Array<[string, string, string, string]> { return [ [ 'IRPC normal — L6', 'Período e moeda do plano', `${m.input.from}–${m.input.to};${m.months}meses;origem:${m.originalCurrency};resultado emMZN:${m.source?.accountingProfitBeforeTax ?? 'Indeterminado'};${m.originalCurrency !== 'MZN' ? 'Conversão para simulação com pressupostos documentados;regra fiscal anual não confirmada automaticamente.' : ''}${m.pending.join(' | ')}`, `${m.input.reference};${m.input.conversionReference};${m.input.mznMapsReference};${m.rule}`, ], ...m.rows.map( (r) => [ 'IRPC normal — L6', `${({ revenue: 'Receitas', cost_of_sales: 'Custo das vendas', operating_expenses: 'Despesas operacionais', personnel: 'Pessoal', depreciation: 'Depreciação contabilística', interest_expense: 'Juros reconhecidos' } as Record)[r.code] ?? r.code}/mês${r.period} (${r.date})`, `Original:${r.originalAmount} ${r.originalCurrency};MZN:${r.value ? amount(r.value) : 'Indeterminado'};${r.reason}`, r.evidence ? irpcFxEvidenceLabel(r.evidence) : 'OrigemMZN;sem conversão', ] as [string, string, string, string], ), ]; }