360 lines
14 KiB
JavaScript
360 lines
14 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
recordSales,
|
|
saveSalesBook,
|
|
} from '../../resources/js/lib/iva-operations.ts';
|
|
import {
|
|
recordAcquisitions,
|
|
saveAcquisitionBook,
|
|
acquisitionBook,
|
|
} from '../../resources/js/lib/iva-acquisitions.ts';
|
|
import {
|
|
closureBook,
|
|
closureCurrent,
|
|
closureSources,
|
|
closureReportRows,
|
|
recordClosure,
|
|
} from '../../resources/js/lib/iva-closure.ts';
|
|
import {
|
|
newSubstitution,
|
|
previousDeclarations,
|
|
substitutionBook,
|
|
recordSubstitution,
|
|
saveSubstitutionBook,
|
|
substitutionCurrent,
|
|
substitutionReportRows,
|
|
} from '../../resources/js/lib/iva-substitution.ts';
|
|
const frozen = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/iva-l3c-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const old = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/iva-l3ab-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
function fixture() {
|
|
const d = structuredClone(frozen),
|
|
base = JSON.parse(old.modelo_a_snapshot),
|
|
snap = JSON.parse(d.modelo_a_snapshot);
|
|
for (const [k, v] of Object.entries(base))
|
|
if (k !== 'version') snap[k] = { ...v, ...snap[k] };
|
|
d.modelo_a_snapshot = JSON.stringify(snap);
|
|
const sales = snap.salesOperations['opportunity:373'];
|
|
sales.rows[2].amount = '1375';
|
|
d.modelo_a_snapshot = saveSalesBook(d, recordSales(d, sales));
|
|
d.modelo_a_snapshot = saveAcquisitionBook(
|
|
d,
|
|
recordAcquisitions(d, acquisitionBook(d), '2026-10-25T12:30:00.000Z'),
|
|
);
|
|
const prior = previousDeclarations(d).find((p) => p.period === '2026-03'),
|
|
source = closureSources(d)
|
|
.filter((s) => s.period === '2026-03')
|
|
.at(-1);
|
|
const row = {
|
|
...newSubstitution('sub-Mar'),
|
|
prior,
|
|
source,
|
|
priorReference: 'SYNTHETIC QA declaraçãoMarço',
|
|
priorDate: '2026-04-15',
|
|
reference: 'SYNTHETIC QA substituiçãoMarço',
|
|
date: '2026-05-10',
|
|
reason: 'SYNTHETIC QA operação de Março corrigida',
|
|
interest: 'documented',
|
|
interestAmount: '1',
|
|
interestRef: 'SYNTHETIC QA juros adicionais identificados',
|
|
payments: [
|
|
{
|
|
id: 'payment2',
|
|
reference: 'SYNTHETIC QA reciboP2',
|
|
date: '2026-05-11',
|
|
amount: '20',
|
|
},
|
|
],
|
|
};
|
|
return { d, b: { version: 1, rows: [row] } };
|
|
}
|
|
const value = (r, f) =>
|
|
r.fields[f] && `${r.fields[f].numerator}/${r.fields[f].denominator}`;
|
|
function alternative(d, debit, credit) {
|
|
const s = JSON.parse(d.modelo_a_snapshot),
|
|
calc = structuredClone(s.books['opportunity:373'].result);
|
|
calc.revision += 10;
|
|
calc.calculatedAt += '-unit';
|
|
const p = calc.periods.find((p) => p.period === '2026-03');
|
|
p.fields['17'] = String(debit);
|
|
p.fields['18'] = String(credit);
|
|
p.exactFields['17'] = { numerator: String(debit), denominator: '1' };
|
|
p.exactFields['18'] = { numerator: String(credit), denominator: '1' };
|
|
s.books['opportunity:373'].history.push(calc);
|
|
d.modelo_a_snapshot = JSON.stringify(s);
|
|
return closureSources(d).find(
|
|
(p) => p.revision === calc.revision && p.period === '2026-03',
|
|
);
|
|
}
|
|
void test('real corrected L2 version gives additional60 plus1;original72/30/42 and credits remain unchanged', () => {
|
|
const { d, b } = fixture(),
|
|
before = closureBook(d),
|
|
r = recordSubstitution(d, b).result.rows[0];
|
|
assert.equal(r.status, 'determined', r.reason);
|
|
assert.equal(value(r, '21'), '60/1');
|
|
assert.equal(value(r, '22'), '60/1');
|
|
assert.equal(value(r, '24'), '61/1');
|
|
assert.deepEqual(r.paid, { numerator: '20', denominator: '1' });
|
|
assert.deepEqual(r.outstanding, { numerator: '41', denominator: '1' });
|
|
assert.deepEqual(r.totalOutstanding, { numerator: '83', denominator: '1' });
|
|
assert.deepEqual(r.totalPaid, { numerator: '50', denominator: '1' });
|
|
for (const f of ['19', '20', '25', '26', '27'])
|
|
assert.equal(r.fields[f], null);
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, recordSubstitution(d, b));
|
|
assert.deepEqual(closureBook(d), before);
|
|
assert.ok(closureCurrent(d, before));
|
|
assert.match(
|
|
closureReportRows(d)
|
|
.map((r) => r.join(' '))
|
|
.join(' '),
|
|
/Calculado24:72\/1;pagamentos registados:30\/1;por liquidar:42\/1/,
|
|
);
|
|
});
|
|
void test('quadro9 four sign cases use exact difference;credit waits for communication and never offsets original debt', () => {
|
|
for (const [priorKind, actual17, actual18, expected] of [
|
|
['debit', 220, 0, '60/1'],
|
|
['credit', 20, 0, '120/1'],
|
|
['credit', 0, 80, '20/1'],
|
|
['debit', 0, 40, '-200/1'],
|
|
]) {
|
|
const { d, b } = fixture();
|
|
if (priorKind === 'credit') {
|
|
const jan = previousDeclarations(d).find(
|
|
(p) => p.period === '2026-01',
|
|
);
|
|
b.rows[0].prior = { ...jan };
|
|
b.rows[0].prior.period = '2026-03'; // use an actual frozen credit-origin version in March below
|
|
const snap = JSON.parse(d.modelo_a_snapshot),
|
|
ab = snap.ivaClosures['opportunity:373'],
|
|
calc = structuredClone(ab.result),
|
|
r = calc.rows[2];
|
|
r.fields['17'] = { numerator: '0', denominator: '1' };
|
|
r.fields['18'] = { numerator: '100', denominator: '1' };
|
|
r.id = 'unit-credit-Mar';
|
|
calc.inputs[2].id = r.id;
|
|
ab.history.push(calc);
|
|
d.modelo_a_snapshot = JSON.stringify(snap);
|
|
b.rows[0].prior = previousDeclarations(d).find(
|
|
(p) => p.id === 'unit-credit-Mar',
|
|
);
|
|
}
|
|
b.rows[0].source = alternative(d, actual17, actual18);
|
|
const r = recordSubstitution(d, b).result.rows[0];
|
|
assert.equal(r.status, 'determined', r.reason);
|
|
assert.equal(value(r, '21'), expected);
|
|
if (expected.startsWith('-')) {
|
|
assert.equal(value(r, '22'), '0/1');
|
|
assert.deepEqual(r.pendingCredit, {
|
|
numerator: '200',
|
|
denominator: '1',
|
|
});
|
|
assert.equal(value(r, '26'), null);
|
|
assert.equal(r.totalOutstanding.numerator, '42');
|
|
}
|
|
}
|
|
});
|
|
void test('missing facts,wrong period,currency,unknown source and interest never produce presumed zero', () => {
|
|
const { d, b } = fixture();
|
|
for (const patch of [
|
|
{ prior: null },
|
|
{ source: null },
|
|
{ priorReference: '' },
|
|
{ priorDate: '' },
|
|
{ date: '2026-04-01' },
|
|
{ reference: '' },
|
|
{ reason: '' },
|
|
{ interest: '' },
|
|
]) {
|
|
const bad = structuredClone(b);
|
|
Object.assign(bad.rows[0], patch);
|
|
assert.equal(
|
|
recordSubstitution(d, bad).result.rows[0].status,
|
|
'indeterminate',
|
|
JSON.stringify(patch),
|
|
);
|
|
}
|
|
const bad = structuredClone(b);
|
|
bad.rows[0].source = closureSources(d).find((s) => s.period === '2026-02');
|
|
assert.equal(recordSubstitution(d, bad).result.rows[0].fields['21'], null);
|
|
assert.equal(
|
|
recordSubstitution({ ...d, currency: 'USD' }, b).result.rows[0].status,
|
|
'indeterminate',
|
|
);
|
|
});
|
|
void test('old payment cannot be re-used and two substitutions cannot repeat one predecessor or payment', () => {
|
|
const { d, b } = fixture();
|
|
b.rows[0].payments[0].reference = 'SYNTHETIC QA reciboP1';
|
|
assert.match(
|
|
recordSubstitution(d, b).result.rows[0].reason,
|
|
/Pagamento já/,
|
|
);
|
|
const f = fixture();
|
|
f.b.rows.push({ ...structuredClone(f.b.rows[0]), id: 'copy' });
|
|
assert.ok(
|
|
recordSubstitution(f.d, f.b).result.rows.every(
|
|
(r) => r.status === 'indeterminate',
|
|
),
|
|
);
|
|
const g = fixture();
|
|
g.b.rows[0].payments.push({ ...g.b.rows[0].payments[0], id: 'copy' });
|
|
assert.equal(recordSubstitution(g.d, g.b).result.rows[0].paid, null);
|
|
});
|
|
void test('successive substitution links frozen predecessor,charges only new difference and preserves all payments', () => {
|
|
const { d, b } = fixture();
|
|
let book = recordSubstitution(d, b, '2026-10-25T12:31:00Z');
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
const next = structuredClone(book.rows[0]);
|
|
next.id = 'sub-Mar-2';
|
|
next.prior = previousDeclarations(d).find((p) => p.kind === 'C');
|
|
next.source = alternative(d, 250, 0);
|
|
next.priorReference = next.reference;
|
|
next.priorDate = next.date;
|
|
next.reference = 'SYNTHETIC QA segunda substituição';
|
|
next.date = '2026-06-10';
|
|
next.interest = 'none';
|
|
next.interestRef = 'SYNTHETIC QA sem juros adicionais';
|
|
next.payments = [];
|
|
book = recordSubstitution(d, { ...book, rows: [...book.rows, next] });
|
|
const r = book.result.rows[1];
|
|
assert.equal(r.status, 'determined', r.reason);
|
|
assert.equal(value(r, '21'), '30/1');
|
|
assert.deepEqual(r.totalOutstanding, {
|
|
numerator: '113',
|
|
denominator: '1',
|
|
});
|
|
assert.deepEqual(r.totalPaid, { numerator: '50', denominator: '1' });
|
|
assert.equal(book.history.length, 1);
|
|
next.payments = [
|
|
{
|
|
id: 'reuse',
|
|
reference: 'SYNTHETIC QA reciboP2',
|
|
date: '2026-06-11',
|
|
amount: '20',
|
|
},
|
|
];
|
|
assert.equal(
|
|
recordSubstitution(d, { ...book, rows: [book.rows[0], next] }).result
|
|
.rows[1].paid,
|
|
null,
|
|
);
|
|
});
|
|
void test('save/recover retains every L1/L2/AB map;report uses frozen inputs and recalculation appends history', () => {
|
|
const { d, b } = fixture(),
|
|
before = JSON.parse(d.modelo_a_snapshot);
|
|
let book = recordSubstitution(d, b);
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
assert.ok(substitutionCurrent(d, substitutionBook(d)));
|
|
const stored = JSON.parse(d.modelo_a_snapshot);
|
|
for (const [k, v] of Object.entries(before))
|
|
assert.deepEqual(stored[k], v, k);
|
|
book.rows[0].reference = 'new deliberate edit';
|
|
assert.equal(substitutionCurrent(d, book), false);
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
const report = substitutionReportRows(d)
|
|
.map((r) => r.join(' '))
|
|
.join(' ');
|
|
assert.match(report, /SYNTHETIC QA substituiçãoMarço/);
|
|
assert.doesNotMatch(report, /new deliberate edit/);
|
|
const oldResult = structuredClone(book.result);
|
|
book = recordSubstitution(d, book);
|
|
assert.deepEqual(book.history.at(-1), oldResult);
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
assert.ok(substitutionCurrent(d, book));
|
|
assert.match(
|
|
substitutionReportRows(d)
|
|
.map((r) => r.join(' '))
|
|
.join(' '),
|
|
/new deliberate edit/,
|
|
);
|
|
});
|
|
void test('fractional delta remains exact and overpayment does not generate communicated credit', () => {
|
|
const { d, b } = fixture();
|
|
const s = alternative(d, 220, 0),
|
|
snap = JSON.parse(d.modelo_a_snapshot),
|
|
calc = snap.books['opportunity:373'].history.at(-1);
|
|
calc.periods[2].exactFields['17'] = { numerator: '661', denominator: '3' };
|
|
d.modelo_a_snapshot = JSON.stringify(snap);
|
|
b.rows[0].source = closureSources(d).find(
|
|
(x) => x.revision === s.revision && x.period === '2026-03',
|
|
);
|
|
b.rows[0].payments[0].amount = '100';
|
|
const r = recordSubstitution(d, b).result.rows[0];
|
|
assert.equal(value(r, '21'), '181/3');
|
|
assert.deepEqual(r.overpayment, { numerator: '116', denominator: '3' });
|
|
assert.equal(r.pendingCredit.numerator, '0');
|
|
assert.equal(r.fields['20'], null);
|
|
});
|
|
void test('a payment already frozen in C cannot be added again in A/B;original valid A/B remains current', () => {
|
|
const { d, b } = fixture();
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, recordSubstitution(d, b));
|
|
const ab = closureBook(d);
|
|
assert.ok(closureCurrent(d, ab));
|
|
ab.rows[2].payments.push({ ...b.rows[0].payments[0] });
|
|
assert.equal(closureCurrent(d, ab), false);
|
|
const r = recordClosure(d, ab).result.rows[2];
|
|
assert.equal(r.paid, null);
|
|
assert.match(r.reason, /Pagamento/);
|
|
});
|
|
void test('a newly conflicting payment in recovered AB makes C stale and requires reconciliation', () => {
|
|
const { d, b } = fixture();
|
|
const book = recordSubstitution(d, b);
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
assert.ok(substitutionCurrent(d, book));
|
|
const s = JSON.parse(d.modelo_a_snapshot);
|
|
s.ivaClosures['opportunity:373'].rows[2].payments.push({
|
|
...b.rows[0].payments[0],
|
|
});
|
|
d.modelo_a_snapshot = JSON.stringify(s);
|
|
assert.equal(substitutionCurrent(d, book), false);
|
|
assert.equal(recordSubstitution(d, book).result.rows[0].paid, null);
|
|
});
|
|
void test('a substitution cannot link back through a circular predecessor chain', () => {
|
|
const { d, b } = fixture();
|
|
let book = recordSubstitution(d, b, '2026-10-25T12:31:00Z');
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
const child = {
|
|
...structuredClone(book.rows[0]),
|
|
id: 'child',
|
|
prior: previousDeclarations(d).find((p) => p.kind === 'C'),
|
|
priorReference: book.rows[0].reference,
|
|
priorDate: book.rows[0].date,
|
|
date: '2026-06-10',
|
|
reference: 'child-ref',
|
|
payments: [],
|
|
};
|
|
book = recordSubstitution(
|
|
d,
|
|
{ ...book, rows: [...book.rows, child] },
|
|
'2026-10-25T12:32:00Z',
|
|
);
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, book);
|
|
book.rows[0].prior = previousDeclarations(d).find(
|
|
(p) => p.kind === 'C' && p.id === 'child',
|
|
);
|
|
assert.ok(
|
|
recordSubstitution(d, book).result.rows.every((r) =>
|
|
/circular/.test(r.reason),
|
|
),
|
|
);
|
|
});
|
|
void test('export synthetic demonstrated correction for guarded private API and actual component proof', () => {
|
|
const { d, b } = fixture();
|
|
const result = recordSubstitution(d, b, '2026-10-25T12:31:00.000Z');
|
|
const again = recordSubstitution(d, result, '2026-10-25T12:32:00.000Z');
|
|
d.modelo_a_snapshot = saveSubstitutionBook(d, again);
|
|
assert.ok(substitutionCurrent(d, again));
|
|
if (process.env.IVA_L3C_CALCULATED)
|
|
fs.writeFileSync(process.env.IVA_L3C_CALCULATED, JSON.stringify(d));
|
|
});
|