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

350 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
);
}