1056 lines
38 KiB
TypeScript
1056 lines
38 KiB
TypeScript
import { inputText } from './input-text.ts';
|
||
import type { AcquisitionCalculation } from './iva-acquisitions.ts';
|
||
import {
|
||
addFractions,
|
||
decimalFraction,
|
||
fraction,
|
||
subtractFractions,
|
||
positiveFraction,
|
||
fractionLabel,
|
||
fractionPresentation,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
|
||
export const closureVersion = 'iva-closure-ab/1';
|
||
export const closureSource =
|
||
'L3-A/B: CIVA21(2/5),Lei13/2016 PDF15/p1426(273),23(1),PDF16/p1426(274);RCIVA12/15,Decreto7/2008 PDF3/p101;ModeloA/VAT Return9c0b99a8,p1–2,quadros8/10/11. Crédito anterior/comunicado19/20;22=max(17−19−20,0);25=max(18+19+20−17,0);24=22+23;reporte integral26=25 quando expressamente escolhido. Sem substituição21,reembolso27 ou restituição13(2). Cabeçalho remete18/19 mas caixas são19/20;divergência conservada,distinta deP00105/06.';
|
||
type Choice = '' | 'yes' | 'no';
|
||
export type ClosureSource = {
|
||
revision: number;
|
||
calculatedAt: string;
|
||
fingerprint: string;
|
||
opportunity: string;
|
||
currency: string;
|
||
period: string;
|
||
location: string;
|
||
projected: boolean;
|
||
debit: FiscalFraction | null;
|
||
credit: FiscalFraction | null;
|
||
};
|
||
export type ClosureCredit = {
|
||
id: string;
|
||
kind: 'opening' | 'communication' | 'previous';
|
||
period: string;
|
||
reference: string;
|
||
date: string;
|
||
amount: string;
|
||
previousId: string;
|
||
carryKey: string;
|
||
};
|
||
export type ClosurePayment = {
|
||
id: string;
|
||
reference: string;
|
||
date: string;
|
||
amount: string;
|
||
};
|
||
export type ClosureInput = {
|
||
id: string;
|
||
period: string;
|
||
entityRef: string;
|
||
origin: 'projected' | 'effective';
|
||
source: ClosureSource | null;
|
||
firstDeclaration: Choice;
|
||
filing: '' | 'timely' | 'late';
|
||
submissionDate: string;
|
||
filingRef: string;
|
||
creditsComplete: Choice;
|
||
credits: ClosureCredit[];
|
||
interest: '' | 'none' | 'documented';
|
||
interestAmount: string;
|
||
interestRef: string;
|
||
destination: '' | 'report' | 'refund';
|
||
refundAmount?: string;
|
||
payments: ClosurePayment[];
|
||
};
|
||
export type ClosureResult = {
|
||
id: string;
|
||
period: string;
|
||
entityRef: string;
|
||
origin: ClosureInput['origin'];
|
||
status: 'determined' | 'indeterminate';
|
||
reason: string;
|
||
inputKey: string;
|
||
financialKey: string;
|
||
carryKey: string;
|
||
fields: Record<string, FiscalFraction | null>;
|
||
paid: FiscalFraction | null;
|
||
outstanding: FiscalFraction | null;
|
||
overpayment: FiscalFraction | null;
|
||
source: string;
|
||
};
|
||
export type ClosureBook = {
|
||
version: 1;
|
||
rows: ClosureInput[];
|
||
result?: {
|
||
calculatedAt: string;
|
||
key: string;
|
||
inputs: ClosureInput[];
|
||
rows: ClosureResult[];
|
||
};
|
||
history?: Array<NonNullable<ClosureBook['result']>>;
|
||
};
|
||
const scope = (d: Record<string, unknown>) =>
|
||
inputText(d.context_opportunity || 'general');
|
||
const zero = () => fraction(0n);
|
||
export function closureFingerprint(v: unknown): string {
|
||
const raw = JSON.stringify(v);
|
||
let h = 14695981039346656037n;
|
||
for (let i = 0; i < raw.length; i++)
|
||
h = BigInt.asUintN(
|
||
64,
|
||
(h ^ BigInt(raw.charCodeAt(i))) * 1099511628211n,
|
||
);
|
||
return `${raw.length}:${h.toString(16)}`;
|
||
}
|
||
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, ClosureBook> {
|
||
const v = store(d).ivaClosures;
|
||
if (v === undefined) return {};
|
||
if (!v || typeof v !== 'object' || Array.isArray(v))
|
||
throw Error('Mapa de fechos não recuperável;originais preservados.');
|
||
return v as Record<string, ClosureBook>;
|
||
}
|
||
function validate(b: ClosureBook): 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) ||
|
||
typeof b.result.key !== 'string'))
|
||
)
|
||
throw Error('FechoL3 não recuperável;originais preservados.');
|
||
if (
|
||
b.result?.rows.some(
|
||
(r) =>
|
||
!r ||
|
||
typeof r.id !== 'string' ||
|
||
!r.fields ||
|
||
typeof r.fields !== 'object' ||
|
||
!['determined', 'indeterminate'].includes(r.status) ||
|
||
Object.values(r.fields).some(
|
||
(v) =>
|
||
v !== null &&
|
||
(!v ||
|
||
!/^\d{1,80}$/.test(v.numerator) ||
|
||
!/^\d{1,80}$/.test(v.denominator) ||
|
||
BigInt(v.denominator) === 0n),
|
||
),
|
||
)
|
||
)
|
||
throw Error('ResultadoL3 não recuperável;originais preservados.');
|
||
for (const r of b.rows) {
|
||
if (
|
||
!r ||
|
||
![
|
||
'id',
|
||
'period',
|
||
'entityRef',
|
||
'submissionDate',
|
||
'filingRef',
|
||
'interestAmount',
|
||
'interestRef',
|
||
].every((k) => typeof r[k as keyof ClosureInput] === 'string') ||
|
||
!['projected', 'effective'].includes(r.origin) ||
|
||
!['', 'yes', 'no'].includes(r.firstDeclaration) ||
|
||
!['', 'yes', 'no'].includes(r.creditsComplete) ||
|
||
!['', 'timely', 'late'].includes(r.filing) ||
|
||
!['', 'none', 'documented'].includes(r.interest) ||
|
||
!['', 'report', 'refund'].includes(r.destination) ||
|
||
(r.refundAmount !== undefined &&
|
||
typeof r.refundAmount !== 'string') ||
|
||
!Array.isArray(r.credits) ||
|
||
!Array.isArray(r.payments) ||
|
||
r.credits.some(
|
||
(c) =>
|
||
!c ||
|
||
!['opening', 'communication', 'previous'].includes(
|
||
c.kind,
|
||
) ||
|
||
![
|
||
'id',
|
||
'period',
|
||
'reference',
|
||
'date',
|
||
'amount',
|
||
'previousId',
|
||
'carryKey',
|
||
].every(
|
||
(k) => typeof c[k as keyof ClosureCredit] === 'string',
|
||
),
|
||
) ||
|
||
r.payments.some(
|
||
(p) =>
|
||
!p ||
|
||
!['id', 'reference', 'date', 'amount'].every(
|
||
(k) => typeof p[k as keyof ClosurePayment] === 'string',
|
||
),
|
||
) ||
|
||
(r.source !== null &&
|
||
(!r.source ||
|
||
!Number.isInteger(r.source.revision) ||
|
||
![
|
||
'calculatedAt',
|
||
'fingerprint',
|
||
'opportunity',
|
||
'currency',
|
||
'period',
|
||
'location',
|
||
].every(
|
||
(k) =>
|
||
typeof r.source![k as keyof ClosureSource] ===
|
||
'string',
|
||
)))
|
||
)
|
||
throw Error(
|
||
'EntradasL3 não recuperáveis;originais preservados,sem substituir por zero.',
|
||
);
|
||
}
|
||
}
|
||
export function closureBook(d: Record<string, unknown>): ClosureBook {
|
||
const b = maps(d)[scope(d)] ?? { version: 1, rows: [] };
|
||
validate(b);
|
||
return b;
|
||
}
|
||
export function saveClosureBook(
|
||
d: Record<string, unknown>,
|
||
b: ClosureBook,
|
||
): string {
|
||
validate(b);
|
||
const s = store(d);
|
||
return JSON.stringify({ ...s, ivaClosures: { ...maps(d), [scope(d)]: b } });
|
||
}
|
||
export function closureSources(d: Record<string, unknown>): ClosureSource[] {
|
||
const b = (
|
||
store(d).books as Record<
|
||
string,
|
||
{
|
||
result?: AcquisitionCalculation;
|
||
history?: AcquisitionCalculation[];
|
||
}
|
||
>
|
||
)[scope(d)];
|
||
return b
|
||
? [...(b.history ?? []), ...(b.result ? [b.result] : [])]
|
||
.flatMap((calc) =>
|
||
calc.periods.map((p) => ({
|
||
revision: calc.revision,
|
||
calculatedAt: calc.calculatedAt,
|
||
fingerprint: closureFingerprint(calc),
|
||
opportunity: calc.opportunity,
|
||
currency: calc.currency,
|
||
period: p.period,
|
||
location: calc.location,
|
||
projected:
|
||
calc.inputs.length === 0 ||
|
||
calc.inputs.some((r) => r.origin === 'projected'),
|
||
debit:
|
||
p.exactFields?.['17'] ??
|
||
(p.fields['17'] === null
|
||
? null
|
||
: decimalFraction(p.fields['17'])),
|
||
credit:
|
||
p.exactFields?.['18'] ??
|
||
(p.fields['18'] === null
|
||
? null
|
||
: decimalFraction(p.fields['18'])),
|
||
})),
|
||
)
|
||
.filter(
|
||
(r, i, a) =>
|
||
a.findIndex(
|
||
(s) =>
|
||
s.fingerprint === r.fingerprint &&
|
||
s.period === r.period,
|
||
) === i,
|
||
)
|
||
: [];
|
||
}
|
||
export function newClosure(
|
||
id: string,
|
||
source: ClosureSource | null,
|
||
entityRef = '',
|
||
): ClosureInput {
|
||
return {
|
||
id,
|
||
period: source?.period ?? '',
|
||
entityRef,
|
||
origin: 'projected',
|
||
source: source ? structuredClone(source) : null,
|
||
firstDeclaration: '',
|
||
filing: '',
|
||
submissionDate: '',
|
||
filingRef: '',
|
||
creditsComplete: '',
|
||
credits: [],
|
||
interest: '',
|
||
interestAmount: '',
|
||
interestRef: '',
|
||
destination: '',
|
||
payments: [],
|
||
};
|
||
}
|
||
export function newClosureCredit(
|
||
id: string,
|
||
kind: ClosureCredit['kind'],
|
||
): ClosureCredit {
|
||
return {
|
||
id,
|
||
kind,
|
||
period: '',
|
||
reference: '',
|
||
date: '',
|
||
amount: '',
|
||
previousId: '',
|
||
carryKey: '',
|
||
};
|
||
}
|
||
const priorPeriod = (p: string) => {
|
||
const d = new Date(p + '-01T00:00:00Z');
|
||
d.setUTCMonth(d.getUTCMonth() - 1);
|
||
return d.toISOString().slice(0, 7);
|
||
};
|
||
const validDate = (d: string) =>
|
||
/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(d) &&
|
||
!Number.isNaN(Date.parse(d)) &&
|
||
new Date(d).toISOString().slice(0, 10) === d;
|
||
const normalize = (s: string) => s.trim().toLocaleLowerCase();
|
||
function creditIdentity(r: ClosureInput, c: ClosureCredit) {
|
||
return closureFingerprint([
|
||
normalize(r.entityRef),
|
||
r.origin,
|
||
c.kind === 'previous' ? 'carry' : 'document',
|
||
c.kind === 'previous' ? c.previousId : normalize(c.reference),
|
||
c.period,
|
||
]);
|
||
}
|
||
function paymentIdentity(r: ClosureInput, p: ClosurePayment) {
|
||
return closureFingerprint([
|
||
normalize(r.entityRef),
|
||
r.origin,
|
||
normalize(p.reference),
|
||
]);
|
||
}
|
||
function inputKey(r: ClosureInput) {
|
||
return closureFingerprint(r);
|
||
}
|
||
function financialKey(r: ClosureInput) {
|
||
const { payments: _, ...rest } = r;
|
||
return closureFingerprint(rest);
|
||
}
|
||
function otherUses(d: Record<string, unknown>, b: ClosureBook): ClosureInput[] {
|
||
return [
|
||
...b.rows,
|
||
...Object.entries(maps(d))
|
||
.filter(([id]) => id !== scope(d))
|
||
.flatMap(([, book]) => {
|
||
validate(book);
|
||
return book.rows.filter((r) => r.origin === 'effective');
|
||
}),
|
||
];
|
||
}
|
||
function duplicateCredits(
|
||
d: Record<string, unknown>,
|
||
b: ClosureBook,
|
||
): Set<string> {
|
||
const uses = new Map<string, number>();
|
||
for (const r of otherUses(d, b))
|
||
for (const c of r.credits) {
|
||
const k = creditIdentity(r, c);
|
||
uses.set(k, (uses.get(k) ?? 0) + 1);
|
||
}
|
||
return new Set([...uses].filter(([, n]) => n > 1).map(([k]) => k));
|
||
}
|
||
function substitutionPayments(d: Record<string, unknown>): string[] {
|
||
const m = store(d).ivaSubstitutions as
|
||
| Record<
|
||
string,
|
||
{
|
||
result?: {
|
||
inputs: Array<{
|
||
id: string;
|
||
prior: { entityRef: string; origin: string } | null;
|
||
payments: ClosurePayment[];
|
||
}>;
|
||
rows: Array<{
|
||
id: string;
|
||
status: string;
|
||
paid: FiscalFraction | null;
|
||
}>;
|
||
};
|
||
history?: Array<{
|
||
inputs: Array<{
|
||
id: string;
|
||
prior: { entityRef: string; origin: string } | null;
|
||
payments: ClosurePayment[];
|
||
}>;
|
||
rows: Array<{
|
||
id: string;
|
||
status: string;
|
||
paid: FiscalFraction | null;
|
||
}>;
|
||
}>;
|
||
}
|
||
>
|
||
| undefined;
|
||
return Object.entries(m ?? {}).flatMap(([id, b]) =>
|
||
[...(b.history ?? []), ...(b.result ? [b.result] : [])].flatMap((c) =>
|
||
c.inputs
|
||
.filter(
|
||
(r) =>
|
||
r.prior &&
|
||
(id === scope(d) || r.prior.origin === 'effective') &&
|
||
c.rows.some(
|
||
(v) =>
|
||
v.id === r.id &&
|
||
v.status === 'determined' &&
|
||
v.paid,
|
||
),
|
||
)
|
||
.flatMap((r) =>
|
||
r.payments.map((p) =>
|
||
closureFingerprint([
|
||
normalize(r.prior!.entityRef),
|
||
r.prior!.origin,
|
||
normalize(p.reference),
|
||
]),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
function bookKey(d: Record<string, unknown>, b: ClosureBook) {
|
||
const sources = closureSources(d);
|
||
const values: unknown[] = [
|
||
b.rows,
|
||
b.rows.map((r) =>
|
||
sources.some((s) => JSON.stringify(s) === JSON.stringify(r.source)),
|
||
),
|
||
otherUses(d, b).map((r) => [
|
||
normalize(r.entityRef),
|
||
r.origin,
|
||
r.period,
|
||
r.credits.map((c) => creditIdentity(r, c)),
|
||
r.payments.map((p) => paymentIdentity(r, p)),
|
||
]),
|
||
];
|
||
const reserved = substitutionPayments(d),
|
||
conflicts = b.rows
|
||
.flatMap((r) => r.payments.map((p) => paymentIdentity(r, p)))
|
||
.filter((k) => reserved.includes(k));
|
||
if (conflicts.length) values.push(conflicts);
|
||
return closureFingerprint(values);
|
||
}
|
||
export function closureCurrent(
|
||
d: Record<string, unknown>,
|
||
b: ClosureBook,
|
||
): boolean {
|
||
return (
|
||
!!b.result &&
|
||
b.result.key === bookKey(d, b) &&
|
||
b.result.rows.length === b.rows.length &&
|
||
b.rows.every(
|
||
(r) =>
|
||
b.result!.rows.filter(
|
||
(x) => x.id === r.id && x.inputKey === inputKey(r),
|
||
).length === 1,
|
||
)
|
||
);
|
||
}
|
||
export function adoptPreviousCredit(
|
||
c: ClosureCredit,
|
||
previous: ClosureResult,
|
||
): ClosureCredit {
|
||
if (previous.status !== 'determined' || !previous.fields['26'])
|
||
throw Error('Fecho anterior indeterminado;sem crédito presumido.');
|
||
return {
|
||
...c,
|
||
kind: 'previous',
|
||
period: previous.period,
|
||
reference: `Fecho ${previous.id}`,
|
||
previousId: previous.id,
|
||
carryKey: previous.carryKey,
|
||
amount: fractionPresentation(previous.fields['26']).decimal,
|
||
date: '',
|
||
};
|
||
}
|
||
function refundLockConflict(
|
||
d: Record<string, unknown>,
|
||
row: ClosureInput,
|
||
fields: Record<string, FiscalFraction | null>,
|
||
): boolean {
|
||
const books = store(d).ivaRefunds as
|
||
| Record<
|
||
string,
|
||
{
|
||
rows: Array<{
|
||
source: {
|
||
closureId: string;
|
||
entityRef: string;
|
||
origin: string;
|
||
period: string;
|
||
credit: FiscalFraction;
|
||
carry: FiscalFraction;
|
||
requested: FiscalFraction;
|
||
} | null;
|
||
stage: string;
|
||
decision: string;
|
||
receipts: unknown[];
|
||
}>;
|
||
result?: {
|
||
inputs: Array<{
|
||
source: {
|
||
closureId: string;
|
||
entityRef: string;
|
||
origin: string;
|
||
period: string;
|
||
credit: FiscalFraction;
|
||
carry: FiscalFraction;
|
||
requested: FiscalFraction;
|
||
} | null;
|
||
stage: string;
|
||
decision: string;
|
||
receipts: unknown[];
|
||
}>;
|
||
};
|
||
history?: Array<{
|
||
inputs: Array<{
|
||
source: {
|
||
closureId: string;
|
||
entityRef: string;
|
||
origin: string;
|
||
period: string;
|
||
credit: FiscalFraction;
|
||
carry: FiscalFraction;
|
||
requested: FiscalFraction;
|
||
} | null;
|
||
stage: string;
|
||
decision: string;
|
||
receipts: unknown[];
|
||
}>;
|
||
}>;
|
||
}
|
||
>
|
||
| undefined;
|
||
return Object.entries(books ?? {}).some(([context, book]) =>
|
||
[
|
||
...book.rows,
|
||
...[
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
].flatMap((c) => c.inputs),
|
||
].some(
|
||
(r) =>
|
||
r.source &&
|
||
(context === scope(d) || r.source.origin === 'effective') &&
|
||
r.source.closureId === row.id &&
|
||
normalize(r.source.entityRef) === normalize(row.entityRef) &&
|
||
r.source.origin === row.origin &&
|
||
r.source.period === row.period &&
|
||
(r.stage === 'submitted' ||
|
||
r.decision !== 'pending' ||
|
||
r.receipts.length > 0) &&
|
||
[
|
||
['25', r.source.credit],
|
||
['26', r.source.carry],
|
||
['27', r.source.requested],
|
||
].some(
|
||
([f, v]) =>
|
||
JSON.stringify(fields[f as string]) !==
|
||
JSON.stringify(v),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
function effectiveCarryAlreadyUsed(
|
||
d: Record<string, unknown>,
|
||
row: ClosureInput,
|
||
fields: Record<string, FiscalFraction | null>,
|
||
): boolean {
|
||
if (row.origin !== 'effective' || row.destination !== 'refund')
|
||
return false;
|
||
const books = maps(d),
|
||
own = books[scope(d)];
|
||
if (!own) return false;
|
||
const prior = [...(own.history ?? []), ...(own.result ? [own.result] : [])]
|
||
.flatMap((c) => c.rows)
|
||
.filter(
|
||
(r) =>
|
||
r.id === row.id &&
|
||
r.origin === 'effective' &&
|
||
r.fields['26'] &&
|
||
fields['26'] &&
|
||
BigInt(
|
||
subtractFractions(r.fields['26'], fields['26']).numerator,
|
||
) > 0n,
|
||
);
|
||
return prior.some((p) =>
|
||
Object.values(books).some((b) =>
|
||
[...(b.history ?? []), ...(b.result ? [b.result] : [])].some((c) =>
|
||
c.inputs.some(
|
||
(input) =>
|
||
input.origin === 'effective' &&
|
||
normalize(input.entityRef) ===
|
||
normalize(row.entityRef) &&
|
||
input.period > row.period &&
|
||
input.credits.some(
|
||
(credit) =>
|
||
credit.kind === 'previous' &&
|
||
credit.previousId === p.id &&
|
||
credit.carryKey === p.carryKey,
|
||
) &&
|
||
c.rows.some(
|
||
(r) =>
|
||
r.id === input.id && r.status === 'determined',
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
export function recordClosure(
|
||
d: Record<string, unknown>,
|
||
b: ClosureBook,
|
||
now = new Date().toISOString(),
|
||
): ClosureBook {
|
||
validate(b);
|
||
if (new Set(b.rows.map((r) => r.id)).size !== b.rows.length)
|
||
throw Error('Identidade de fecho repetida.');
|
||
const contexts = b.rows.map((r) =>
|
||
JSON.stringify([normalize(r.entityRef), r.origin, r.period]),
|
||
);
|
||
if (new Set(contexts).size !== contexts.length)
|
||
throw Error(
|
||
'Mesmo âmbito/período repetido neste cenário;sem duplicar o fecho.',
|
||
);
|
||
const duplicate = duplicateCredits(d, b),
|
||
puses = new Map<string, number>();
|
||
for (const r of otherUses(d, b))
|
||
for (const p of r.payments) {
|
||
const k = paymentIdentity(r, p);
|
||
puses.set(k, (puses.get(k) ?? 0) + 1);
|
||
}
|
||
const results: ClosureResult[] = [];
|
||
for (const row of [...b.rows].sort((a, z) =>
|
||
a.period.localeCompare(z.period),
|
||
)) {
|
||
const fields: Record<string, FiscalFraction | null> =
|
||
Object.fromEntries(
|
||
[
|
||
'17',
|
||
'18',
|
||
'19',
|
||
'20',
|
||
'21',
|
||
'22',
|
||
'23',
|
||
'24',
|
||
'25',
|
||
'26',
|
||
'27',
|
||
].map((f) => [f, null]),
|
||
);
|
||
const r: ClosureResult = {
|
||
id: row.id,
|
||
period: row.period,
|
||
entityRef: row.entityRef,
|
||
origin: row.origin,
|
||
status: 'indeterminate',
|
||
reason: '',
|
||
inputKey: inputKey(row),
|
||
financialKey: financialKey(row),
|
||
carryKey: '',
|
||
fields,
|
||
paid: null,
|
||
outstanding: null,
|
||
overpayment: null,
|
||
source: closureSource,
|
||
};
|
||
const stop = (reason: string) => {
|
||
r.reason = reason;
|
||
results.push(r);
|
||
};
|
||
if (
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
||
!row.entityRef.trim() ||
|
||
d.currency !== 'MZN'
|
||
) {
|
||
stop(
|
||
'Identifique âmbito fiscal e período2026/MZN;sem consolidar oportunidades automaticamente.',
|
||
);
|
||
continue;
|
||
}
|
||
if (
|
||
row.origin === 'effective' &&
|
||
otherUses(d, b).some(
|
||
(other) =>
|
||
other !== row &&
|
||
other.origin === 'effective' &&
|
||
normalize(other.entityRef) === normalize(row.entityRef) &&
|
||
other.period === row.period,
|
||
)
|
||
) {
|
||
stop(
|
||
'Mesmo âmbito fiscal/período efectivo reservado noutra oportunidade recuperada;reconcilie a declaração única,sem duplicar ou consolidar dossiers silenciosamente.',
|
||
);
|
||
continue;
|
||
}
|
||
const source = closureSources(d).find(
|
||
(s) => JSON.stringify(s) === JSON.stringify(row.source),
|
||
);
|
||
if (
|
||
!source ||
|
||
source.period !== row.period ||
|
||
source.opportunity !== scope(d) ||
|
||
source.currency !== 'MZN' ||
|
||
!source.debit ||
|
||
!source.credit
|
||
) {
|
||
stop(
|
||
'ResultadoL2 congelado correspondente ausente ou17/18 indeterminados;sem recalcular nem presumir impostozero.',
|
||
);
|
||
continue;
|
||
}
|
||
fields['17'] = source.debit;
|
||
fields['18'] = source.credit;
|
||
if (
|
||
BigInt(source.debit.numerator) < 0n ||
|
||
BigInt(source.credit.numerator) < 0n ||
|
||
(BigInt(source.debit.numerator) > 0n &&
|
||
BigInt(source.credit.numerator) > 0n)
|
||
) {
|
||
stop('Sinais17/18 incompatíveis;resolver origemL2.');
|
||
continue;
|
||
}
|
||
if (row.origin === 'effective' && source.projected) {
|
||
stop(
|
||
'OrigemL2 contém pressupostos projectados;não os apresentar como declaração efectiva.',
|
||
);
|
||
continue;
|
||
}
|
||
if (row.firstDeclaration !== 'yes') {
|
||
stop(
|
||
'Este percursoA/B exige primeira declaração;substituição21 permaneceL3-C.',
|
||
);
|
||
continue;
|
||
}
|
||
if (
|
||
!validDate(row.submissionDate) ||
|
||
row.submissionDate.slice(0, 7) < row.period ||
|
||
!row.filingRef.trim() ||
|
||
row.filing !== 'timely'
|
||
) {
|
||
stop(
|
||
row.filing === 'late'
|
||
? 'Declaração fora do prazo:RCIVA15 e exemplar não permitem uso19/20 neste percurso;conservar conta de crédito e tratar procedimento próprio,sem o apagar.'
|
||
: 'Indique data e fundamento da entrega em prazo,efectivos ou pressupostos;sem calendário/prazo presumido.',
|
||
);
|
||
continue;
|
||
}
|
||
if (row.creditsComplete !== 'yes') {
|
||
stop(
|
||
'Reconcilie todos os créditos aplicáveis;lista vazia só significa ausência quando expressamente confirmada.',
|
||
);
|
||
continue;
|
||
}
|
||
const credits19: FiscalFraction[] = [],
|
||
credits20: FiscalFraction[] = [];
|
||
let invalid = '';
|
||
for (const c of row.credits) {
|
||
if (
|
||
!c.reference.trim() ||
|
||
!/^20\d{2}-(0[1-9]|1[0-2])$/.test(c.period) ||
|
||
c.period > row.period ||
|
||
(c.kind !== 'communication' && c.period === row.period) ||
|
||
duplicate.has(creditIdentity(row, c))
|
||
) {
|
||
invalid =
|
||
'Crédito sem origem/período aplicável ou já utilizado/reservado noutra linha/âmbito recuperado;sem utilização duplicada.';
|
||
break;
|
||
}
|
||
if (c.kind === 'previous') {
|
||
const p = results.find((p) => p.id === c.previousId);
|
||
if (
|
||
!p ||
|
||
p.status !== 'determined' ||
|
||
p.period !== priorPeriod(row.period) ||
|
||
p.entityRef.trim().toLowerCase() !==
|
||
row.entityRef.trim().toLowerCase() ||
|
||
p.origin !== row.origin ||
|
||
p.period !== c.period ||
|
||
p.carryKey !== c.carryKey ||
|
||
!p.fields['26']
|
||
) {
|
||
invalid =
|
||
'Fecho anterior alterado,indeterminado,fora do mês imediatamente anterior ou de outro âmbito;não adoptar novo crédito silenciosamente.';
|
||
break;
|
||
}
|
||
credits19.push(p.fields['26']);
|
||
} else {
|
||
let v: FiscalFraction;
|
||
try {
|
||
v = decimalFraction(c.amount);
|
||
} catch {
|
||
invalid =
|
||
'Montante de crédito em falta/inválido;não substituir porzero.';
|
||
break;
|
||
}
|
||
if (
|
||
c.kind === 'opening' &&
|
||
c.period !== priorPeriod(row.period)
|
||
) {
|
||
invalid =
|
||
'Saldo de abertura exige referência ao saldo disponível do mês imediatamente anterior;origem mais antiga deve ser reconciliada nesse saldo.';
|
||
break;
|
||
}
|
||
if (
|
||
c.kind === 'opening' &&
|
||
b.rows.some(
|
||
(p) =>
|
||
p.id !== row.id &&
|
||
p.entityRef.trim().toLowerCase() ===
|
||
row.entityRef.trim().toLowerCase() &&
|
||
p.origin === row.origin &&
|
||
p.period === priorPeriod(row.period),
|
||
)
|
||
) {
|
||
invalid =
|
||
'Há fecho do período anterior neste livro;reutilize26 expressamente em vez de introduzir outra abertura.';
|
||
break;
|
||
}
|
||
if (
|
||
c.kind === 'communication' &&
|
||
(!validDate(c.date) || c.date > row.submissionDate)
|
||
) {
|
||
invalid =
|
||
'Crédito20 exige comunicação recebida,referência e data não posterior à entrega declarada;pedido não é comunicação.';
|
||
break;
|
||
}
|
||
(c.kind === 'opening' ? credits19 : credits20).push(v);
|
||
}
|
||
}
|
||
if (invalid) {
|
||
stop(invalid);
|
||
continue;
|
||
}
|
||
fields['19'] = addFractions(credits19);
|
||
fields['20'] = addFractions(credits20);
|
||
const balance = subtractFractions(
|
||
addFractions([source.credit, fields['19'], fields['20']]),
|
||
source.debit,
|
||
);
|
||
fields['22'] = positiveFraction(
|
||
fraction(-BigInt(balance.numerator), BigInt(balance.denominator)),
|
||
);
|
||
fields['25'] = positiveFraction(balance);
|
||
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 {
|
||
/* Preserve unknown. */
|
||
}
|
||
}
|
||
fields['24'] = fields['23']
|
||
? addFractions([fields['22'], fields['23']])
|
||
: null;
|
||
if (row.destination === 'report') {
|
||
fields['26'] = fields['25'];
|
||
fields['27'] = zero();
|
||
} else if (row.destination === 'refund') {
|
||
let amount: FiscalFraction;
|
||
try {
|
||
amount = decimalFraction(row.refundAmount ?? '');
|
||
} catch {
|
||
stop(
|
||
'Identifique a parcela destinada ao pedido;sem montante presumido.',
|
||
);
|
||
continue;
|
||
}
|
||
if (
|
||
BigInt(amount.numerator) === 0n ||
|
||
BigInt(subtractFractions(fields['25'], amount).numerator) < 0n
|
||
) {
|
||
stop(
|
||
'Parcela do pedido deve ser positiva e não exceder o crédito25;sem dupla utilização.',
|
||
);
|
||
continue;
|
||
}
|
||
fields['27'] = amount;
|
||
fields['26'] = subtractFractions(fields['25'], amount);
|
||
r.source +=
|
||
' D:afectação expressa26+27=25,sem concessão/recebimento;CIVA21 eModeloA11.';
|
||
} else {
|
||
stop(
|
||
'Escolha reporte integral ou parcela para preparação de reembolsoD;sem destino presumido.',
|
||
);
|
||
continue;
|
||
}
|
||
if (refundLockConflict(d, row, fields)) {
|
||
stop(
|
||
'Crédito ligado a pedido apresentado/decisão/recebimento;conserve a afectação e reconcilie o procedimento,sem o voltar a reportar ou alterar silenciosamente.',
|
||
);
|
||
continue;
|
||
}
|
||
if (effectiveCarryAlreadyUsed(d, row, fields)) {
|
||
stop(
|
||
'Reporte efectivo já utilizado numa declaração posterior recuperada;prepare o pedido sobre o saldo disponível do período reconciliado,sem reafectar retroactivamente crédito consumido.',
|
||
);
|
||
continue;
|
||
}
|
||
if (!fields['24']) {
|
||
stop(
|
||
'Juros23 por identificar:indique montante documentado/pressuposto ou inexistência expressa com motivo;sem taxa presumida.',
|
||
);
|
||
continue;
|
||
}
|
||
r.status = 'determined';
|
||
r.carryKey = closureFingerprint([
|
||
r.id,
|
||
r.financialKey,
|
||
fields['26'],
|
||
scope(d),
|
||
]);
|
||
const payments: FiscalFraction[] = [];
|
||
for (const p of row.payments) {
|
||
if (
|
||
!p.reference.trim() ||
|
||
!validDate(p.date) ||
|
||
(puses.get(paymentIdentity(row, p)) ?? 0) > 1 ||
|
||
substitutionPayments(d).includes(paymentIdentity(row, p))
|
||
) {
|
||
invalid =
|
||
'Pagamento sem referência/data ou repetido noutro período/declaração;conserve o documento e reconcilie a imputação.';
|
||
break;
|
||
}
|
||
try {
|
||
payments.push(decimalFraction(p.amount));
|
||
} catch {
|
||
invalid =
|
||
'Montante de pagamento por identificar;sem baixa presumida.';
|
||
break;
|
||
}
|
||
}
|
||
if (invalid) {
|
||
stop(invalid);
|
||
continue;
|
||
}
|
||
r.paid = addFractions(payments);
|
||
r.outstanding = positiveFraction(
|
||
subtractFractions(fields['24'], r.paid),
|
||
);
|
||
r.overpayment = positiveFraction(
|
||
subtractFractions(r.paid, fields['24']),
|
||
);
|
||
r.status = 'determined';
|
||
r.carryKey = closureFingerprint([
|
||
r.id,
|
||
r.financialKey,
|
||
fields['26'],
|
||
scope(d),
|
||
]);
|
||
r.reason =
|
||
'Saldo calculado, pagamentos registados e crédito transportado distintos. Pagamento não altera22/25/26;excedente pago não cria crédito20 sem comunicação.21 não aplicável à primeira declaração;' +
|
||
(row.destination === 'refund'
|
||
? '26/27 separados pela afectação;27 é parcela destinada a pedidoD,sem concessão nem recebimento. '
|
||
: '27zero pela opção expressa de reporte integral. ') +
|
||
(row.origin === 'projected'
|
||
? 'Pressupostos de simulação,sem declaração ou pagamento efectuados pelo portal.'
|
||
: 'Dados efectivos declarados;registo não certifica cobrança nem submissãoAT.');
|
||
results.push(r);
|
||
}
|
||
const result = {
|
||
calculatedAt: now,
|
||
key: bookKey(d, b),
|
||
inputs: structuredClone(b.rows),
|
||
rows: results,
|
||
};
|
||
return {
|
||
...b,
|
||
...(b.result
|
||
? { history: [...(b.history ?? []), structuredClone(b.result)] }
|
||
: {}),
|
||
result,
|
||
};
|
||
}
|
||
export function closureReportRows(
|
||
d: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const b = closureBook(d);
|
||
if (!b.rows.length) return [];
|
||
const hasRefund = b.result?.rows.some(
|
||
(r) => r.fields['27'] && BigInt(r.fields['27'].numerator) > 0n,
|
||
);
|
||
const description = hasRefund
|
||
? closureSource.replace(
|
||
'Sem substituição21,reembolso27 ou restituição13(2).',
|
||
'ExtensãoD:26+27=25;27é parcela destinada a pedido,não decisão/recebimento. Substituição21 e restituição13(2) mantêm percursos próprios.',
|
||
)
|
||
: closureSource;
|
||
const out: Array<[string, string, string, string]> = [
|
||
[
|
||
'IVA mensal',
|
||
'L3-A/B — fecho por período',
|
||
closureCurrent(d, b)
|
||
? 'Resultado correspondente às entradas'
|
||
: 'Histórico preservado;rever entradas/origens antes de utilizar',
|
||
description,
|
||
],
|
||
];
|
||
for (const r of b.result?.rows ?? []) {
|
||
const input = b.result?.inputs.find((x) => x.id === r.id),
|
||
source = input?.source;
|
||
const context = `${r.entityRef};${scope(d)};MZN;${r.origin === 'projected' ? 'Pressuposto de simulação' : 'Dados efectivos declarados'};versão guardada${b.result!.calculatedAt}`;
|
||
out.push([
|
||
'IVA mensal',
|
||
`Fecho A/B — ${r.period}`,
|
||
Object.entries(r.fields)
|
||
.map(
|
||
([f, v]) =>
|
||
`${f}:${v ? fractionPresentation(v).decimal : f === '21' && input?.firstDeclaration === 'yes' ? 'Não aplicável (primeira declaração)' : 'Indeterminado'}`,
|
||
)
|
||
.join('; '),
|
||
`${context};${r.reason}`,
|
||
]);
|
||
const credits =
|
||
(input?.credits ?? [])
|
||
.map(
|
||
(c) =>
|
||
`${c.kind === 'communication' ? 'Comunicação20' : c.kind === 'previous' ? 'Transporte19' : 'Abertura19'}: ${c.reference};origem${c.period};montante apresentado${c.amount}MZN${c.date ? ';recebida' + c.date : ''}`,
|
||
)
|
||
.join(' | ') || 'Sem créditos registados';
|
||
const payments =
|
||
(input?.payments ?? [])
|
||
.map((p) => `${p.reference};${p.date};${p.amount}MZN`)
|
||
.join(' | ') || 'Sem pagamentos registados';
|
||
out.push([
|
||
'IVA mensal',
|
||
`Pagamentos e reporte — ${r.period}`,
|
||
`Calculado24:${r.fields['24'] ? fractionLabel(r.fields['24']) : 'Indeterminado'};pagamentos registados:${r.paid ? fractionLabel(r.paid) : 'Indeterminado'};por liquidar:${r.outstanding ? fractionLabel(r.outstanding) : 'Indeterminado'};excedente pago:${r.overpayment ? fractionLabel(r.overpayment) : 'Indeterminado'};transportado26:${r.fields['26'] ? fractionLabel(r.fields['26']) : 'Indeterminado'}`,
|
||
`${context};OrigemL2:revisão${source?.revision ?? 'por identificar'},${source?.period ?? ''},${source?.location ?? ''},${source?.calculatedAt ?? ''};${credits};${payments};entrega${input?.submissionDate ?? ''},${input?.filingRef ?? ''};juros:${input?.interestRef ?? ''}`,
|
||
]);
|
||
}
|
||
return out;
|
||
} catch {
|
||
return [
|
||
[
|
||
'IVA mensal',
|
||
'L3-A/B',
|
||
'Fecho guardado não recuperável',
|
||
'Originais preservados;sem crédito ou pagamento presumidos.',
|
||
],
|
||
];
|
||
}
|
||
}
|