Files
Investidor-V2/resources/js/lib/iva-substitution.ts
T

654 lines
24 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 { inputText } from './input-text.ts';
import {
closureBook,
closureSources,
closureFingerprint,
type ClosureSource,
type ClosurePayment,
} from './iva-closure.ts';
import {
addFractions,
subtractFractions,
positiveFraction,
decimalFraction,
fraction,
fractionPresentation,
type FiscalFraction,
} from './fiscal-rational.ts';
export const substitutionSource =
'L3-C: VAT Return.pdf SHA2569c0b99a8,p2/quadro9;RCIVA15,Decreto7/2008 PDF3/p101.21=(17actual−18actual)−(17anterior−18anterior);devedor→22;credor só utilizável em declaração posterior em prazo após comunicação20. Sem repetir19/20 ou pagamentos anteriores. Exemplar recebido assinala substituiçãoModeloA;RCIVA15 refereModeloB:correspondência de formulário/submissão não automatizada.';
export type PriorDeclaration = {
kind: 'AB' | 'C';
id: string;
period: string;
entityRef: string;
origin: 'projected' | 'effective';
calculatedAt: string;
declarationDate: string;
declarationReference: string;
fingerprint: string;
debit: FiscalFraction;
credit: FiscalFraction;
paid: FiscalFraction;
outstanding: FiscalFraction;
carry: FiscalFraction;
paymentRefs: string[];
};
export type SubstitutionInput = {
id: string;
prior: PriorDeclaration | null;
source: ClosureSource | null;
priorReference: string;
priorDate: string;
date: string;
reference: string;
reason: string;
stage: 'preparation' | 'presented';
interest: '' | 'none' | 'documented';
interestAmount: string;
interestRef: string;
payments: ClosurePayment[];
};
export type SubstitutionResult = {
id: string;
status: 'determined' | 'indeterminate';
reason: string;
inputKey: string;
fields: Record<string, FiscalFraction | null>;
pendingCredit: FiscalFraction | null;
paid: FiscalFraction | null;
outstanding: FiscalFraction | null;
overpayment: FiscalFraction | null;
totalPaid: FiscalFraction | null;
totalOutstanding: FiscalFraction | null;
source: string;
};
export type SubstitutionCalculation = {
calculatedAt: string;
key: string;
inputs: SubstitutionInput[];
rows: SubstitutionResult[];
};
export type SubstitutionBook = {
version: 1;
rows: SubstitutionInput[];
result?: SubstitutionCalculation;
history?: SubstitutionCalculation[];
};
const zero = () => fraction(0n),
scope = (d: Record<string, unknown>) =>
inputText(d.context_opportunity || 'general'),
norm = (s: string) => s.trim().toLocaleLowerCase();
const date = (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;
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 fiscais não recuperáveis;originais preservados.');
return s;
}
function maps(d: Record<string, unknown>): Record<string, SubstitutionBook> {
const s = store(d).ivaSubstitutions;
if (s === undefined) return {};
if (!s || typeof s !== 'object' || Array.isArray(s))
throw Error('Substituições não recuperáveis;originais preservados.');
return s as Record<string, SubstitutionBook>;
}
function validate(b: SubstitutionBook): 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('Livro de substituições não recuperável;sem o substituir.');
for (const r of b.rows)
if (
!r ||
![
'id',
'priorReference',
'priorDate',
'date',
'reference',
'reason',
'interestAmount',
'interestRef',
].every(
(k) => typeof r[k as keyof SubstitutionInput] === 'string',
) ||
!['preparation', 'presented'].includes(r.stage) ||
!['', 'none', 'documented'].includes(r.interest) ||
!Array.isArray(r.payments) ||
r.payments.some(
(p) =>
!p ||
!['id', 'reference', 'date', 'amount'].every(
(k) => typeof p[k as keyof ClosurePayment] === 'string',
),
)
)
throw Error(
'Entradas da substituição não recuperáveis;originais preservados.',
);
}
export function substitutionBook(d: Record<string, unknown>): SubstitutionBook {
const b = maps(d)[scope(d)] ?? { version: 1, rows: [] };
validate(b);
return b;
}
export function saveSubstitutionBook(
d: Record<string, unknown>,
b: SubstitutionBook,
): string {
validate(b);
const s = store(d);
return JSON.stringify({
...s,
ivaSubstitutions: { ...maps(d), [scope(d)]: b },
});
}
export function newSubstitution(id: string): SubstitutionInput {
return {
id,
prior: null,
source: null,
priorReference: '',
priorDate: '',
date: '',
reference: '',
reason: '',
stage: 'preparation',
interest: '',
interestAmount: '',
interestRef: '',
payments: [],
};
}
export function previousDeclarations(
d: Record<string, unknown>,
b = substitutionBook(d),
): PriorDeclaration[] {
const ab = closureBook(d),
out: PriorDeclaration[] = [];
for (const calc of [
...(ab.history ?? []),
...(ab.result ? [ab.result] : []),
])
for (const r of calc.rows) {
const input = calc.inputs.find((i) => i.id === r.id);
if (
r.status !== 'determined' ||
!input ||
!r.fields['17'] ||
!r.fields['18'] ||
!r.fields['26'] ||
!r.paid ||
!r.outstanding
)
continue;
out.push({
kind: 'AB',
id: r.id,
period: r.period,
entityRef: r.entityRef,
origin: r.origin,
calculatedAt: calc.calculatedAt,
declarationDate: input.submissionDate,
declarationReference: '',
fingerprint: closureFingerprint([input, r]),
debit: r.fields['17'],
credit: r.fields['18'],
paid: r.paid,
outstanding: r.outstanding,
carry: r.fields['26'],
paymentRefs: input.payments.map((p) => p.reference),
});
}
for (const calc of [...(b.history ?? []), ...(b.result ? [b.result] : [])])
for (const r of calc.rows) {
const input = calc.inputs.find((i) => i.id === r.id);
if (
r.status !== 'determined' ||
!input?.prior ||
!input.source ||
!r.totalPaid ||
!r.totalOutstanding
)
continue;
out.push({
kind: 'C',
id: r.id,
period: input.prior.period,
entityRef: input.prior.entityRef,
origin: input.prior.origin,
calculatedAt: calc.calculatedAt,
declarationDate: input.date,
declarationReference: input.reference,
fingerprint: closureFingerprint([input, r]),
debit: input.source.debit!,
credit: input.source.credit!,
paid: r.totalPaid,
outstanding: r.totalOutstanding,
carry: input.prior.carry,
paymentRefs: [
...input.prior.paymentRefs,
...input.payments.map((p) => p.reference),
],
});
}
return out.filter(
(r, i, a) =>
a.findIndex(
(s) =>
s.kind === r.kind &&
s.id === r.id &&
s.fingerprint === r.fingerprint,
) === i,
);
}
function available(
d: Record<string, unknown>,
b: SubstitutionBook,
r: SubstitutionInput,
) {
return [
previousDeclarations(d, b).some(
(p) => JSON.stringify(p) === JSON.stringify(r.prior),
),
closureSources(d).some(
(s) => JSON.stringify(s) === JSON.stringify(r.source),
),
];
}
function pastPaymentUses(
d: Record<string, unknown>,
b: SubstitutionBook,
): Array<{ id: string; entity: string; origin: string; reference: string }> {
return Object.entries({ ...maps(d), [scope(d)]: b }).flatMap(
([context, book]) =>
[
...(book.history ?? []),
...(book.result ? [book.result] : []),
].flatMap((calc) =>
calc.inputs
.filter(
(r) =>
r.prior &&
(context === scope(d) ||
r.prior.origin === 'effective') &&
calc.rows.some(
(v) =>
v.id === r.id &&
v.status === 'determined' &&
v.paid,
),
)
.flatMap((r) =>
r.payments.map((p) => ({
id: r.id,
entity: norm(r.prior!.entityRef),
origin: r.prior!.origin,
reference: norm(p.reference),
})),
),
),
);
}
function abPaymentKeys(d: Record<string, unknown>): string[] {
const ab = store(d).ivaClosures as
| Record<string, ReturnType<typeof closureBook>>
| undefined;
return Object.entries(ab ?? {}).flatMap(([id, v]) =>
[
...v.rows,
...[...(v.history ?? []), ...(v.result ? [v.result] : [])].flatMap(
(c) => c.inputs,
),
]
.filter((r) => id === scope(d) || r.origin === 'effective')
.flatMap((r) =>
r.payments.map((p) =>
JSON.stringify([
norm(r.entityRef),
r.origin,
norm(p.reference),
]),
),
),
);
}
function key(d: Record<string, unknown>, b: SubstitutionBook) {
const ab = abPaymentKeys(d),
past = pastPaymentUses(d, b);
return closureFingerprint([
b.rows,
b.rows.map((r) => available(d, b, r)),
uses(d, b).map((r) => [
r.prior?.entityRef,
r.prior?.origin,
r.prior?.kind,
r.prior?.id,
r.payments.map((p) => norm(p.reference)),
]),
b.rows.map((r) =>
r.payments.map(
(p) =>
!!r.prior &&
(ab.includes(
JSON.stringify([
norm(r.prior.entityRef),
r.prior.origin,
norm(p.reference),
]),
) ||
past.some(
(v) =>
v.id !== r.id &&
v.entity === norm(r.prior!.entityRef) &&
v.origin === r.prior!.origin &&
v.reference === norm(p.reference),
)),
),
),
]);
}
export function substitutionCurrent(
d: Record<string, unknown>,
b: SubstitutionBook,
): boolean {
return (
!!b.result &&
b.result.key === key(d, b) &&
b.result.rows.length === b.rows.length &&
b.rows.every((r) =>
b.result!.rows.some(
(s) => s.id === r.id && s.inputKey === closureFingerprint(r),
),
)
);
}
function uses(
d: Record<string, unknown>,
b: SubstitutionBook,
): SubstitutionInput[] {
return [
...b.rows,
...Object.entries(maps(d))
.filter(([id]) => id !== scope(d))
.flatMap(([, v]) => {
validate(v);
return v.rows.filter((r) => r.prior?.origin === 'effective');
}),
];
}
export function recordSubstitution(
d: Record<string, unknown>,
b: SubstitutionBook,
now = new Date().toISOString(),
): SubstitutionBook {
validate(b);
if (new Set(b.rows.map((r) => r.id)).size !== b.rows.length)
throw Error('Identidade de substituição repetida.');
const all = uses(d, b),
paymentsAB = abPaymentKeys(d);
const rows: SubstitutionResult[] = b.rows.map((row) => {
const fields: Record<string, FiscalFraction | null> =
Object.fromEntries(
[
'17',
'18',
'19',
'20',
'21',
'22',
'23',
'24',
'25',
'26',
'27',
].map((k) => [k, null]),
);
const result: SubstitutionResult = {
id: row.id,
status: 'indeterminate',
reason: '',
inputKey: closureFingerprint(row),
fields,
pendingCredit: null,
paid: null,
outstanding: null,
overpayment: null,
totalPaid: null,
totalOutstanding: null,
source: substitutionSource,
};
const stop = (reason: string) => ({ ...result, reason });
const prior = row.prior,
source = row.source;
if (!prior || !source || !available(d, b, row).every(Boolean))
return stop(
'Escolha declaração anterior e resultadoL2 congelados recuperáveis;sem fabricar ou recalcular origens.',
);
if (
d.currency !== 'MZN' ||
source.currency !== 'MZN' ||
source.opportunity !== scope(d) ||
source.period !== prior.period ||
!/^2026-(0[1-9]|1[0-2])$/.test(prior.period) ||
!source.debit ||
!source.credit
)
return stop(
'A substituição exige mesmo âmbito e período2026/MZN,com17/18 determinados.',
);
if (
!prior.entityRef.trim() ||
(prior.origin === 'effective' && source.projected)
)
return stop(
'Origem projectada não serve como declaração efectiva;conservar pressupostos identificados.',
);
const seen = new Set([row.id]);
let ancestor: PriorDeclaration | null = prior;
while (ancestor?.kind === 'C') {
if (seen.has(ancestor.id)) break;
seen.add(ancestor.id);
ancestor = b.rows.find((r) => r.id === ancestor!.id)?.prior ?? null;
}
if (ancestor?.kind === 'C' && seen.has(ancestor.id))
return stop(
'Sequência circular de substituições;ligue cada versão à anterior sem ciclos.',
);
if (
all.filter(
(r) =>
r.prior &&
norm(r.prior.entityRef) === norm(prior.entityRef) &&
r.prior.origin === prior.origin &&
r.prior.kind === prior.kind &&
r.prior.id === prior.id,
).length > 1
)
return stop(
'Declaração anterior já ligada a outra substituição;reconcilie a sequência sem duplicar o diferencial.',
);
if (
!row.priorReference.trim() ||
row.priorDate !== prior.declarationDate ||
(prior.declarationReference &&
row.priorReference !== prior.declarationReference) ||
!date(row.priorDate) ||
row.priorDate.slice(0, 7) < prior.period ||
!date(row.date) ||
row.date < row.priorDate ||
!row.reference.trim() ||
!row.reason.trim()
)
return stop(
'Reutilize a data/referência disponível da versão anterior;identifique referência/data da declaração anterior e da substituição,com motivo;preparação não equivale a submissão.',
);
if (
BigInt(source.debit.numerator) < 0n ||
BigInt(source.credit.numerator) < 0n ||
(BigInt(source.debit.numerator) > 0n &&
BigInt(source.credit.numerator) > 0n)
)
return stop('Sinais17/18 incompatíveis na origemL2.');
fields['17'] = source.debit;
fields['18'] = source.credit;
fields['21'] = subtractFractions(
subtractFractions(source.debit, source.credit),
subtractFractions(prior.debit, prior.credit),
);
fields['22'] = positiveFraction(fields['21']);
result.pendingCredit = positiveFraction(
subtractFractions(zero(), fields['21']),
);
if (row.interest === 'none' && row.interestRef.trim())
fields['23'] = zero();
else if (row.interest === 'documented' && row.interestRef.trim())
try {
fields['23'] = decimalFraction(row.interestAmount);
} catch {
/* unknown preserved */
}
if (!fields['23'])
return stop(
'Juros adicionais por identificar;sem copiar juros anteriores ou presumir taxa/prazo.',
);
fields['24'] = addFractions([fields['22'], fields['23']]);
const paid: FiscalFraction[] = [];
for (const p of row.payments) {
const ident = JSON.stringify([
norm(prior.entityRef),
prior.origin,
norm(p.reference),
]);
if (
!p.reference.trim() ||
!date(p.date) ||
p.date < row.date ||
prior.paymentRefs.some(
(ref) => norm(ref) === norm(p.reference),
) ||
pastPaymentUses(d, b).some(
(v) =>
v.id !== row.id &&
v.entity === norm(prior.entityRef) &&
v.origin === prior.origin &&
v.reference === norm(p.reference),
) ||
paymentsAB.includes(ident) ||
all.flatMap((r) =>
r.payments.filter(
(p2) =>
r.prior &&
JSON.stringify([
norm(r.prior.entityRef),
r.prior.origin,
norm(p2.reference),
]) === ident,
),
).length > 1
)
return stop(
'Pagamento já registado na anterior/outra declaração,sem referência/data aplicável;registe só pagamentos adicionais.',
);
try {
paid.push(decimalFraction(p.amount));
} catch {
return stop(
'Montante adicional não identificado;sem baixa presumida.',
);
}
}
result.paid = addFractions(paid);
result.outstanding = positiveFraction(
subtractFractions(fields['24'], result.paid),
);
result.overpayment = positiveFraction(
subtractFractions(result.paid, fields['24']),
);
result.totalPaid = addFractions([prior.paid, result.paid]);
result.totalOutstanding = addFractions([
prior.outstanding,
result.outstanding,
]);
result.status = 'determined';
result.reason =
'Diferencial21 separado da declaração anterior;19/20 não reutilizados,25/26/27 não atribuídos por esta substituição. Crédito credor aguarda comunicação20;sem compensar dívida anterior automaticamente. Pagamentos adicionais e saldo anterior separados;sem submissãoAT ou certificação de cobrança. ' +
(row.stage === 'preparation'
? 'Preparação/simulação;referências e datas declaradas não comprovam entrega.'
: 'Apresentação registada pelo utilizador fora do InvesteMoz.');
return result;
});
const result = {
calculatedAt: now,
key: key(d, b),
inputs: structuredClone(b.rows),
rows,
};
return {
...b,
...(b.result
? { history: [...(b.history ?? []), structuredClone(b.result)] }
: {}),
result,
};
}
export function substitutionReportRows(
d: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const b = substitutionBook(d);
if (!b.rows.length) return [];
const out: Array<[string, string, string, string]> = [
[
'IVA mensal',
'L3-C — substituições',
substitutionCurrent(d, b)
? 'Resultado correspondente às entradas'
: 'Histórico preservado;rever entradas/origens',
substitutionSource,
],
];
const fmt = (v: FiscalFraction | null | undefined) =>
v ? fractionPresentation(v).decimal : 'Indeterminado';
for (const r of b.result?.rows ?? []) {
const input = b.result!.inputs.find((i) => i.id === r.id),
p = input?.prior;
out.push([
'IVA mensal',
`Substituição — ${p?.period ?? 'por identificar'}`,
`21:${fmt(r.fields['21'])};22 adicional:${fmt(r.fields['22'])};23 adicional:${fmt(r.fields['23'])};24 adicional:${fmt(r.fields['24'])};crédito aguardando comunicação:${fmt(r.pendingCredit)}`,
`${p?.entityRef ?? ''};${scope(d)};MZN;${p?.origin === 'effective' ? 'Dados efectivos declarados' : 'Pressuposto de simulação'};anterior ${input?.priorReference ?? ''},${input?.priorDate ?? ''};substituição ${input?.reference ?? ''},${input?.date ?? ''};${input?.reason ?? ''};${r.reason}`,
]);
out.push([
'IVA mensal',
`Reconciliação da substituição — ${p?.period ?? ''}`,
`Anterior:pago${fmt(p?.paid)},por liquidar${fmt(p?.outstanding)},reporte histórico26:${fmt(p?.carry)};adicional:pago${fmt(r.paid)},por liquidar${fmt(r.outstanding)},excedente${fmt(r.overpayment)};total registado${fmt(r.totalPaid)},total por liquidar sem compensação${fmt(r.totalOutstanding)}`,
`Origem anterior${p?.kind}/${p?.id},versão${p?.calculatedAt};origemL2 revisão${input?.source?.revision},${input?.source?.calculatedAt};pagamentos adicionais:${input?.payments.map((p) => `${p.reference};${p.date};${p.amount}MZN`).join(' | ') || 'nenhum'};juros adicionais:${input?.interestRef ?? ''};histórico${b.result!.calculatedAt};${substitutionSource}`,
]);
}
return out;
} catch {
return [
[
'IVA mensal',
'L3-C',
'Substituição não recuperável',
'Originais preservados;sem diferencial/crédito presumidos.',
],
];
}
}