152 lines
5.2 KiB
TypeScript
152 lines
5.2 KiB
TypeScript
import {
|
|
addFractions,
|
|
subtractFractions,
|
|
positiveFraction,
|
|
decimalFraction,
|
|
type FiscalFraction,
|
|
} from './fiscal-rational.ts';
|
|
export type FiscalClaimEvents = {
|
|
stage: 'preparation' | 'submitted';
|
|
requestReference: string;
|
|
requestDate: string;
|
|
decision: 'pending' | 'approved' | 'partial' | 'rejected';
|
|
decisionReference: string;
|
|
decisionDate: string;
|
|
decisionIssuer: string;
|
|
approvedAmount: string;
|
|
receipts: Array<{
|
|
id: string;
|
|
reference: string;
|
|
date: string;
|
|
amount: string;
|
|
}>;
|
|
};
|
|
export type FiscalClaimState = {
|
|
status: 'determined' | 'indeterminate';
|
|
reason: string;
|
|
approved: FiscalFraction | null;
|
|
received: FiscalFraction | null;
|
|
awaitingReceipt: FiscalFraction | null;
|
|
notGranted: FiscalFraction | null;
|
|
};
|
|
export function newFiscalClaimEvents(): FiscalClaimEvents {
|
|
return {
|
|
stage: 'preparation',
|
|
requestReference: '',
|
|
requestDate: '',
|
|
decision: 'pending',
|
|
decisionReference: '',
|
|
decisionDate: '',
|
|
decisionIssuer: '',
|
|
approvedAmount: '',
|
|
receipts: [],
|
|
};
|
|
}
|
|
export const fiscalClaimDate = (v: string) =>
|
|
/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(v) &&
|
|
!Number.isNaN(Date.parse(v)) &&
|
|
new Date(v).toISOString().slice(0, 10) === v;
|
|
export function assessFiscalClaim(
|
|
row: FiscalClaimEvents,
|
|
requested: FiscalFraction,
|
|
duplicate: (p: FiscalClaimEvents['receipts'][number]) => boolean,
|
|
): FiscalClaimState {
|
|
const r: FiscalClaimState = {
|
|
status: 'indeterminate',
|
|
reason: '',
|
|
approved: null,
|
|
received: null,
|
|
awaitingReceipt: null,
|
|
notGranted: null,
|
|
},
|
|
stop = (reason: string) => ({ ...r, reason });
|
|
if (!row.requestReference.trim() || !fiscalClaimDate(row.requestDate))
|
|
return stop('Identifique referência/data do pedido ou preparação.');
|
|
if (
|
|
row.stage === 'preparation' &&
|
|
(row.decision !== 'pending' || row.receipts.length)
|
|
)
|
|
return stop(
|
|
'Preparação não é pedido apresentado;registe apresentação externa antes de decisão/recebimento.',
|
|
);
|
|
if (row.decision === 'pending') {
|
|
if (row.receipts.length)
|
|
return stop(
|
|
'Recebimento sem decisão identificada;conservar registo sem o presumir reconhecido.',
|
|
);
|
|
return {
|
|
...r,
|
|
status: 'determined',
|
|
reason: 'Pedido conservado;decisão e recebimento ainda não identificados.',
|
|
};
|
|
}
|
|
if (
|
|
!row.decisionReference.trim() ||
|
|
!row.decisionIssuer.trim() ||
|
|
!fiscalClaimDate(row.decisionDate) ||
|
|
row.decisionDate < row.requestDate
|
|
)
|
|
return stop(
|
|
'Identifique decisão recebida,número/data e entidade emitente;pedido não equivale a deferimento.',
|
|
);
|
|
let approved: FiscalFraction;
|
|
try {
|
|
approved = decimalFraction(row.approvedAmount);
|
|
} catch {
|
|
return stop(
|
|
'Montante reconhecido por identificar;indeferimento exigezero expresso,sem transformar desconhecido emzero.',
|
|
);
|
|
}
|
|
if (
|
|
BigInt(subtractFractions(approved, requested).numerator) > 0n ||
|
|
(row.decision === 'approved' &&
|
|
JSON.stringify(approved) !== JSON.stringify(requested)) ||
|
|
(row.decision === 'rejected' && BigInt(approved.numerator) !== 0n) ||
|
|
(row.decision === 'partial' &&
|
|
(BigInt(approved.numerator) <= 0n ||
|
|
BigInt(subtractFractions(requested, approved).numerator) <= 0n))
|
|
)
|
|
return stop(
|
|
'Montante reconhecido incompatível com decisão/pedido;conservar o âmbito exacto,sem generalizar o deferimento.',
|
|
);
|
|
r.approved = approved;
|
|
r.notGranted = subtractFractions(requested, approved);
|
|
const receipts: FiscalFraction[] = [];
|
|
for (const p of row.receipts) {
|
|
if (
|
|
!p.reference.trim() ||
|
|
!fiscalClaimDate(p.date) ||
|
|
p.date < row.decisionDate ||
|
|
duplicate(p)
|
|
)
|
|
return stop(
|
|
'Recebimento sem referência/data posterior à decisão ou já registado noutro pedido;sem duplicar caixa.',
|
|
);
|
|
try {
|
|
const value = decimalFraction(p.amount);
|
|
if (BigInt(value.numerator) <= 0n)
|
|
return stop(
|
|
'Recebimento exige montante positivo identificado.',
|
|
);
|
|
receipts.push(value);
|
|
} catch {
|
|
return stop(
|
|
'Montante recebido por identificar;decisão não cria recebimento.',
|
|
);
|
|
}
|
|
}
|
|
r.received = addFractions(receipts);
|
|
if (BigInt(subtractFractions(r.received, approved).numerator) > 0n)
|
|
return stop(
|
|
'Recebimentos excedem o montante reconhecido;reconcilie sem criar crédito ou caixa adicional.',
|
|
);
|
|
r.awaitingReceipt = positiveFraction(
|
|
subtractFractions(approved, r.received),
|
|
);
|
|
return {
|
|
...r,
|
|
status: 'determined',
|
|
reason: 'Pedido,decisão e recebimento separados;sem execução bancária ou sincronização silenciosa de caixa.',
|
|
};
|
|
}
|