338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import fs from 'node:fs';
|
|
import { closureFixture } from './fixtures/simulator-closure-fixture.mjs';
|
|
import {
|
|
calculateSales,
|
|
evaluateSalesOperation,
|
|
newSalesOperation,
|
|
operationTreatments,
|
|
recordSales,
|
|
reuseSales,
|
|
salesBook,
|
|
salesCurrent,
|
|
salesReportRows,
|
|
saveSalesBook,
|
|
splitSales,
|
|
} from '../../resources/js/lib/iva-operations.ts';
|
|
import {
|
|
acquisitionBook,
|
|
calculateAcquisitions,
|
|
newAcquisition,
|
|
saveAcquisitionBook,
|
|
} from '../../resources/js/lib/iva-acquisitions.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
|
|
export function operationsFixture() {
|
|
const draft = closureFixture().draft;
|
|
draft.project_title = 'SYNTHETIC QA — L1 operações IVA';
|
|
draft.province_id = 'cf5e81f1-ffbc-535d-b1b6-b44b445c4421';
|
|
draft.selection_labels = { province_id: 'Manica' };
|
|
const rows = operationTreatments
|
|
.filter((t) =>
|
|
[
|
|
'general',
|
|
'private_health',
|
|
'private_education',
|
|
'private_training',
|
|
'personal_lessons',
|
|
'export_goods',
|
|
'housing_rent',
|
|
'public_health',
|
|
'public_education',
|
|
'public_training',
|
|
'foreign_operation',
|
|
'other',
|
|
].includes(t.id),
|
|
)
|
|
.map((t, i) => ({
|
|
...newSalesOperation(`qa-${i}`, '2026-01', 'MZN'),
|
|
description: `SYNTHETIC QA ${t.label}`,
|
|
establishment: 'SYNTHETIC QA estabelecimento Manica',
|
|
amount: t.id === 'general' ? '1161' : '100',
|
|
basis: t.id === 'general' ? 'included' : 'net',
|
|
treatment: t.id,
|
|
confirmed: t.conditions.map((_, i) => i),
|
|
evidence:
|
|
'SYNTHETIC QA — pressupostos de ensaio, não documento real',
|
|
}));
|
|
const book = {
|
|
currency: 'MZN',
|
|
rows,
|
|
periods: [{ period: '2026-01', complete: true }],
|
|
};
|
|
return { draft, book };
|
|
}
|
|
void test('each enumerated treatment calculates only with its conditions, article and source', () => {
|
|
const { book } = operationsFixture();
|
|
for (const row of book.rows) {
|
|
const r = evaluateSalesOperation(row, 'MZN');
|
|
if (['other', 'foreign_operation'].includes(row.treatment)) {
|
|
assert.equal(r.vat, null);
|
|
continue;
|
|
}
|
|
assert.notEqual(r.vat, null, row.treatment);
|
|
assert.ok(r.article.includes('CIVA'));
|
|
assert.ok(r.source.includes('Lei'));
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...row, confirmed: [] }, 'MZN').vat,
|
|
null,
|
|
row.treatment,
|
|
);
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...row, evidence: '' }, 'MZN').vat,
|
|
null,
|
|
);
|
|
}
|
|
const by = Object.fromEntries(
|
|
book.rows.map((r) => [r.treatment, evaluateSalesOperation(r, 'MZN')]),
|
|
);
|
|
assert.equal(by.private_health.vat, '5.00');
|
|
assert.equal(by.export_goods.field, '05');
|
|
assert.match(by.export_goods.reason, /não é taxa zero/);
|
|
assert.equal(by.housing_rent.field, '07');
|
|
});
|
|
void test('VAT-included general uses statutory unit rounding, conserves gross; included5percent presentation is covered by the rational delta', () => {
|
|
const row = operationsFixture().book.rows[0];
|
|
const r = evaluateSalesOperation(row, 'MZN');
|
|
assert.equal(r.net, '1001.00');
|
|
assert.equal(r.vat, '160.00');
|
|
assert.equal(r.gross, '1161.00');
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...row, amount: '1160' }, 'MZN').net,
|
|
'1000.00',
|
|
);
|
|
assert.equal(
|
|
evaluateSalesOperation(
|
|
{ ...row, basis: 'net', amount: '1000.03' },
|
|
'MZN',
|
|
).vat,
|
|
'160.0048',
|
|
);
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...row, basis: '', amount: '0' }, 'MZN').vat,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...row, amount: '0' }, 'MZN').vat,
|
|
'0.00',
|
|
);
|
|
const reduced = operationsFixture().book.rows[1];
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...reduced, basis: 'included' }, 'MZN').vat,
|
|
'4.761905',
|
|
);
|
|
assert.equal(
|
|
evaluateSalesOperation(
|
|
{ ...reduced, basis: 'included', amount: '105' },
|
|
'MZN',
|
|
).net,
|
|
'100.00',
|
|
);
|
|
assert.equal(
|
|
evaluateSalesOperation(
|
|
{ ...reduced, basis: 'included', amount: '105' },
|
|
'MZN',
|
|
).vat,
|
|
'5.00',
|
|
);
|
|
});
|
|
void test('missing information, invalid dates/currency and legal ambiguity never become zero', () => {
|
|
const row = operationsFixture().book.rows[0];
|
|
for (const patch of [
|
|
{ amount: '' },
|
|
{ amount: '-1' },
|
|
{ period: '2025-12' },
|
|
{ period: '2026-13' },
|
|
{ establishment: '' },
|
|
{ sourceCurrency: 'USD' },
|
|
{ treatment: 'invented' },
|
|
])
|
|
assert.equal(
|
|
evaluateSalesOperation({ ...row, ...patch }, 'MZN').vat,
|
|
null,
|
|
);
|
|
const { draft, book } = operationsFixture();
|
|
book.result = calculateSales(draft, book);
|
|
assert.equal(book.result.rows[0].vat, '160.00');
|
|
assert.equal(book.result.periods[0].fields['02'], null);
|
|
assert.match(book.result.periods[0].reasons.join(' '), /campo06/);
|
|
book.rows = book.rows.filter(
|
|
(r) => !['other', 'foreign_operation'].includes(r.treatment),
|
|
);
|
|
book.result = calculateSales(draft, book);
|
|
assert.equal(book.result.periods[0].fields['04'], '20.00');
|
|
assert.equal(book.result.periods[0].fields['07'], '400.00');
|
|
assert.equal(book.result.periods[0].fields['05'], '100.00');
|
|
book.periods[0].complete = false;
|
|
assert.equal(calculateSales(draft, book).periods[0].fields['02'], null);
|
|
book.rows = [];
|
|
book.periods[0].complete = true;
|
|
assert.equal(calculateSales(draft, book).periods[0].fields['02'], '0.00');
|
|
});
|
|
void test('reuse preserves original sales, intentional edits and period; drift makes new calculation indeterminate', () => {
|
|
const { draft } = operationsFixture();
|
|
let book = reuseSales(draft, salesBook(draft));
|
|
assert.equal(book.rows[0].amount, '1000.03');
|
|
assert.equal(book.rows[3].amount, '');
|
|
assert.equal(book.rows[0].treatment, 'other');
|
|
book.rows[0].description = 'Edited';
|
|
book = reuseSales(draft, book);
|
|
assert.equal(book.rows.length, 4);
|
|
assert.equal(book.rows[0].description, 'Edited');
|
|
const template = operationsFixture().book.rows[0];
|
|
book.rows[0] = {
|
|
...template,
|
|
...book.rows[0],
|
|
treatment: 'general',
|
|
confirmed: [0, 1],
|
|
establishment: 'QA',
|
|
evidence: 'QA',
|
|
};
|
|
assert.notEqual(calculateSales(draft, book).rows[0].vat, null);
|
|
book.rows[0].amount = '900';
|
|
assert.equal(calculateSales(draft, book).rows[0].vat, null);
|
|
assert.throws(
|
|
() => reuseSales({ ...draft, currency: 'USD' }, book),
|
|
/moeda/i,
|
|
);
|
|
});
|
|
void test('opportunity-specific persistence, report and stale history preserve the first when only the second changes', () => {
|
|
const { draft, book } = operationsFixture();
|
|
draft.context_opportunity = 'opportunity:1';
|
|
book.result = calculateSales(draft, book, '2026-10-24T15:00:00Z');
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, book);
|
|
const first = JSON.stringify(salesBook(draft));
|
|
draft.context_opportunity = 'opportunity:309';
|
|
assert.equal(salesBook(draft).rows.length, 0);
|
|
const second = operationsFixture().book;
|
|
second.rows[0].amount = '2320';
|
|
second.result = calculateSales(draft, second);
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, second);
|
|
const recovered = JSON.parse(JSON.stringify(draft));
|
|
assert.equal(salesCurrent(recovered, salesBook(recovered)), true);
|
|
assert.equal(
|
|
JSON.stringify(
|
|
salesBook({ ...recovered, context_opportunity: 'opportunity:1' }),
|
|
),
|
|
first,
|
|
);
|
|
assert.match(
|
|
JSON.stringify(
|
|
createProjectDossierReport(recovered, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
),
|
|
/2320/,
|
|
);
|
|
assert.match(JSON.stringify(salesReportRows(recovered)), /Manica/);
|
|
second.rows[0].amount = '1';
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, second);
|
|
assert.equal(salesCurrent(draft, second), false);
|
|
assert.match(JSON.stringify(salesReportRows(draft)), /Histórico/);
|
|
assert.throws(() => salesBook({ ...draft, modelo_a_snapshot: 'broken' }));
|
|
assert.throws(
|
|
() =>
|
|
salesBook({
|
|
...draft,
|
|
modelo_a_snapshot: JSON.stringify({
|
|
version: 1,
|
|
books: {},
|
|
salesOperations: [],
|
|
}),
|
|
}),
|
|
/preservados/,
|
|
);
|
|
});
|
|
void test('acquisitions use current classified sales, refuse stale absence and do not universalise deduction', () => {
|
|
const { draft, book } = operationsFixture();
|
|
book.rows = book.rows.filter(
|
|
(r) => !['other', 'foreign_operation'].includes(r.treatment),
|
|
);
|
|
book.result = calculateSales(draft, book);
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, book);
|
|
const a = {
|
|
currency: 'MZN',
|
|
rows: [],
|
|
periods: ['2026-01', '2026-02', '2026-03', '2026-04'].map((period) => ({
|
|
period,
|
|
complete: true,
|
|
absent: ['03', '04', '05', '06', '07', '11', '12', '13'],
|
|
})),
|
|
};
|
|
const result = calculateAcquisitions(draft, a);
|
|
assert.equal(result.periods[0].fields['04'], '20.00');
|
|
assert.equal(result.periods[0].fields['14'], '1901.00');
|
|
assert.equal(result.periods[0].fields['16'], '180.00');
|
|
a.result = result;
|
|
draft.modelo_a_snapshot = saveAcquisitionBook(draft, a);
|
|
assert.equal(salesCurrent(draft, salesBook(draft)), true);
|
|
assert.equal(
|
|
acquisitionBook(draft).result.periods[0].fields['04'],
|
|
'20.00',
|
|
);
|
|
a.rows = [
|
|
{
|
|
...newAcquisition('new-context-purchase', '2026-01', 'MZN'),
|
|
net: '100',
|
|
vat: '16',
|
|
gross: '116',
|
|
documentType: 'invoice',
|
|
documentRef: 'SYNTHETIC QA',
|
|
documentDate: '2026-01-10',
|
|
supplier: 'SYNTHETIC QA',
|
|
supplierRegistered: 'yes',
|
|
legalDocument: 'yes',
|
|
held: 'yes',
|
|
domestic: 'yes',
|
|
allocation: 'taxable_only',
|
|
exclusion: 'none',
|
|
integrity: 'yes',
|
|
},
|
|
];
|
|
a.taxpayerRegistered = 'yes';
|
|
assert.equal(calculateAcquisitions(draft, a).periods[0].fields['09'], null);
|
|
a.rows[0].allocation = 'no_right';
|
|
assert.equal(
|
|
calculateAcquisitions(draft, a).periods[0].fields['09'],
|
|
'0.00',
|
|
);
|
|
a.rows = [];
|
|
book.rows[0].amount = '116';
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, book);
|
|
assert.equal(calculateAcquisitions(draft, a).periods[0].fields['02'], null);
|
|
});
|
|
void test('split source reconciles every parcel and recalculation keeps the prior frozen version', () => {
|
|
const { draft } = operationsFixture();
|
|
let book = reuseSales(draft, salesBook(draft));
|
|
book = splitSales(book, book.rows[0].id, 'second-part');
|
|
const group = book.rows.filter((r) => r.splitSource);
|
|
assert.equal(group.length, 2);
|
|
for (const [i, r] of group.entries())
|
|
Object.assign(r, {
|
|
amount: i ? '500.03' : '500',
|
|
treatment: 'general',
|
|
confirmed: [0, 1],
|
|
establishment: 'QA',
|
|
evidence: 'QA',
|
|
});
|
|
assert.notEqual(calculateSales(draft, book).rows[0].vat, null);
|
|
group[1].amount = '499';
|
|
assert.equal(calculateSales(draft, book).rows[0].vat, null);
|
|
group[1].amount = '500.03';
|
|
book = recordSales(draft, book);
|
|
const frozen = JSON.stringify(book.result);
|
|
book = recordSales(draft, book);
|
|
assert.equal(JSON.stringify(book.history[0]), frozen);
|
|
assert.equal(book.result.revision, 2);
|
|
});
|
|
if (process.env.IVA_L1_FIXTURE_OUT) {
|
|
const { draft, book } = operationsFixture();
|
|
book.result = calculateSales(draft, book, '2026-10-24T15:00:00Z');
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, book);
|
|
fs.writeFileSync(
|
|
process.env.IVA_L1_FIXTURE_OUT,
|
|
JSON.stringify(draft, null, 2),
|
|
);
|
|
}
|