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 { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, calculateAcquisitions, evaluateAcquisition, newAcquisition, reuseAcquisitions, saveAcquisitionBook, } from '../../resources/js/lib/iva-acquisitions.ts'; import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts'; import { serializeBusinessPlanSnapshot, restoreBusinessPlanSnapshot, } from '../../resources/js/lib/business-financial-plan.ts'; export function acquisitionsFixture() { const draft = closureFixture().draft; draft.project_title = 'SYNTHETIC QA — aquisições e apuramento parcial Modelo A'; draft.province_id = 'cf5e81f1-ffbc-535d-b1b6-b44b445c4421'; draft.district_id = ''; draft.selection_labels = { province_id: 'Manica' }; const book = { currency: 'MZN', taxpayerRegistered: 'yes', rows: [ { ...newAcquisition('qa-inventory', '2026-01', 'MZN'), net: '100', vat: '16', gross: '116', documentType: 'invoice', documentRef: 'SYNTHETIC QA 001', documentDate: '2026-01-10', supplier: 'SYNTHETIC QA supplier', supplierRegistered: 'yes', legalDocument: 'yes', held: 'yes', domestic: 'yes', allocation: 'taxable_only', exclusion: 'none', integrity: 'yes', description: 'SYNTHETIC QA existências', }, ], periods: ['2026-01', '2026-02', '2026-03', '2026-04'].map((period) => ({ period, complete: true, absent: [...acquisitionExcludedFields], })), }; return { draft, book }; } void test('a recorded purchase does not establish VAT, legal document or deduction', () => { const row = newAcquisition('a', '2026-01', 'MZN'); row.net = '100'; assert.equal(evaluateAcquisition(row, 'MZN', 'yes').deductible, null); row.vat = '16'; let r = evaluateAcquisition(row, 'MZN', 'yes'); assert.equal(r.gross, '116.00'); assert.equal(r.deductible, null); row.gross = '117'; assert.match(evaluateAcquisition(row, 'MZN', 'yes').reason, /corresponder/); }); void test('eligibility handles exclusions, mixed use, document, supplier, date and currency without inventing rules', () => { const row = acquisitionsFixture().book.rows[0]; assert.equal(evaluateAcquisition(row, 'MZN').deductible, null); assert.equal(evaluateAcquisition(row, 'MZN', 'yes').deductible, '16.00'); for (const patch of [ { documentRef: '' }, { supplierRegistered: '' }, { held: '' }, { allocation: 'mixed' }, { exclusion: 'special' }, { documentDate: '2026-02-10' }, { documentDate: '2026-99-10' }, { domestic: 'no' }, { sourceCurrency: 'USD' }, ]) assert.equal( evaluateAcquisition({ ...row, ...patch }, 'MZN', 'yes').deductible, null, JSON.stringify(patch), ); for (const patch of [ { supplierRegistered: 'no' }, { allocation: 'no_right' }, { exclusion: 'excluded' }, { integrity: 'no' }, ]) assert.equal( evaluateAcquisition({ ...row, ...patch }, 'MZN', 'yes').deductible, '0.00', ); assert.equal( evaluateAcquisition({ ...row, origin: 'effective' }, 'MZN', 'yes') .deductible, '16.00', ); assert.equal( evaluateAcquisition( { ...row, net: '100', vat: '0', gross: '100', documentRef: '' }, 'MZN', 'yes', ).deductible, '0.00', ); }); void test('duplicates, ambiguous opportunity and absent declarations are not silently accepted', () => { const { draft, book } = acquisitionsFixture(); book.rows.push({ ...book.rows[0], id: 'duplicate' }); assert.throws(() => calculateAcquisitions(draft, book), /repetido/); assert.throws( () => acquisitionBook({ ...draft, context_opportunity: '', context_applicability_snapshot: JSON.stringify({ opportunity_choices: [{}, {}], }), }), /Escolha a oportunidade/, ); const missing = acquisitionsFixture(); missing.book.periods[0].absent = []; missing.book.result = calculateAcquisitions(missing.draft, missing.book); missing.draft.modelo_a_snapshot = saveAcquisitionBook( missing.draft, missing.book, ); assert.match( JSON.stringify(acquisitionReportRows(missing.draft)), /nenhum/, ); assert.equal(missing.book.result.periods[0].fields['17'], null); }); void test('08–10, formulas14–18, decimal precision, missing parcels and explicit absence', () => { const { draft, book } = acquisitionsFixture(); book.rows.push( { ...book.rows[0], id: 'asset', field: '08', documentLine: '2', net: '200', vat: '32', gross: '232', }, { ...book.rows[0], id: 'service', field: '10', documentLine: '3', net: '50', vat: '8', gross: '58', }, ); const result = calculateAcquisitions(draft, book); const f = result.periods[0].fields; assert.equal(f['08'], '32.00'); assert.equal(f['09'], '16.00'); assert.equal(f['10'], '8.00'); assert.equal(f['14'], '1000.03'); assert.equal(f['15'], '56.00'); assert.equal(f['16'], '160.0048'); assert.equal(f['17'], '104.0048'); assert.equal(f['18'], '0.00'); assert.equal(result.periods[2].fields['17'], '0.00'); assert.equal(result.periods[3].fields['17'], null); book.periods[0].absent = []; const incomplete = calculateAcquisitions(draft, book).periods[0]; for (const field of ['14', '15', '16', '17', '18']) assert.equal(incomplete.fields[field], null); book.periods[0].absent = [...acquisitionExcludedFields]; book.periods[0].complete = false; assert.equal( calculateAcquisitions(draft, book).periods[0].fields['15'], null, ); const credit = acquisitionsFixture(); credit.book.rows[0] = { ...credit.book.rows[0], net: '2000', vat: '320', gross: '2320', }; assert.equal( calculateAcquisitions(credit.draft, credit.book).periods[0].fields[ '18' ], '159.9952', ); assert.throws( () => calculateAcquisitions({ ...draft, imports: '5' }, book), /importa/, ); }); void test('reuse keeps source values unclassified, preserves deliberate changes and detects source drift', () => { const { draft } = acquisitionsFixture(); let book = reuseAcquisitions(draft, acquisitionBook(draft)); assert.equal(book.rows[0].sourceAmount, '0.00'); assert.equal(book.rows[0].net, ''); assert.equal(book.rows[0].sourceBasis, ''); book.rows[0].net = '123'; book = reuseAcquisitions(draft, book); assert.equal(book.rows[0].net, '123'); assert.equal(book.rows.length, 4); const plan = restoreBusinessPlanSnapshot(draft.business_plan_snapshot); plan.monthlyInventoryPurchases = '1'; const changed = { ...draft, business_plan_snapshot: serializeBusinessPlanSnapshot(plan), }; assert.throws(() => calculateAcquisitions(changed, book), /plano mudaram/); }); void test('asset and operating budgets can be reused explicitly without becoming VAT or deductions', () => { const { draft, book } = acquisitionsFixture(); const plan = restoreBusinessPlanSnapshot(draft.business_plan_snapshot); plan.assetInvestment = '200'; plan.monthlyOperatingExpenses = '50'; draft.business_plan_snapshot = serializeBusinessPlanSnapshot(plan); const refs = acquisitionPlanReferences(draft); assert.equal(refs.find((r) => r.kind === 'asset').amount, '200'); assert.equal(refs.find((r) => r.key === 'operating:1').amount, '50'); const row = { ...newAcquisition('asset-reference', '2026-01', 'MZN'), field: '08', sourceField: 'asset', sourceMonth: 1, sourceAmount: '200', sourceBasis: 'net', net: '200', }; book.rows = [row]; assert.equal(calculateAcquisitions(draft, book).rows[0].deductible, null); row.vat = '32'; assert.equal(calculateAcquisitions(draft, book).rows[0].deductible, null); assert.equal(calculateAcquisitions(draft, book).rows[0].gross, '232.00'); }); void test('order month is retained as origin and never forces invoice or deduction month', () => { const { draft } = acquisitionsFixture(); const plan = restoreBusinessPlanSnapshot(draft.business_plan_snapshot); plan.useInventoryPurchaseSchedule = true; plan.inventoryPurchaseSchedule = [ { amount: '100', leadMonths: 0 }, { amount: '0', leadMonths: 0 }, { amount: '0', leadMonths: 0 }, { amount: '0', leadMonths: 0 }, ]; draft.business_plan_snapshot = serializeBusinessPlanSnapshot(plan); let book = reuseAcquisitions(draft, acquisitionBook(draft)); book.taxpayerRegistered = 'yes'; book.periods = book.periods.map((p) => ({ ...p, complete: true, absent: [...acquisitionExcludedFields], })); book.rows = book.rows.map((row) => ({ ...row, net: row.sourceAmount, vat: '0', sourceBasis: 'net', })); book.rows[0] = { ...book.rows[0], ...acquisitionsFixture().book.rows[0], id: 'plan-1', sourceMonth: 1, sourcePeriod: '2026-01', sourceAmount: '100.00', sourceBasis: 'net', period: '2026-02', documentDate: '2026-02-10', }; const result = calculateAcquisitions(draft, book); assert.equal(result.rows[0].deductible, '16.00'); assert.equal(result.periods[0].fields['09'], '0.00'); assert.equal(result.periods[1].fields['09'], '16.00'); assert.equal(result.inputs[0].sourcePeriod, '2026-01'); }); void test('second opportunity changes preserve first book, frozen results and report context after recovery', () => { const { draft, book } = acquisitionsFixture(); draft.context_opportunity = 'opportunity:1'; book.result = calculateAcquisitions(draft, book); draft.modelo_a_snapshot = saveAcquisitionBook(draft, book); const first = JSON.stringify(acquisitionBook(draft)); draft.context_opportunity = 'opportunity:309'; assert.equal(acquisitionBook(draft).rows.length, 0); const second = acquisitionsFixture().book; second.rows[0].vat = '32'; second.rows[0].net = '200'; second.rows[0].gross = '232'; second.result = calculateAcquisitions(draft, second); draft.modelo_a_snapshot = saveAcquisitionBook(draft, second); const recovered = JSON.parse(JSON.stringify(draft)); assert.equal( acquisitionCurrent(recovered, acquisitionBook(recovered)), true, ); assert.equal( acquisitionBook(recovered).result.periods[0].fields['17'], '128.0048', ); assert.equal( JSON.stringify( acquisitionBook({ ...draft, context_opportunity: 'opportunity:1' }), ), first, ); const report = JSON.stringify( createProjectDossierReport(recovered, { selectedSections: new Set(['finance']), }), ); assert.match(report, /128\.0048/); assert.match(report, /opportunity:309/); assert.doesNotMatch(report, /opportunity:1;/); const old = JSON.stringify(second.result); second.rows[0].vat = '33'; draft.modelo_a_snapshot = saveAcquisitionBook(draft, second); assert.equal(acquisitionCurrent(draft, second), false); assert.equal(JSON.stringify(second.result), old); assert.match( JSON.stringify(acquisitionReportRows(draft)), /Histórico desactualizado/, ); assert.throws(() => acquisitionBook({ modelo_a_snapshot: 'broken' })); }); if (process.env.IVA_ACQUISITION_FIXTURE_OUT) { const { draft, book } = acquisitionsFixture(); book.result = calculateAcquisitions( draft, book, '2026-10-24T12:00:00.000Z', ); draft.modelo_a_snapshot = saveAcquisitionBook(draft, book); fs.writeFileSync( process.env.IVA_ACQUISITION_FIXTURE_OUT, JSON.stringify(draft, null, 2), ); }