Files
Investidor-V2/resources/js/lib/irpc-normal-period-fx.ts
T

330 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<IrpcFxAmount & { period: number }>;
};
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<ReturnType<typeof rawNormalFinancialSource>>;
export function mapNormalPeriodFx(
d: Record<string, unknown>,
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<typeof mapNormalPeriodFx>;
export function normalPeriodFxDependency(
d: Record<string, unknown>,
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<string, string>)[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],
),
];
}