Files
Investidor-V2/tests/frontend/iva-operations.test.mjs
T

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),
);
}