Files
Investidor-V2/resources/js/lib/irpc-pec.ts
T

148 lines
4.5 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 {
subtractFractions,
decimalFraction,
type FiscalFraction,
} from './fiscal-rational.ts';
import {
validateProofParcel,
requireProofParcel,
type FiscalProofParcel,
} from './fiscal-proof-parcel.ts';
export const pecVersion = 'IRPC-PEC/1';
export const pecSource =
'CIRPC, art. 66(1–2), Lei 34/2007, exemplar BR52, p. 768-(72): dedução após créditos anteriores, até à colecta, sem reembolso do PEC. Recorte demonstrado: pagamento efectivo do próprio exercício 2026, residente geral, ano civil, MZN. Não calcula a obrigação PEC, transporte, caducidade ou consumo de saldos oficiais.';
export type PecPayment = FiscalProofParcel & {
id: string;
document: string;
issuer: string;
date: string;
subjectNuit: string;
year: string;
currency: string;
proofAmount: string;
amount: string;
effective: '' | 'yes' | 'no';
reference: string;
previouslyUsed: string;
priorUseReference: string;
};
export type PecPayments = {
version: 1;
state: '' | 'known' | 'absent';
reference: string;
rows: PecPayment[];
};
export const newPecPayment = (id: string): PecPayment => ({
id,
document: '',
issuer: '',
date: '',
subjectNuit: '',
year: '',
currency: 'MZN',
proofAmount: '',
amount: '',
effective: '',
reference: '',
previouslyUsed: '',
priorUseReference: '',
});
export function validatePec(p: PecPayments) {
const keys = Object.keys(newPecPayment(''));
if (
!p ||
p.version !== 1 ||
!['', 'known', 'absent'].includes(p.state) ||
typeof p.reference !== 'string' ||
!Array.isArray(p.rows) ||
new Set(p.rows.map((r) => r?.id)).size !== p.rows.length ||
p.rows.some(
(r) =>
!r ||
keys.some(
(k) =>
typeof (r as unknown as Record<string, unknown>)[k] !==
'string',
) ||
!['', 'yes', 'no'].includes(r.effective),
)
) {
throw Error(
'Comprovativos PEC não recuperáveis; originais conservados.',
);
}
for (const r of p.rows) validateProofParcel(r);
}
export const pecCompare = (a: FiscalFraction, b: FiscalFraction) =>
BigInt(a.numerator) * BigInt(b.denominator) -
BigInt(b.numerator) * BigInt(a.denominator);
const money = (s: string) => {
if (!/^\d{1,18}(\.\d{1,2})?$/.test(s))
throw Error(
'Montante PEC em falta/inválido; declare zero explicitamente quando conhecido.',
);
return decimalFraction(s);
};
export function pecPaymentValue(
r: PecPayment,
subjectNuit: string,
year: string,
) {
requireProofParcel(r);
if (
!r.id.trim() ||
!r.document.trim() ||
!r.issuer.trim() ||
!r.reference.trim() ||
!r.priorUseReference.trim() ||
r.effective !== 'yes' ||
r.currency !== 'MZN' ||
!/^20\d{2}-\d{2}-\d{2}$/.test(r.date) ||
!Number.isFinite(Date.parse(r.date)) ||
new Date(r.date).toISOString().slice(0, 10) !== r.date ||
!r.date.startsWith(year + '-') ||
r.subjectNuit !== subjectNuit ||
r.year !== year ||
year !== '2026'
) {
throw Error(
'PEC: exige prova efectiva, natureza PEC, titular, exercício 2026, data válida e MZN; saldos anteriores e previsões ficam separados.',
);
}
const proofAmount = money(r.proofAmount),
value = money(r.amount),
previouslyUsed = money(r.previouslyUsed);
if (
pecCompare(previouslyUsed, proofAmount) > 0n ||
pecCompare(value, subtractFractions(proofAmount, previouslyUsed)) > 0n
)
throw Error(
'PEC: parcela e utilizações anteriores excedem o comprovativo disponível.',
);
return { proofAmount, value, previouslyUsed };
}
export function pecDeduction(
available: FiscalFraction,
colecta: FiscalFraction | null,
ready: boolean,
selected?: FiscalFraction | null,
) {
const deducted =
ready && colecta
? (selected ??
(pecCompare(available, colecta) > 0n ? colecta : available))
: null;
return {
version: pecVersion,
available,
deducted,
remaining: deducted ? subtractFractions(available, deducted) : null,
afterPec:
deducted && colecta ? subtractFractions(colecta, deducted) : null,
source: pecSource,
officialBalancesConsumed: false,
carryCreated: false,
};
}