483 lines
17 KiB
TypeScript
483 lines
17 KiB
TypeScript
import { inputText } from './input-text.ts';
|
||
import { acquisitionBook, acquisitionCurrent } from './iva-acquisitions.ts';
|
||
import { beverageParcel } from './iva-beverage-parcels.ts';
|
||
import { closureFingerprint } from './iva-closure.ts';
|
||
import {
|
||
assessFiscalClaim,
|
||
newFiscalClaimEvents,
|
||
fiscalClaimDate,
|
||
type FiscalClaimEvents,
|
||
type FiscalClaimState,
|
||
} from './fiscal-claim-events.ts';
|
||
import {
|
||
decimalFraction,
|
||
fractionPresentation,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
import {
|
||
beverageRange,
|
||
beverageRangesOverlap,
|
||
beverageDeductionEvidence,
|
||
reconcileBeverageAllocation,
|
||
type BeverageAllocation,
|
||
} from './iva-beverage-allocation.ts';
|
||
export const beverageRestitutionSource =
|
||
'L3-E:CIVA13(1)(c/d/h),13(2–3),Lei13/2016 PDF12/p1426(270). Bebidas:dedução ou restituição,sem aplicar20(1)(d);não isenção directa. Provisões de pesca costeira excluídas. Linha integral ou parcelas documentadas sem sobreposição;dedução efectiva anterior reconciliada por montante e suporte,sem apagar históricos. Procedimento/formulário específico não automatizado;sem importar limites/prazos do reembolso geral21.';
|
||
export type BeverageRestitutionSource = {
|
||
parcel: string;
|
||
acquisitionId: string;
|
||
scope: string;
|
||
period: string;
|
||
origin: 'projected' | 'effective';
|
||
documentRef: string;
|
||
documentDate: string;
|
||
documentLine: string;
|
||
supplier: string;
|
||
vat: FiscalFraction;
|
||
fingerprint: string;
|
||
operationId: string;
|
||
operationRef: string;
|
||
calculatedAt: string;
|
||
};
|
||
export type BeverageRestitutionInput = FiscalClaimEvents & {
|
||
id: string;
|
||
source: BeverageRestitutionSource | null;
|
||
entityRef: string;
|
||
exclusive: '' | 'yes' | 'no';
|
||
allocation?: BeverageAllocation;
|
||
};
|
||
export type BeverageRestitutionResult = FiscalClaimState & {
|
||
id: string;
|
||
inputKey: string;
|
||
requested: FiscalFraction | null;
|
||
source: string;
|
||
};
|
||
type Calc = {
|
||
calculatedAt: string;
|
||
key: string;
|
||
inputs: BeverageRestitutionInput[];
|
||
rows: BeverageRestitutionResult[];
|
||
};
|
||
export type BeverageRestitutionBook = {
|
||
version: 1;
|
||
rows: BeverageRestitutionInput[];
|
||
result?: Calc;
|
||
history?: Calc[];
|
||
};
|
||
const scope = (d: Record<string, unknown>) =>
|
||
inputText(d.context_opportunity || 'general'),
|
||
norm = (s: string) => s.trim().toLocaleLowerCase();
|
||
function store(d: Record<string, unknown>): Record<string, unknown> {
|
||
const s = JSON.parse(
|
||
inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'),
|
||
);
|
||
if (s.version !== 1 || !s.books)
|
||
throw Error('Livros não recuperáveis;originais preservados.');
|
||
return s;
|
||
}
|
||
function maps(
|
||
d: Record<string, unknown>,
|
||
): Record<string, BeverageRestitutionBook> {
|
||
const m = store(d).ivaBeverageRestitutions;
|
||
if (m === undefined) return {};
|
||
if (!m || typeof m !== 'object' || Array.isArray(m))
|
||
throw Error(
|
||
'Mapa de restituições não recuperável;originais preservados.',
|
||
);
|
||
return m as Record<string, BeverageRestitutionBook>;
|
||
}
|
||
function validate(b: BeverageRestitutionBook): void {
|
||
if (
|
||
!b ||
|
||
b.version !== 1 ||
|
||
!Array.isArray(b.rows) ||
|
||
(b.history !== undefined && !Array.isArray(b.history)) ||
|
||
(b.result !== undefined &&
|
||
(!Array.isArray(b.result.rows) || !Array.isArray(b.result.inputs)))
|
||
)
|
||
throw Error('LivroE não recuperável;originais preservados.');
|
||
for (const r of b.rows)
|
||
if (
|
||
!r ||
|
||
![
|
||
'id',
|
||
'entityRef',
|
||
'requestReference',
|
||
'requestDate',
|
||
'decisionReference',
|
||
'decisionDate',
|
||
'decisionIssuer',
|
||
'approvedAmount',
|
||
].every(
|
||
(k) =>
|
||
typeof r[k as keyof BeverageRestitutionInput] === 'string',
|
||
) ||
|
||
(r.allocation !== undefined &&
|
||
(!r.allocation ||
|
||
![
|
||
'from',
|
||
'to',
|
||
'reference',
|
||
'deductionFrom',
|
||
'deductionTo',
|
||
'deductionReference',
|
||
].every(
|
||
(k) =>
|
||
typeof r.allocation![
|
||
k as keyof BeverageAllocation
|
||
] === 'string',
|
||
))) ||
|
||
!['', 'yes', 'no'].includes(r.exclusive) ||
|
||
!['preparation', 'submitted'].includes(r.stage) ||
|
||
!['pending', 'approved', 'partial', 'rejected'].includes(
|
||
r.decision,
|
||
) ||
|
||
!Array.isArray(r.receipts) ||
|
||
r.receipts.some(
|
||
(p) =>
|
||
!p ||
|
||
!['id', 'reference', 'date', 'amount'].every(
|
||
(k) => typeof p[k as keyof typeof p] === 'string',
|
||
),
|
||
)
|
||
)
|
||
throw Error('EntradasE não recuperáveis;sem apagar documentos.');
|
||
}
|
||
export function beverageRestitutionBook(
|
||
d: Record<string, unknown>,
|
||
): BeverageRestitutionBook {
|
||
const b = maps(d)[scope(d)] ?? { version: 1, rows: [] };
|
||
validate(b);
|
||
return b;
|
||
}
|
||
export function saveBeverageRestitutionBook(
|
||
d: Record<string, unknown>,
|
||
b: BeverageRestitutionBook,
|
||
): string {
|
||
validate(b);
|
||
const s = store(d);
|
||
return JSON.stringify({
|
||
...s,
|
||
ivaBeverageRestitutions: { ...maps(d), [scope(d)]: b },
|
||
});
|
||
}
|
||
export function beverageRestitutionSources(
|
||
d: Record<string, unknown>,
|
||
): BeverageRestitutionSource[] {
|
||
const b = acquisitionBook(d);
|
||
if (!acquisitionCurrent(d, b)) return [];
|
||
return b.rows.flatMap((row) => {
|
||
const result = b.result!.rows.find((r) => r.id === row.id);
|
||
if (
|
||
row.bc?.expense !== 'beverages' ||
|
||
row.bc.beveragePath !== 'refund' ||
|
||
!result?.reason.startsWith('Via restituição13(2)') ||
|
||
result.deductible !== '0.00' ||
|
||
!result.vat ||
|
||
!row.deductionLink
|
||
)
|
||
return [];
|
||
const vat = decimalFraction(result.vat);
|
||
if (BigInt(vat.numerator) <= 0n) return [];
|
||
return [
|
||
{
|
||
parcel: beverageParcel(row),
|
||
acquisitionId: row.id,
|
||
scope: scope(d),
|
||
period: row.period,
|
||
origin: row.origin,
|
||
documentRef: row.documentRef,
|
||
documentDate: row.documentDate,
|
||
documentLine: row.documentLine,
|
||
supplier: row.supplier,
|
||
vat,
|
||
fingerprint: closureFingerprint([row, result]),
|
||
operationId: row.deductionLink.operationId,
|
||
operationRef: row.deductionLink.allocationRef,
|
||
calculatedAt: b.result!.calculatedAt,
|
||
},
|
||
];
|
||
});
|
||
}
|
||
export function newBeverageRestitution(
|
||
id: string,
|
||
source: BeverageRestitutionSource | null,
|
||
entityRef = '',
|
||
): BeverageRestitutionInput {
|
||
return {
|
||
...newFiscalClaimEvents(),
|
||
id,
|
||
source: source ? structuredClone(source) : null,
|
||
entityRef,
|
||
exclusive: '',
|
||
};
|
||
}
|
||
function sameSource(
|
||
d: Record<string, unknown>,
|
||
r: BeverageRestitutionInput,
|
||
): boolean {
|
||
return (
|
||
!!r.source &&
|
||
beverageRestitutionSources(d).some(
|
||
(s) =>
|
||
s.scope === r.source!.scope &&
|
||
s.parcel === r.source!.parcel &&
|
||
s.fingerprint === r.source!.fingerprint,
|
||
)
|
||
);
|
||
}
|
||
function allUses(
|
||
d: Record<string, unknown>,
|
||
b: BeverageRestitutionBook,
|
||
): BeverageRestitutionInput[] {
|
||
return [
|
||
...b.rows,
|
||
...Object.entries(maps(d))
|
||
.filter(([id]) => id !== scope(d))
|
||
.flatMap(([, v]) => {
|
||
validate(v);
|
||
return v.rows.filter((r) => r.source?.origin === 'effective');
|
||
}),
|
||
];
|
||
}
|
||
function past(
|
||
d: Record<string, unknown>,
|
||
b: BeverageRestitutionBook,
|
||
): BeverageRestitutionInput[] {
|
||
return Object.entries({ ...maps(d), [scope(d)]: b }).flatMap(
|
||
([context, v]) =>
|
||
[...(v.history ?? []), ...(v.result ? [v.result] : [])].flatMap(
|
||
(c) =>
|
||
c.inputs.filter(
|
||
(r) =>
|
||
(context === scope(d) ||
|
||
r.source?.origin === 'effective') &&
|
||
c.rows.some(
|
||
(v) =>
|
||
v.id === r.id &&
|
||
v.status === 'determined' &&
|
||
v.requested &&
|
||
BigInt(v.requested.numerator) > 0n,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
function committedParcel(r: BeverageRestitutionInput): string {
|
||
if (!r.source) return '';
|
||
try {
|
||
const range = beverageRange(r.source, r.allocation);
|
||
return closureFingerprint([
|
||
r.source.parcel,
|
||
r.source.origin,
|
||
r.source.vat,
|
||
range.from,
|
||
range.to,
|
||
]);
|
||
} catch {
|
||
return 'invalid';
|
||
}
|
||
}
|
||
const receiptKey = (
|
||
r: BeverageRestitutionInput,
|
||
p: FiscalClaimEvents['receipts'][number],
|
||
) => JSON.stringify([norm(r.entityRef), r.source?.origin, norm(p.reference)]);
|
||
function key(d: Record<string, unknown>, b: BeverageRestitutionBook): string {
|
||
return closureFingerprint([
|
||
b.rows,
|
||
b.rows.map((r) => [
|
||
sameSource(d, r),
|
||
r.source ? beverageDeductionEvidence(d, r.source) : null,
|
||
]),
|
||
allUses(d, b).map((r) => [
|
||
r.id,
|
||
r.source,
|
||
r.allocation,
|
||
r.receipts.map((p) => receiptKey(r, p)),
|
||
]),
|
||
past(d, b)
|
||
.filter(
|
||
(r) =>
|
||
r.stage === 'submitted' ||
|
||
r.decision !== 'pending' ||
|
||
r.receipts.length,
|
||
)
|
||
.map((r) => [
|
||
r.id,
|
||
r.source,
|
||
r.allocation,
|
||
r.receipts.map((p) => receiptKey(r, p)),
|
||
]),
|
||
]);
|
||
}
|
||
export function beverageRestitutionCurrent(
|
||
d: Record<string, unknown>,
|
||
b: BeverageRestitutionBook,
|
||
): boolean {
|
||
return (
|
||
!!b.result &&
|
||
b.result.key === key(d, b) &&
|
||
b.rows.length === b.result.rows.length &&
|
||
b.rows.every((r) =>
|
||
b.result!.rows.some(
|
||
(v) => v.id === r.id && v.inputKey === closureFingerprint(r),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
export function recordBeverageRestitution(
|
||
d: Record<string, unknown>,
|
||
b: BeverageRestitutionBook,
|
||
now = new Date().toISOString(),
|
||
): BeverageRestitutionBook {
|
||
validate(b);
|
||
if (new Set(b.rows.map((r) => r.id)).size !== b.rows.length)
|
||
throw Error('IdentidadeE repetida.');
|
||
const uses = allUses(d, b),
|
||
history = past(d, b);
|
||
const rows = b.rows.map((row) => {
|
||
const r: BeverageRestitutionResult = {
|
||
id: row.id,
|
||
inputKey: closureFingerprint(row),
|
||
status: 'indeterminate',
|
||
reason: '',
|
||
requested: null,
|
||
approved: null,
|
||
received: null,
|
||
awaitingReceipt: null,
|
||
notGranted: null,
|
||
source: beverageRestitutionSource,
|
||
},
|
||
stop = (reason: string) => ({ ...r, reason }),
|
||
s = row.source;
|
||
if (
|
||
!s ||
|
||
!sameSource(d, row) ||
|
||
d.currency !== 'MZN' ||
|
||
s.scope !== scope(d)
|
||
)
|
||
return stop(
|
||
'OrigemL1/L2 alterada ou ausente;sem restituição presumida.',
|
||
);
|
||
if (!row.entityRef.trim() || row.exclusive !== 'yes')
|
||
return stop(
|
||
'Identifique entidade e confirme a parcela exclusivamente para restituição;sem dupla utilização.',
|
||
);
|
||
const reconciled = reconcileBeverageAllocation(d, s, row.allocation);
|
||
if (!reconciled.range) return stop(reconciled.reason);
|
||
const related = (v: BeverageRestitutionInput) =>
|
||
v.source?.origin === s.origin && v.source?.parcel === s.parcel;
|
||
const overlaps = (v: BeverageRestitutionInput) => {
|
||
try {
|
||
return beverageRangesOverlap(
|
||
reconciled.range!,
|
||
beverageRange(v.source!, v.allocation),
|
||
);
|
||
} catch {
|
||
return true;
|
||
}
|
||
};
|
||
if (
|
||
uses.some((v) => v !== row && related(v) && overlaps(v)) ||
|
||
history.some(
|
||
(v) =>
|
||
related(v) &&
|
||
(v.stage === 'submitted' ||
|
||
v.decision !== 'pending' ||
|
||
v.receipts.length) &&
|
||
(v.id !== row.id
|
||
? overlaps(v)
|
||
: committedParcel(v) !== committedParcel(row)),
|
||
)
|
||
)
|
||
return stop(
|
||
'Parcela documental já ligada a pedido ou histórico comprometido;limites sobrepostos ou alterados. Sem duplicar nem libertar IVA por apagar a preparação.',
|
||
);
|
||
r.requested = reconciled.range.amount;
|
||
if (
|
||
!fiscalClaimDate(row.requestDate) ||
|
||
row.requestDate < s.documentDate
|
||
)
|
||
return stop(
|
||
'Data do pedido/preparação anterior ao documento ou por identificar.',
|
||
);
|
||
const events = assessFiscalClaim(
|
||
row,
|
||
reconciled.range.amount,
|
||
(p) =>
|
||
uses.flatMap((v) =>
|
||
v.receipts.filter(
|
||
(q) => receiptKey(v, q) === receiptKey(row, p),
|
||
),
|
||
).length > 1 ||
|
||
history.some(
|
||
(v) =>
|
||
v.id !== row.id &&
|
||
v.receipts.some(
|
||
(q) => receiptKey(v, q) === receiptKey(row, p),
|
||
),
|
||
),
|
||
);
|
||
Object.assign(r, events);
|
||
if (r.status === 'determined')
|
||
r.reason +=
|
||
' Linha13(2) fora da deduçãoL2 e do crédito geralD;pedido não é isenção directa nem concessão automática. Decisão e recebimento registados fora doInvesteMoz;procedimento/formulário específico e prazos não automatizados,sem os importar deD.';
|
||
return r;
|
||
});
|
||
const result = {
|
||
calculatedAt: now,
|
||
key: '',
|
||
inputs: structuredClone(b.rows),
|
||
rows,
|
||
};
|
||
const next = {
|
||
...b,
|
||
...(b.result
|
||
? { history: [...(b.history ?? []), structuredClone(b.result)] }
|
||
: {}),
|
||
result,
|
||
};
|
||
result.key = key(d, next);
|
||
return next;
|
||
}
|
||
export function beverageRestitutionReportRows(
|
||
d: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const b = beverageRestitutionBook(d);
|
||
if (!b.rows.length) return [];
|
||
const out: Array<[string, string, string, string]> = [
|
||
[
|
||
'IVA mensal',
|
||
'L3-E — restituição de bebidas',
|
||
beverageRestitutionCurrent(d, b)
|
||
? 'Resultado correspondente às entradas'
|
||
: 'Histórico preservado;rever origem/parcela',
|
||
beverageRestitutionSource,
|
||
],
|
||
],
|
||
fmt = (v: FiscalFraction | null) =>
|
||
v ? fractionPresentation(v).decimal : 'Indeterminado';
|
||
for (const r of b.result?.rows ?? []) {
|
||
const i = b.result!.inputs.find((v) => v.id === r.id),
|
||
s = i?.source;
|
||
out.push([
|
||
'IVA mensal',
|
||
`Restituição de bebidas — ${s?.period ?? ''}`,
|
||
`IVA documental:${fmt(s?.vat ?? null)};parcelaIVA:${fmt(r.requested)};dedução desta parcela:${r.requested ? '0pela via exclusivaE' : 'Indeterminada;ver origem'};reconhecido:${fmt(r.approved)};recebido:${fmt(r.received)};aguarda recebimento:${fmt(r.awaitingReceipt)}`,
|
||
`${i?.entityRef ?? ''};${scope(d)};${s?.origin === 'effective' ? 'Dados efectivos declarados' : 'Pressuposto de simulação'};${s?.documentRef ?? ''};${s?.documentDate ?? ''};linha${s?.documentLine ?? ''};operaçãoL1${s?.operationId ?? ''};${s?.operationRef ?? ''};pedido${i?.requestReference ?? ''},${i?.requestDate ?? ''};decisão${i?.decisionReference ?? ''},${i?.decisionDate ?? ''},${i?.decisionIssuer ?? ''};recibos:${i?.receipts.map((p) => `${p.reference};${p.date};${p.amount}MZN`).join(' | ') || 'nenhum registado'};${i?.allocation ? `repartiçãoIVA[${i.allocation.from},${i.allocation.to}[;suporte${i.allocation.reference};dedução anterior[${i.allocation.deductionFrom},${i.allocation.deductionTo}[;${i.allocation.deductionReference};` : ''}${r.reason}`,
|
||
]);
|
||
}
|
||
return out;
|
||
} catch {
|
||
return [
|
||
[
|
||
'IVA mensal',
|
||
'L3-E',
|
||
'Restituição não recuperável',
|
||
'Originais preservados;sem dedução/restituição presumidas.',
|
||
],
|
||
];
|
||
}
|
||
}
|