202 lines
7.5 KiB
TypeScript
202 lines
7.5 KiB
TypeScript
import { inputText } from './input-text.ts';
|
|
import type { AcquisitionBook } from './iva-acquisitions.ts';
|
|
import { beverageParcel } from './iva-beverage-parcels.ts';
|
|
import {
|
|
addFractions,
|
|
decimalFraction,
|
|
fraction,
|
|
fractionLabel,
|
|
subtractFractions,
|
|
type FiscalFraction,
|
|
} from './fiscal-rational.ts';
|
|
|
|
/** Technical representation of disjoint VAT portions, not an additional fiscal condition. */
|
|
export type BeverageAllocation = {
|
|
from: string;
|
|
to: string;
|
|
reference: string;
|
|
deductionFrom: string;
|
|
deductionTo: string;
|
|
deductionReference: string;
|
|
};
|
|
type Source = { parcel: string; origin: string; vat: FiscalFraction };
|
|
export type BeverageRange = {
|
|
from: FiscalFraction;
|
|
to: FiscalFraction;
|
|
amount: FiscalFraction;
|
|
};
|
|
function boundary(raw: string): FiscalFraction {
|
|
const parts = /^(\d{1,40})\/(\d{1,40})$/.exec(raw);
|
|
if (!parts) return decimalFraction(raw);
|
|
if (BigInt(parts[2]) === 0n)
|
|
throw Error('Denominador nulo nos limites da parcela.');
|
|
return fraction(BigInt(parts[1]), BigInt(parts[2]));
|
|
}
|
|
export function beverageRange(
|
|
source: Source,
|
|
allocation?: BeverageAllocation,
|
|
): BeverageRange {
|
|
const from = allocation ? boundary(allocation.from) : fraction(0n),
|
|
to = allocation ? boundary(allocation.to) : source.vat,
|
|
amount = subtractFractions(to, from);
|
|
if (
|
|
BigInt(amount.numerator) <= 0n ||
|
|
BigInt(subtractFractions(source.vat, to).numerator) < 0n
|
|
)
|
|
throw Error(
|
|
'Limites da parcela fora do IVA documental; indique início inferior ao fim e fim até ao total da linha.',
|
|
);
|
|
if (allocation && !allocation.reference.trim())
|
|
throw Error('Identifique o suporte da repartição da linha.');
|
|
return { from, to, amount };
|
|
}
|
|
export function beverageRangesOverlap(
|
|
a: BeverageRange,
|
|
b: BeverageRange,
|
|
): boolean {
|
|
return (
|
|
BigInt(subtractFractions(a.to, b.from).numerator) > 0n &&
|
|
BigInt(subtractFractions(b.to, a.from).numerator) > 0n
|
|
);
|
|
}
|
|
export function beverageDeductionEvidence(
|
|
d: Record<string, unknown>,
|
|
source: Source,
|
|
): {
|
|
amount: FiscalFraction | null;
|
|
reason: string;
|
|
records: Array<{
|
|
context: string;
|
|
revision: number;
|
|
calculatedAt: string;
|
|
amount: FiscalFraction;
|
|
documents: string[];
|
|
}>;
|
|
} {
|
|
const store = JSON.parse(inputText(d.modelo_a_snapshot || '{}')),
|
|
books = (store.books ?? {}) as Record<string, AcquisitionBook>,
|
|
records: Array<{
|
|
context: string;
|
|
revision: number;
|
|
calculatedAt: string;
|
|
amount: FiscalFraction;
|
|
documents: string[];
|
|
}> = [];
|
|
for (const [context, book] of Object.entries(books))
|
|
for (const calc of [
|
|
...(book.history ?? []),
|
|
...(book.result ? [book.result] : []),
|
|
]) {
|
|
const inputs = calc.inputs.filter(
|
|
(row) =>
|
|
row.origin === 'effective' &&
|
|
source.origin === 'effective' &&
|
|
beverageParcel(row) === source.parcel,
|
|
);
|
|
const amounts: FiscalFraction[] = [];
|
|
for (const input of inputs) {
|
|
const result = calc.rows.find((r) => r.id === input.id);
|
|
if (!result || result.deductible === null) {
|
|
if (input.bc?.beveragePath === 'deduction')
|
|
return {
|
|
amount: null,
|
|
reason: 'Dedução efectiva anterior indeterminada; identificar montante exacto e documento antes da repartição.',
|
|
records,
|
|
};
|
|
continue;
|
|
}
|
|
const amount =
|
|
result.deductibleExact ??
|
|
decimalFraction(result.deductible);
|
|
if (BigInt(amount.numerator) > 0n) amounts.push(amount);
|
|
}
|
|
if (amounts.length)
|
|
records.push({
|
|
context,
|
|
revision: calc.revision,
|
|
calculatedAt: calc.calculatedAt,
|
|
amount: addFractions(amounts),
|
|
documents: inputs.map(
|
|
(row) =>
|
|
`${row.documentRef};linha${row.documentLine};${row.period}`,
|
|
),
|
|
});
|
|
}
|
|
const unique = [
|
|
...new Map(
|
|
records.map((r) => [fractionLabel(r.amount), r.amount]),
|
|
).values(),
|
|
];
|
|
if (unique.length > 1)
|
|
return {
|
|
amount: null,
|
|
reason: 'Montantes de dedução efectiva divergentes nas versões recuperadas; reconciliar a sequência e o fundamento das regularizações. Uma correcção preparada não liberta IVA para E.',
|
|
records,
|
|
};
|
|
return {
|
|
amount: unique[0] ?? fraction(0n),
|
|
reason: records.length
|
|
? 'Dedução efectiva declarada preservada; a sua afectação à parcela exige suporte expresso.'
|
|
: 'Sem dedução efectiva positiva identificada nos livros recuperados; não é consulta à conta corrente AT.',
|
|
records,
|
|
};
|
|
}
|
|
export function reconcileBeverageAllocation(
|
|
d: Record<string, unknown>,
|
|
source: Source,
|
|
allocation?: BeverageAllocation,
|
|
): {
|
|
range: BeverageRange | null;
|
|
prior: FiscalFraction | null;
|
|
reason: string;
|
|
} {
|
|
const evidence = beverageDeductionEvidence(d, source);
|
|
if (!evidence.amount)
|
|
return { range: null, prior: null, reason: evidence.reason };
|
|
try {
|
|
const range = beverageRange(source, allocation);
|
|
if (BigInt(evidence.amount.numerator) > 0n) {
|
|
if (!allocation || !allocation.deductionReference.trim())
|
|
return {
|
|
range: null,
|
|
prior: evidence.amount,
|
|
reason: 'Dedução efectiva anterior identificada; indique limites e suporte da parcela já deduzida, sem a apagar ou repetir em restituição.',
|
|
};
|
|
const deduction = beverageRange(source, {
|
|
...allocation,
|
|
from: allocation.deductionFrom,
|
|
to: allocation.deductionTo,
|
|
});
|
|
if (
|
|
BigInt(
|
|
subtractFractions(deduction.amount, evidence.amount)
|
|
.numerator,
|
|
) !== 0n ||
|
|
beverageRangesOverlap(range, deduction)
|
|
)
|
|
return {
|
|
range: null,
|
|
prior: evidence.amount,
|
|
reason: 'Parcela da dedução não coincide com o montante efectivo anterior ou sobrepõe-se à restituição; benefício duplicado impedido. Se toda a linha foi deduzida, é necessária regularização com fundamento próprio, ainda não automatizada nesta via.',
|
|
};
|
|
} else if (
|
|
allocation &&
|
|
(allocation.deductionFrom ||
|
|
allocation.deductionTo ||
|
|
allocation.deductionReference)
|
|
)
|
|
return {
|
|
range: null,
|
|
prior: evidence.amount,
|
|
reason: 'Foi indicada dedução anterior sem correspondência nos livros efectivos recuperados; resolver a divergência antes de calcular.',
|
|
};
|
|
return { range, prior: evidence.amount, reason: evidence.reason };
|
|
} catch (error) {
|
|
return {
|
|
range: null,
|
|
prior: evidence.amount,
|
|
reason: String(error instanceof Error ? error.message : error),
|
|
};
|
|
}
|
|
}
|