338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
newCompositionBase,
|
|
newCompositionPart,
|
|
switchCompositionPart,
|
|
compositionTreatments,
|
|
compositionConditions,
|
|
evaluateComposition,
|
|
recordCompositions,
|
|
saveCompositionBook,
|
|
compositionBook,
|
|
compositionCurrent,
|
|
compositionReportRows,
|
|
} from '../../resources/js/lib/iva-composition-base.ts';
|
|
import { noncashBaseCurrent } from '../../resources/js/lib/iva-noncash-base.ts';
|
|
import { fuelBaseCurrent } from '../../resources/js/lib/iva-fuel-base.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
const frozen = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/iva-g7f-preserved-inputs.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const part = (treatment = 'tax_component', id = treatment) => ({
|
|
...newCompositionPart(id),
|
|
treatment,
|
|
description:
|
|
'SYNTHETIC QA ' +
|
|
(treatment === 'tax_component'
|
|
? 'imposição A'
|
|
: 'transporte por conta do cliente'),
|
|
amount: treatment === 'tax_component' ? '100.10' : '50.25',
|
|
included: 'no',
|
|
reference: 'SYNTHETIC QA referência ' + id,
|
|
natureConfirmed: 'yes',
|
|
ownVatExcluded: 'yes',
|
|
});
|
|
const row = () => ({
|
|
...newCompositionBase('g7f-operation-A', 'MZN'),
|
|
description: 'SYNTHETIC QA composição operação A',
|
|
period: '2026-12',
|
|
operationRef: 'SYNTHETIC QA operação A',
|
|
initialAmount: '1000.25',
|
|
initialRef: 'SYNTHETIC QA preço sem próprioIVA/exclusões15(6)consideradas',
|
|
documentRef: 'SYNTHETIC QA G7F documento A',
|
|
documentDate: '2026-12-12',
|
|
compositionRef:
|
|
'SYNTHETIC QA parcelas A,B e montante inicial reconciliados,sem duplicação',
|
|
evidence: 'SYNTHETIC QA pressupostos expressos,não documento real',
|
|
conditions: Object.fromEntries(
|
|
compositionConditions.map(([key]) => [key, 'yes']),
|
|
),
|
|
parts: compositionTreatments.map((t) => part(t.id)),
|
|
});
|
|
const draft = () => ({
|
|
...structuredClone(frozen),
|
|
context_opportunity: 'opportunity:344',
|
|
project_title: 'SYNTHETIC QA — G7F / Composição da contraprestação',
|
|
});
|
|
|
|
void test('P101 operation and each tax/accessory condition required;ownIVA or wrong nature not silently included', () => {
|
|
const input = row(),
|
|
ok = evaluateComposition(input, 'MZN');
|
|
assert.equal(ok.baseAmount, '1150.60');
|
|
assert.equal(ok.vat, null);
|
|
assert.equal(ok.rate, null);
|
|
for (const [key] of compositionConditions)
|
|
for (const value of ['', 'unknown', 'no']) {
|
|
const r = evaluateComposition(
|
|
{ ...input, conditions: { ...input.conditions, [key]: value } },
|
|
'MZN',
|
|
);
|
|
assert.equal(
|
|
r.status,
|
|
value === 'no' ? 'not_applicable' : 'indeterminate',
|
|
);
|
|
assert.equal(r.baseAmount, null);
|
|
}
|
|
for (const t of compositionTreatments)
|
|
for (const key of ['natureConfirmed', 'ownVatExcluded'])
|
|
for (const value of ['', 'unknown', 'no']) {
|
|
const p = { ...part(t.id), [key]: value },
|
|
r = evaluateComposition({ ...input, parts: [p] }, 'MZN');
|
|
assert.equal(
|
|
r.status,
|
|
value === 'no' ? 'not_applicable' : 'indeterminate',
|
|
);
|
|
assert.equal(r.baseAmount, null);
|
|
assert.equal(r.vat, null);
|
|
}
|
|
for (const patch of [
|
|
{ description: '' },
|
|
{ operationRef: '' },
|
|
{ initialRef: '' },
|
|
{ documentRef: '' },
|
|
{ documentDate: '2026-02-30' },
|
|
{ compositionRef: '' },
|
|
{ evidence: '' },
|
|
{ period: '2025-12' },
|
|
{ currency: 'USD' },
|
|
])
|
|
assert.equal(
|
|
evaluateComposition({ ...input, ...patch }, 'MZN').baseAmount,
|
|
null,
|
|
);
|
|
for (const patch of [
|
|
{ treatment: '' },
|
|
{ description: '' },
|
|
{ reference: '' },
|
|
{ included: '' },
|
|
{ included: 'unknown' },
|
|
{ amount: '' },
|
|
])
|
|
assert.equal(
|
|
evaluateComposition(
|
|
{ ...input, parts: [{ ...part(), ...patch }] },
|
|
'MZN',
|
|
).baseAmount,
|
|
null,
|
|
);
|
|
});
|
|
|
|
void test('single operation base adds missing components only;alreadyincluded does not doublecount;exactdecimal and preserved edits', () => {
|
|
const input = row();
|
|
for (const [parts, expected] of [
|
|
[[part('tax_component')], '1100.35'],
|
|
[[part('accessory_component')], '1050.50'],
|
|
[
|
|
[{ ...part(), included: 'yes' }, part('accessory_component')],
|
|
'1050.50',
|
|
],
|
|
[input.parts.map((p) => ({ ...p, included: 'yes' })), '1000.25'],
|
|
])
|
|
assert.equal(
|
|
evaluateComposition({ ...input, parts }, 'MZN').baseAmount,
|
|
expected,
|
|
);
|
|
assert.equal(
|
|
evaluateComposition(
|
|
{
|
|
...input,
|
|
initialAmount: '0',
|
|
parts: [{ ...part(), amount: '0' }],
|
|
},
|
|
'MZN',
|
|
).baseAmount,
|
|
'0.00',
|
|
);
|
|
assert.equal(
|
|
evaluateComposition(
|
|
{
|
|
...input,
|
|
initialAmount: '1.000002',
|
|
parts: [{ ...part(), amount: '0.000001' }],
|
|
},
|
|
'MZN',
|
|
).baseAmount,
|
|
'1.000003',
|
|
);
|
|
assert.equal(
|
|
evaluateComposition(
|
|
{
|
|
...input,
|
|
initialAmount: '999999999999999.999999',
|
|
parts: [{ ...part(), amount: '999999999999999.999999' }],
|
|
},
|
|
'MZN',
|
|
).baseAmount,
|
|
'1999999999999999.999998',
|
|
);
|
|
for (const amount of ['-1', 'NaN', '0.0000001', ''])
|
|
assert.equal(
|
|
evaluateComposition(
|
|
{ ...input, parts: [{ ...part(), amount }] },
|
|
'MZN',
|
|
).baseAmount,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
evaluateComposition({ ...input, initialAmount: '' }, 'MZN').baseAmount,
|
|
null,
|
|
);
|
|
const switched = switchCompositionPart(part(), 'accessory_component');
|
|
assert.equal(switched.natureConfirmed, '');
|
|
assert.equal(switched.amount, '100.10');
|
|
assert.equal(switched.reference, part().reference);
|
|
assert.equal(
|
|
evaluateComposition({ ...input, parts: [switched] }, 'MZN').baseAmount,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
evaluateComposition({ ...input, parts: [] }, 'MZN').baseAmount,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
evaluateComposition({ ...input, parts: [part(), part()] }, 'MZN')
|
|
.baseAmount,
|
|
null,
|
|
);
|
|
});
|
|
|
|
void test('save/recovery/history/report keep one base peroperation and all83 previous books/currentkeys unchanged', () => {
|
|
const d = draft(),
|
|
prior = JSON.parse(d.modelo_a_snapshot);
|
|
assert.deepEqual(compositionBook(d), { rows: [] });
|
|
assert.deepEqual(compositionReportRows(d), []);
|
|
let book = recordCompositions(d, { rows: [{ ...row(), conditions: {} }] });
|
|
book = recordCompositions(d, {
|
|
...book,
|
|
rows: [{ ...row(), parts: [{ ...part(), ownVatExcluded: 'no' }] }],
|
|
});
|
|
book = recordCompositions(d, { ...book, rows: [row()] });
|
|
assert.equal(book.history.length, 2);
|
|
assert.ok(
|
|
book.history.every((h) =>
|
|
h.rows.every((r) => r.baseAmount === null && r.vat === null),
|
|
),
|
|
);
|
|
assert.ok(compositionCurrent(d, book));
|
|
assert.equal(book.result.rows.length, 1);
|
|
assert.equal(book.result.inputs[0].parts.length, 2);
|
|
for (const field of ['amount', 'included', 'reference', 'treatment'])
|
|
assert.ok(
|
|
!compositionCurrent(d, {
|
|
...book,
|
|
rows: book.rows.map((r) => ({
|
|
...r,
|
|
parts: r.parts.map((p) => ({ ...p, [field]: 'changed' })),
|
|
})),
|
|
}),
|
|
);
|
|
for (const patch of [
|
|
{ context_opportunity: 'opportunity:345' },
|
|
{ currency: 'USD' },
|
|
{ selection_labels: { province_id: 'Maputo' } },
|
|
])
|
|
assert.ok(!compositionCurrent({ ...d, ...patch }, book));
|
|
d.modelo_a_snapshot = saveCompositionBook(d, book);
|
|
assert.deepEqual(compositionBook(JSON.parse(JSON.stringify(d))), book);
|
|
const next = JSON.parse(d.modelo_a_snapshot);
|
|
const {
|
|
considerationCompositions: _considerationCompositions,
|
|
...untouched
|
|
} = next;
|
|
assert.deepEqual(untouched, prior);
|
|
for (const [scope, previous] of Object.entries(prior.noncashBases))
|
|
assert.ok(
|
|
noncashBaseCurrent({ ...d, context_opportunity: scope }, previous),
|
|
);
|
|
for (const [scope, previous] of Object.entries(prior.regulatedFuelBases))
|
|
assert.ok(
|
|
fuelBaseCurrent({ ...d, context_opportunity: scope }, previous),
|
|
);
|
|
assert.equal(
|
|
next.noncashBases['opportunity:343'].result.rows[1].baseAmount,
|
|
'2700.75',
|
|
);
|
|
assert.equal(
|
|
next.regulatedFuelBases['opportunity:342'].result.rows[0].baseAmount,
|
|
'1400.15',
|
|
);
|
|
assert.equal(
|
|
next.regulatedFuelBases['opportunity:342'].result.rows[1].baseAmount,
|
|
'1500.25',
|
|
);
|
|
assert.equal(
|
|
next.importExemptions['opportunity:338'].result.rows[1].importVat,
|
|
'400.00',
|
|
);
|
|
assert.equal(
|
|
next.books['opportunity:329'].result.periods[0].fields['11'],
|
|
null,
|
|
);
|
|
const report = JSON.stringify(
|
|
createProjectDossierReport(d, {
|
|
selectedSections: new Set(['finance']),
|
|
version: 'QA G7F',
|
|
}),
|
|
);
|
|
for (const token of [
|
|
'Base P101',
|
|
'1150.60',
|
|
'IVA: Indeterminado',
|
|
'opportunity:344',
|
|
'Manica',
|
|
'2026-12',
|
|
'operação A',
|
|
'15(5)',
|
|
'100.10',
|
|
'50.25',
|
|
'accessory_component',
|
|
'campo11',
|
|
])
|
|
assert.ok(report.includes(token), token);
|
|
assert.equal(compositionReportRows(d).length, 1);
|
|
assert.deepEqual(
|
|
compositionReportRows({ ...d, context_opportunity: 'opportunity:345' }),
|
|
[],
|
|
);
|
|
if (process.env.IVA_G7F_CALCULATED)
|
|
fs.writeFileSync(process.env.IVA_G7F_CALCULATED, JSON.stringify(d));
|
|
});
|
|
|
|
void test('corrupt nested components/store cannot overwrite originals;duplicate operation ids cannot be recorded', () => {
|
|
const d = draft(),
|
|
original = d.modelo_a_snapshot;
|
|
for (const patch of [
|
|
{ initialAmount: 4 },
|
|
{ conditions: [] },
|
|
{ parts: {} },
|
|
{ parts: [{ ...part(), amount: 4 }] },
|
|
{ parts: [null] },
|
|
])
|
|
assert.throws(
|
|
() => saveCompositionBook(d, { rows: [{ ...row(), ...patch }] }),
|
|
/originais preservados/,
|
|
);
|
|
assert.equal(d.modelo_a_snapshot, original);
|
|
assert.throws(
|
|
() => recordCompositions(d, { rows: [row(), row()] }),
|
|
/distintos/,
|
|
);
|
|
assert.throws(() => recordCompositions(d, { rows: [] }), /distintos/);
|
|
assert.throws(
|
|
() =>
|
|
compositionBook({
|
|
...d,
|
|
modelo_a_snapshot: JSON.stringify({
|
|
...JSON.parse(original),
|
|
considerationCompositions: [],
|
|
}),
|
|
}),
|
|
/originais preservados/,
|
|
);
|
|
});
|