449 lines
16 KiB
JavaScript
449 lines
16 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { fixture } from './fixtures/iva-beverage-fixture.mjs';
|
|
import {
|
|
recordBeverageRestitution,
|
|
saveBeverageRestitutionBook,
|
|
beverageRestitutionBook,
|
|
beverageRestitutionSources,
|
|
beverageRestitutionCurrent,
|
|
beverageRestitutionReportRows,
|
|
} from '../../resources/js/lib/iva-beverage-restitution.ts';
|
|
import { beverageDeductionEvidence } from '../../resources/js/lib/iva-beverage-allocation.ts';
|
|
import {
|
|
acquisitionBook,
|
|
recordAcquisitions,
|
|
saveAcquisitionBook,
|
|
} from '../../resources/js/lib/iva-acquisitions.ts';
|
|
import { newAcquisitionAllocation } from '../../resources/js/lib/iva-acquisition-allocation.ts';
|
|
import { fiscalCalculationGaps } from '../../resources/js/lib/iva-calculation-gaps.ts';
|
|
import {
|
|
l3FormPreparations,
|
|
l3FormPreparationHtml,
|
|
l3FormalitiesDependencies,
|
|
} from '../../resources/js/lib/l3-form-preparation.ts';
|
|
const n = (v) => v && `${v.numerator}/${v.denominator}`;
|
|
const allocation = (from, to, patch = {}) => ({
|
|
from,
|
|
to,
|
|
reference: 'SYNTHETIC QA repartição documento',
|
|
deductionFrom: '',
|
|
deductionTo: '',
|
|
deductionReference: '',
|
|
...patch,
|
|
});
|
|
function pending(row, id, from, to) {
|
|
return {
|
|
...structuredClone(row),
|
|
id,
|
|
decision: 'pending',
|
|
decisionReference: '',
|
|
decisionDate: '',
|
|
decisionIssuer: '',
|
|
approvedAmount: '',
|
|
receipts: [],
|
|
allocation: allocation(from, to),
|
|
};
|
|
}
|
|
function partialPrior(numerator = '3', denominator = '8', expected = '6/1') {
|
|
const f = fixture(),
|
|
a = acquisitionBook(f.d);
|
|
a.rows[0].origin = 'effective';
|
|
a.rows[0].allocation = 'mixed';
|
|
a.rows[0].bc.beveragePath = 'deduction';
|
|
a.rows[0].bc.facts.onlyDeduction = 'yes';
|
|
a.rows[0].d = {
|
|
...newAcquisitionAllocation('2026-12'),
|
|
method: 'actual',
|
|
numerator,
|
|
denominator,
|
|
unit: 'unidades físicas documentadas',
|
|
scopeRef: 'SYNTHETIC QA parte com direito',
|
|
source: 'SYNTHETIC QA critério e registos',
|
|
communicationRef: 'SYNTHETIC QA comunicação22(2)',
|
|
noExcludedPayments: 'yes',
|
|
facts: {
|
|
scopeConfirmed: 'yes',
|
|
rightBasis: 'yes',
|
|
actualScope: 'yes',
|
|
communication: 'yes',
|
|
conditionsMet: 'yes',
|
|
},
|
|
};
|
|
let actual = recordAcquisitions(f.d, a);
|
|
assert.equal(
|
|
n(actual.result.rows[0].deductibleExact),
|
|
expected,
|
|
actual.result.rows[0].reason,
|
|
);
|
|
f.d.modelo_a_snapshot = saveAcquisitionBook(f.d, actual);
|
|
actual.rows[0].bc.beveragePath = 'refund';
|
|
actual.rows[0].allocation = 'taxable_only';
|
|
delete actual.rows[0].d;
|
|
actual = recordAcquisitions(f.d, actual);
|
|
f.d.modelo_a_snapshot = saveAcquisitionBook(f.d, actual);
|
|
f.b.rows[0].source = beverageRestitutionSources(f.d)[0];
|
|
return f;
|
|
}
|
|
void test('disjoint portions6+10 share one documentary VAT16 without overlap;save/recovery/report preserve prior maps', () => {
|
|
const { d, b } = fixture(),
|
|
before = JSON.parse(d.modelo_a_snapshot);
|
|
b.rows = [
|
|
pending(b.rows[0], 'portion6', '0', '6'),
|
|
pending(b.rows[0], 'portion10', '6', '16'),
|
|
];
|
|
const book = recordBeverageRestitution(d, b);
|
|
assert.deepEqual(
|
|
book.result.rows.map((r) => [r.status, n(r.requested)]),
|
|
[
|
|
['determined', '6/1'],
|
|
['determined', '10/1'],
|
|
],
|
|
);
|
|
d.modelo_a_snapshot = saveBeverageRestitutionBook(d, book);
|
|
assert.deepEqual(beverageRestitutionBook(d), book);
|
|
for (const [k, v] of Object.entries(before))
|
|
assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k], v, k);
|
|
assert.ok(beverageRestitutionCurrent(d, book));
|
|
assert.match(
|
|
beverageRestitutionReportRows(d).flat().join(' '),
|
|
/IVA documental:16.00;parcelaIVA:6.00/,
|
|
);
|
|
});
|
|
void test('overlap,negative/zero/outside bounds,missing support and duplicate legacy whole line are rejected', () => {
|
|
const { d, b } = fixture();
|
|
for (const pair of [
|
|
['0', '0'],
|
|
['-1', '6'],
|
|
['6', '5'],
|
|
['0', '16.000001'],
|
|
]) {
|
|
const book = { ...b, rows: [pending(b.rows[0], 'bad', ...pair)] };
|
|
assert.equal(
|
|
recordBeverageRestitution(d, book).result.rows[0].requested,
|
|
null,
|
|
);
|
|
}
|
|
let book = {
|
|
...b,
|
|
rows: [
|
|
pending(b.rows[0], 'p1', '0', '10'),
|
|
pending(b.rows[0], 'p2', '9', '16'),
|
|
],
|
|
};
|
|
assert.ok(
|
|
recordBeverageRestitution(d, book).result.rows.every(
|
|
(r) => !r.requested,
|
|
),
|
|
);
|
|
book.rows = [
|
|
pending(b.rows[0], 'p1', '0', '6'),
|
|
structuredClone(b.rows[0]),
|
|
];
|
|
assert.ok(
|
|
recordBeverageRestitution(d, book).result.rows.every(
|
|
(r) => !r.requested,
|
|
),
|
|
);
|
|
book.rows = [pending(b.rows[0], 'p1', '0', '6')];
|
|
book.rows[0].allocation.reference = '';
|
|
assert.equal(
|
|
recordBeverageRestitution(d, book).result.rows[0].requested,
|
|
null,
|
|
);
|
|
});
|
|
void test('effective deduction6 must be explicitly reconciled to its own interval;E10 uses only the disjoint remainder', () => {
|
|
const { d, b } = partialPrior();
|
|
assert.equal(
|
|
n(beverageDeductionEvidence(d, b.rows[0].source).amount),
|
|
'6/1',
|
|
);
|
|
b.rows = [pending(b.rows[0], 'remainder', '6', '16')];
|
|
assert.equal(
|
|
recordBeverageRestitution(d, b).result.rows[0].requested,
|
|
null,
|
|
);
|
|
Object.assign(b.rows[0].allocation, {
|
|
deductionFrom: '0',
|
|
deductionTo: '6',
|
|
deductionReference: 'SYNTHETIC QA correspondência da dedução original',
|
|
});
|
|
let book = recordBeverageRestitution(d, b);
|
|
assert.equal(n(book.result.rows[0].requested), '10/1');
|
|
assert.equal(book.result.rows[0].status, 'determined');
|
|
assert.ok(
|
|
acquisitionBook(d).history.some((c) =>
|
|
c.rows.some((r) => n(r.deductibleExact) === '6/1'),
|
|
),
|
|
);
|
|
for (const patch of [
|
|
{ deductionTo: '5' },
|
|
{ deductionFrom: '4', deductionTo: '10' },
|
|
{ deductionReference: '' },
|
|
]) {
|
|
const changed = structuredClone(b);
|
|
Object.assign(changed.rows[0].allocation, patch);
|
|
assert.equal(
|
|
recordBeverageRestitution(d, changed).result.rows[0].requested,
|
|
null,
|
|
);
|
|
}
|
|
});
|
|
void test('a rounded tiny deduction,divergent historical amounts and editing away a committed portion never releaseVAT', () => {
|
|
const { d, b } = partialPrior(),
|
|
a = acquisitionBook(d),
|
|
old = structuredClone(
|
|
a.history.find((c) =>
|
|
c.rows.some((r) => n(r.deductibleExact) === '6/1'),
|
|
),
|
|
);
|
|
old.rows.find((r) => r.id === 'beverage-purchase').deductibleExact = {
|
|
numerator: '1',
|
|
denominator: '100000000',
|
|
};
|
|
old.rows.find((r) => r.id === 'beverage-purchase').deductible = '0.00';
|
|
a.history.push(old);
|
|
d.modelo_a_snapshot = saveAcquisitionBook(d, a);
|
|
b.rows = [pending(b.rows[0], 'x', '0', '16')];
|
|
assert.equal(
|
|
recordBeverageRestitution(d, b).result.rows[0].requested,
|
|
null,
|
|
);
|
|
assert.match(
|
|
beverageDeductionEvidence(d, b.rows[0].source).reason,
|
|
/divergentes/,
|
|
);
|
|
const f = fixture();
|
|
f.b.rows = [pending(f.b.rows[0], 'committed', '0', '6')];
|
|
let book = recordBeverageRestitution(f.d, f.b);
|
|
f.d.modelo_a_snapshot = saveBeverageRestitutionBook(f.d, book);
|
|
book = recordBeverageRestitution(f.d, { ...book, rows: [] });
|
|
f.d.modelo_a_snapshot = saveBeverageRestitutionBook(f.d, book);
|
|
const overlap = pending(f.b.rows[0], 'new', '5', '16');
|
|
assert.equal(
|
|
recordBeverageRestitution(f.d, { ...book, rows: [overlap] }).result
|
|
.rows[0].requested,
|
|
null,
|
|
);
|
|
const edited = pending(f.b.rows[0], 'committed', '0', '7');
|
|
assert.equal(
|
|
recordBeverageRestitution(f.d, { ...book, rows: [edited] }).result
|
|
.rows[0].requested,
|
|
null,
|
|
);
|
|
});
|
|
void test('gap inventory identifies supplyVAT independently from purchase16 and includes implementation limits of margin/travel/events', () => {
|
|
const { d } = fixture(),
|
|
gaps = fiscalCalculationGaps(d);
|
|
const supply = gaps.find(
|
|
(g) =>
|
|
g.book === 'salesOperations' && g.reason.includes('Bebidas em13'),
|
|
);
|
|
assert.ok(supply);
|
|
assert.match(supply.resolution, /IVA da aquisição separado/);
|
|
const store = JSON.parse(d.modelo_a_snapshot);
|
|
store.regulatedSecondhands = {
|
|
...store.regulatedSecondhands,
|
|
[d.context_opportunity]: {
|
|
rows: [{ id: 'm', period: '2026-12' }],
|
|
result: {
|
|
inputs: [{ id: 'm', period: '2026-12' }],
|
|
rows: [
|
|
{
|
|
id: 'm',
|
|
status: 'determined',
|
|
baseAmount: '100',
|
|
vat: null,
|
|
reason: 'Base da margem calculada;IVA não calculado.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
d.modelo_a_snapshot = JSON.stringify(store);
|
|
const margin = fiscalCalculationGaps(d).find(
|
|
(g) => g.book === 'regulatedSecondhands' && g.kind === 'implementation',
|
|
);
|
|
assert.ok(margin);
|
|
assert.match(margin.resolution, /liquidação/);
|
|
assert.ok(margin.required.length);
|
|
assert.ok(
|
|
fiscalCalculationGaps({ ...d, context_opportunity: 'other-context' })
|
|
.length === 0,
|
|
);
|
|
});
|
|
void test('formal preparation maps only identified L3 fields;E keeps16/0/12/7/5 outside27;HTML escapes documents and lists specific dependencies', () => {
|
|
const { d, b } = fixture();
|
|
b.rows[0].entityRef = '<script>alert(1)</script>';
|
|
d.modelo_a_snapshot = saveBeverageRestitutionBook(
|
|
d,
|
|
recordBeverageRestitution(d, b),
|
|
);
|
|
const forms = l3FormPreparations(d),
|
|
e = forms.find((p) => p.branch === 'E');
|
|
assert.deepEqual(
|
|
e.fields.map((f) => f.value),
|
|
['16.00', '16.00', '0.00', '12.00', '7.00', '5.00'],
|
|
);
|
|
assert.ok(e.fields.every((f) => f.field === 'Dossier'));
|
|
const html = l3FormPreparationHtml(e);
|
|
assert.ok(!html.includes('<script>'));
|
|
assert.ok(html.includes('<script>'));
|
|
assert.match(html, /modelo oficial específico ainda não identificado/);
|
|
assert.match(html, /Condição de retoma/);
|
|
assert.equal(l3FormalitiesDependencies.length, 4);
|
|
assert.ok(
|
|
forms
|
|
.filter((p) => p.branch === 'A/B')
|
|
.every((p) =>
|
|
p.fields.every((f) => !['05', '06'].includes(f.field)),
|
|
),
|
|
);
|
|
});
|
|
void test('export reconciled6deducted/10requested plus gap inventory and preparations for isolated persistence and realUI', async () => {
|
|
const { d, b } = partialPrior();
|
|
b.rows = [pending(b.rows[0], 'remainder', '6', '16')];
|
|
Object.assign(b.rows[0].allocation, {
|
|
deductionFrom: '0',
|
|
deductionTo: '6',
|
|
deductionReference: 'SYNTHETIC QA correspondência da dedução original',
|
|
});
|
|
let book = recordBeverageRestitution(d, b);
|
|
book = recordBeverageRestitution(d, book);
|
|
d.modelo_a_snapshot = saveBeverageRestitutionBook(d, book);
|
|
assert.equal(n(book.result.rows[0].requested), '10/1');
|
|
const fs = await import('node:fs');
|
|
if (process.env.IVA_E_PARTITION_OUTPUT) {
|
|
fs.writeFileSync(process.env.IVA_E_PARTITION_OUTPUT, JSON.stringify(d));
|
|
fs.writeFileSync(
|
|
process.env.IVA_E_PARTITION_OUTPUT.replace(
|
|
'calculated.json',
|
|
'gaps.json',
|
|
),
|
|
JSON.stringify(fiscalCalculationGaps(d), null, 2),
|
|
);
|
|
fs.writeFileSync(
|
|
process.env.IVA_E_PARTITION_OUTPUT.replace(
|
|
'calculated.json',
|
|
'preparations.json',
|
|
),
|
|
JSON.stringify(l3FormPreparations(d), null, 2),
|
|
);
|
|
}
|
|
});
|
|
void test('a failed recalculation does not commit its rejected boundaries;correcting support or decimal presentation keeps the accepted reservation', () => {
|
|
const { d, b } = fixture();
|
|
b.rows = [pending(b.rows[0], 'stable', '0', '6')];
|
|
let book = recordBeverageRestitution(d, b);
|
|
d.modelo_a_snapshot = saveBeverageRestitutionBook(d, book);
|
|
const bad = structuredClone(book);
|
|
bad.rows[0].allocation.to = '17';
|
|
book = recordBeverageRestitution(d, bad);
|
|
assert.equal(book.result.rows[0].requested, null);
|
|
d.modelo_a_snapshot = saveBeverageRestitutionBook(d, book);
|
|
book.rows[0].allocation.to = '6.0';
|
|
book.rows[0].allocation.reference = 'Updated documented support';
|
|
book = recordBeverageRestitution(d, book);
|
|
assert.equal(n(book.result.rows[0].requested), '6/1');
|
|
assert.equal(book.result.rows[0].status, 'determined');
|
|
});
|
|
void test('malformed applicable books are visible errors rather than an empty list;frozen gap labels do not adopt deliberate new edits', () => {
|
|
const { d } = fixture(),
|
|
store = JSON.parse(d.modelo_a_snapshot);
|
|
store.salesOperations[d.context_opportunity].rows[0].description =
|
|
'Deliberate edited description';
|
|
d.modelo_a_snapshot = JSON.stringify(store);
|
|
const gaps = fiscalCalculationGaps(d);
|
|
assert.ok(
|
|
gaps.some((g) => g.book === 'salesOperations' && g.kind === 'stale'),
|
|
);
|
|
assert.ok(
|
|
gaps
|
|
.filter((g) => g.book === 'salesOperations' && g.kind !== 'stale')
|
|
.every(
|
|
(g) => !g.calculation.includes('Deliberate edited description'),
|
|
),
|
|
);
|
|
store.books[d.context_opportunity] = 'malformed';
|
|
assert.throws(
|
|
() =>
|
|
fiscalCalculationGaps({
|
|
...d,
|
|
modelo_a_snapshot: JSON.stringify(store),
|
|
}),
|
|
/não recuperável/,
|
|
);
|
|
});
|
|
void test('identified AB C D fields use their actual frozen books;general request600000 never becomes approval550000 or receipt450000', () => {
|
|
const { d } = fixture();
|
|
const forms = l3FormPreparations({
|
|
...d,
|
|
context_opportunity: 'opportunity:373',
|
|
}),
|
|
march = forms.find((p) => p.branch === 'A/B' && p.period === '2026-03'),
|
|
sub = forms.find((p) => p.branch === 'C');
|
|
assert.ok(march);
|
|
assert.equal(march.fields.find((f) => f.field === '24').value, '72.00');
|
|
assert.ok(sub);
|
|
assert.equal(sub.fields.find((f) => f.field === '24').value, '61.00');
|
|
const refund = l3FormPreparations({
|
|
...d,
|
|
context_opportunity: 'opportunity:374',
|
|
}).find((p) => p.branch === 'D');
|
|
assert.ok(refund);
|
|
assert.deepEqual(
|
|
refund.fields.map((f) => [f.field, f.value]),
|
|
[
|
|
['25', '800000.00'],
|
|
['26', '200000.00'],
|
|
['27', '600000.00'],
|
|
],
|
|
);
|
|
assert.match(
|
|
refund.notes.join(' '),
|
|
/reconhecido550000.00;recebido450000.00;por receber100000.00/,
|
|
);
|
|
assert.ok(march.fields.every((f) => !['05', '06'].includes(f.field)));
|
|
assert.match(l3FormPreparationHtml(sub), /ModeloB/);
|
|
const snapshot = JSON.parse(d.modelo_a_snapshot);
|
|
snapshot.ivaClosures['opportunity:373'].rows.find(
|
|
(r) => r.period === '2026-03',
|
|
).entityRef = 'Deliberate edited entity';
|
|
const retained = l3FormPreparations({
|
|
...d,
|
|
context_opportunity: 'opportunity:373',
|
|
modelo_a_snapshot: JSON.stringify(snapshot),
|
|
}).find((p) => p.branch === 'A/B' && p.period === '2026-03');
|
|
assert.equal(retained.current, false);
|
|
assert.notEqual(retained.entity, 'Deliberate edited entity');
|
|
assert.equal(retained.fields.find((f) => f.field === '24').value, '72.00');
|
|
});
|
|
|
|
void test('non-terminating effective deduction16/3 is reconciled exactly and leaves32/3 forE without decimal approximation or excess', () => {
|
|
const { d, b } = partialPrior('1', '3', '16/3');
|
|
b.rows = [pending(b.rows[0], 'exact-remainder', '16/3', '16')];
|
|
Object.assign(b.rows[0].allocation, {
|
|
deductionFrom: '0',
|
|
deductionTo: '16/3',
|
|
deductionReference: 'SYNTHETIC QA exact allocation',
|
|
});
|
|
let book = recordBeverageRestitution(d, b);
|
|
assert.equal(
|
|
book.result.rows[0].status,
|
|
'determined',
|
|
book.result.rows[0].reason,
|
|
);
|
|
assert.equal(n(book.result.rows[0].requested), '32/3');
|
|
const changed = structuredClone(b);
|
|
changed.rows[0].allocation.from = '5.333333';
|
|
assert.equal(
|
|
recordBeverageRestitution(d, changed).result.rows[0].requested,
|
|
null,
|
|
);
|
|
changed.rows[0].allocation.from = '1/0';
|
|
assert.equal(
|
|
recordBeverageRestitution(d, changed).result.rows[0].requested,
|
|
null,
|
|
);
|
|
});
|