350 lines
13 KiB
JavaScript
350 lines
13 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 {
|
||
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),
|
||
);
|
||
}
|