148 lines
4.5 KiB
TypeScript
148 lines
4.5 KiB
TypeScript
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,
|
||
};
|
||
}
|