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