525 lines
17 KiB
JavaScript
525 lines
17 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
newSalesOperation,
|
|
recordSales,
|
|
saveSalesBook,
|
|
operationTreatments,
|
|
salesBook,
|
|
} from '../../resources/js/lib/iva-operations.ts';
|
|
import {
|
|
newAcquisition,
|
|
evaluateAcquisition,
|
|
recordAcquisitions,
|
|
saveAcquisitionBook,
|
|
acquisitionBook,
|
|
acquisitionCurrent,
|
|
acquisitionReportRows,
|
|
readAcquisitionStore,
|
|
} from '../../resources/js/lib/iva-acquisitions.ts';
|
|
import { createAcquisitionRightLink } from '../../resources/js/lib/iva-acquisition-right.ts';
|
|
import {
|
|
newAcquisitionBC,
|
|
reuseAcquisitionImport,
|
|
acquisitionImportSources,
|
|
} from '../../resources/js/lib/iva-acquisition-bc.ts';
|
|
import {
|
|
newImportExemption,
|
|
saveImportExemptionBook,
|
|
} from '../../resources/js/lib/iva-import-exemptions.ts';
|
|
const frozen = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/iva-l2bc-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
export function bcFixture() {
|
|
const draft = {
|
|
...structuredClone(frozen),
|
|
context_opportunity: 'opportunity:369',
|
|
project_title: 'SYNTHETIC QA — L2-B/C importações e exclusões',
|
|
};
|
|
const sale = {
|
|
...newSalesOperation('sale', '2026-12', 'MZN'),
|
|
description: 'SYNTHETIC QA venda geral',
|
|
establishment: 'Manica',
|
|
amount: '1000',
|
|
basis: 'net',
|
|
treatment: 'general',
|
|
confirmed: operationTreatments
|
|
.find((t) => t.id === 'general')
|
|
.conditions.map((_, i) => i),
|
|
evidence: 'SYNTHETIC QA condições',
|
|
};
|
|
draft.modelo_a_snapshot = saveSalesBook(
|
|
draft,
|
|
recordSales(draft, {
|
|
currency: 'MZN',
|
|
rows: [sale],
|
|
periods: [{ period: '2026-12', complete: true }],
|
|
}),
|
|
);
|
|
const link = {
|
|
...createAcquisitionRightLink(draft, 'sale'),
|
|
scopeConfirmed: 'yes',
|
|
allocationRef: 'SYNTHETIC QA afectação exclusiva',
|
|
noExcludedPayments: 'yes',
|
|
};
|
|
const row = (id, patch = {}, bc = {}) => ({
|
|
...newAcquisition(id, '2026-12', 'MZN'),
|
|
net: '100',
|
|
vat: '16',
|
|
gross: '116',
|
|
description: 'SYNTHETIC QA ' + id,
|
|
documentType: 'invoice',
|
|
documentRef: 'SYNTHETIC QA ' + id,
|
|
documentDate: '2026-12-10',
|
|
supplier: 'SYNTHETIC QA fornecedor',
|
|
supplierRegistered: 'yes',
|
|
legalDocument: 'yes',
|
|
held: 'yes',
|
|
domestic: 'yes',
|
|
allocation: 'taxable_only',
|
|
exclusion: 'none',
|
|
integrity: 'yes',
|
|
deductionLink: structuredClone(link),
|
|
bc: {
|
|
...newAcquisitionBC(),
|
|
expense: 'ordinary',
|
|
reducedSupply: 'no',
|
|
evidence: 'SYNTHETIC QA natureza e âmbito',
|
|
...bc,
|
|
},
|
|
...patch,
|
|
});
|
|
const imported = row(
|
|
'import-due',
|
|
{
|
|
field: '11',
|
|
domestic: 'no',
|
|
documentType: 'customs',
|
|
supplier: '',
|
|
supplierRegistered: '',
|
|
},
|
|
{
|
|
importer: 'SYNTHETIC QA importador',
|
|
belongs: 'yes',
|
|
ownImportVat: 'yes',
|
|
vatBasis: 'due',
|
|
goodsRef: 'SYNTHETIC QA bens/linha1',
|
|
vatRef: 'SYNTHETIC QA bilhete de despacho',
|
|
facts: { exigibleInPeriod: 'yes' },
|
|
},
|
|
);
|
|
const book = {
|
|
currency: 'MZN',
|
|
rightVersion: 'acquisition-operation-right/1',
|
|
taxpayerRegistered: 'yes',
|
|
rows: [
|
|
imported,
|
|
row(
|
|
'diesel',
|
|
{},
|
|
{
|
|
expense: 'fuel',
|
|
fuelKind: 'diesel',
|
|
dieselUse: 'other',
|
|
facts: { resale: 'no' },
|
|
},
|
|
),
|
|
row('travel', {}, { expense: 'travel' }),
|
|
row(
|
|
'fixed-phone',
|
|
{},
|
|
{ expense: 'telephone', facts: { ownLandline: 'yes' } },
|
|
),
|
|
],
|
|
periods: [
|
|
{
|
|
period: '2026-12',
|
|
complete: true,
|
|
importsComplete: true,
|
|
absent: ['12', '13'],
|
|
},
|
|
],
|
|
};
|
|
return { draft, book, row, imported };
|
|
}
|
|
void test('B customs document has own conditions, unknown is not exempt/zero and supplier domestic registration is irrelevant', () => {
|
|
const { draft, imported } = bcFixture();
|
|
const run = (patch = {}, bc = {}) =>
|
|
evaluateAcquisition(
|
|
{ ...imported, ...patch, bc: { ...imported.bc, ...bc } },
|
|
'MZN',
|
|
'yes',
|
|
draft,
|
|
);
|
|
assert.equal(run().deductible, '16.00');
|
|
assert.equal(run().eligibility.status, 'eligible');
|
|
assert.equal(
|
|
run({ net: '', gross: '' }, { reducedSupply: '' }).deductible,
|
|
'16.00',
|
|
);
|
|
assert.equal(run({ vat: '' }).deductible, null);
|
|
assert.equal(
|
|
run({ acquisitionTreatment: 'former_exploration_exemption' })
|
|
.deductible,
|
|
null,
|
|
);
|
|
assert.equal(run({ acquisitionTreatment: 'unknown' }).deductible, null);
|
|
for (const bc of [
|
|
{ belongs: '' },
|
|
{ importer: '' },
|
|
{ goodsRef: '' },
|
|
{ vatRef: '' },
|
|
{ ownImportVat: '' },
|
|
{ vatBasis: 'unknown' },
|
|
{ facts: { exigibleInPeriod: '' } },
|
|
{ facts: { exigibleInPeriod: 'no' } },
|
|
])
|
|
assert.equal(run({}, bc).deductible, null, JSON.stringify(bc));
|
|
assert.equal(run({ documentType: 'invoice' }).deductible, null);
|
|
assert.equal(run({ held: '' }).deductible, null);
|
|
assert.equal(run({ documentDate: '2026-11-10' }).deductible, null);
|
|
assert.equal(
|
|
run({ vat: '0', gross: '100' }, { vatBasis: 'exempt' }).deductible,
|
|
'0.00',
|
|
);
|
|
assert.equal(
|
|
run({ vat: '0', gross: '100' }, { vatBasis: 'zero' }).deductible,
|
|
'0.00',
|
|
);
|
|
assert.equal(
|
|
run({ vat: '', gross: '' }, { vatBasis: 'exempt' }).deductible,
|
|
null,
|
|
);
|
|
assert.equal(run({}, { vatBasis: 'exempt' }).deductible, null);
|
|
});
|
|
void test('C exclusions/exceptions retain their own scope and unknown facts never imply no exclusion', () => {
|
|
const { draft, row } = bcFixture();
|
|
const run = (bc, patch = {}) =>
|
|
evaluateAcquisition(row('case', patch, bc), 'MZN', 'yes', draft);
|
|
const cases = [
|
|
[
|
|
{
|
|
expense: 'vehicle',
|
|
facts: { tourism: 'yes', saleOrExploit: 'no' },
|
|
},
|
|
'0.00',
|
|
],
|
|
[
|
|
{
|
|
expense: 'vehicle',
|
|
facts: { tourism: 'yes', saleOrExploit: 'yes' },
|
|
},
|
|
'16.00',
|
|
],
|
|
[{ expense: 'vehicle', facts: { tourism: 'no' } }, '16.00'],
|
|
[{ expense: 'vehicle', facts: {} }, null],
|
|
[
|
|
{ expense: 'fuel', facts: { resale: 'no' }, fuelKind: 'other' },
|
|
'0.00',
|
|
],
|
|
[{ expense: 'fuel', facts: { resale: 'yes' } }, '16.00'],
|
|
[
|
|
{
|
|
expense: 'fuel',
|
|
facts: { resale: 'no' },
|
|
fuelKind: 'diesel',
|
|
dieselUse: 'other',
|
|
},
|
|
'8.00',
|
|
],
|
|
[{ expense: 'travel' }, '0.00'],
|
|
[
|
|
{ expense: 'lodging_food', facts: { commercialTraveller: 'yes' } },
|
|
'16.00',
|
|
],
|
|
[
|
|
{ expense: 'lodging_food', facts: { commercialTraveller: 'no' } },
|
|
'0.00',
|
|
],
|
|
[{ expense: 'lodging_food', facts: {} }, null],
|
|
[{ expense: 'beverages', facts: { beverageSpecial: 'no' } }, '0.00'],
|
|
[{ expense: 'beverages', facts: {} }, null],
|
|
[{ expense: 'telephone', facts: { ownLandline: 'yes' } }, '16.00'],
|
|
[{ expense: 'telephone', facts: { ownLandline: 'no' } }, '0.00'],
|
|
[{ expense: 'telephone', facts: {} }, null],
|
|
[{ expense: 'luxury' }, '0.00'],
|
|
[{ expense: 'ordinary', reducedSupply: 'yes' }, '0.00'],
|
|
[{ expense: 'ordinary', reducedSupply: '' }, null],
|
|
];
|
|
for (const [bc, expected] of cases) {
|
|
const r = run(bc);
|
|
assert.equal(r.deductible, expected, JSON.stringify(bc));
|
|
assert.ok(r.eligibility.reason);
|
|
assert.match(r.eligibility.source, /20\(3\)/);
|
|
}
|
|
for (const dieselUse of [
|
|
'heavy_passengers',
|
|
'public_passengers',
|
|
'machine',
|
|
'agriculture',
|
|
]) {
|
|
assert.equal(
|
|
run({
|
|
expense: 'fuel',
|
|
fuelKind: 'diesel',
|
|
dieselUse,
|
|
facts: { resale: 'no', dieselScope: 'yes' },
|
|
}).deductible,
|
|
'16.00',
|
|
);
|
|
assert.equal(
|
|
run({
|
|
expense: 'fuel',
|
|
fuelKind: 'diesel',
|
|
dieselUse,
|
|
facts: { resale: 'no', dieselScope: '' },
|
|
}).deductible,
|
|
null,
|
|
);
|
|
}
|
|
for (const patch of [
|
|
{ held: '' },
|
|
{ legalDocument: '' },
|
|
{ documentDate: '2026-11-01' },
|
|
{ allocation: 'mixed' },
|
|
{ integrity: '' },
|
|
])
|
|
assert.equal(
|
|
run({ expense: 'telephone', facts: { ownLandline: 'yes' } }, patch)
|
|
.deductible,
|
|
null,
|
|
);
|
|
});
|
|
void test('F includes import11, excludes denied purchases and preserves exact diesel fractions across totals', () => {
|
|
const { draft, book } = bcFixture();
|
|
const result = recordAcquisitions(draft, book).result;
|
|
assert.deepEqual(
|
|
['09', '11', '14', '15', '16', '17', '18'].map(
|
|
(k) => result.periods[0].fields[k],
|
|
),
|
|
['24.00', '16.00', '1000.00', '40.00', '160.00', '120.00', '0.00'],
|
|
);
|
|
const partial = structuredClone(book);
|
|
partial.periods[0].importsComplete = false;
|
|
assert.equal(
|
|
recordAcquisitions(draft, partial).result.periods[0].fields['11'],
|
|
null,
|
|
);
|
|
assert.equal(
|
|
recordAcquisitions(draft, partial).result.periods[0].fields['15'],
|
|
null,
|
|
);
|
|
partial.periods[0].absent.push('11');
|
|
assert.throws(() => recordAcquisitions(draft, partial), /importações/);
|
|
const precision = structuredClone(book);
|
|
precision.rows = [0, 1].map((i) => ({
|
|
...book.rows[1],
|
|
id: 'exact-' + i,
|
|
documentLine: String(i),
|
|
net: '1',
|
|
vat: '0.000001',
|
|
gross: '1.000001',
|
|
}));
|
|
const exact = recordAcquisitions(draft, precision).result;
|
|
assert.deepEqual(exact.rows[0].deductibleExact, {
|
|
numerator: '1',
|
|
denominator: '2000000',
|
|
});
|
|
assert.equal(exact.periods[0].fields['09'], '0.000001');
|
|
assert.deepEqual(exact.periods[0].exactFields['15'], {
|
|
numerator: '1',
|
|
denominator: '1000000',
|
|
});
|
|
const duplicate = structuredClone(book);
|
|
duplicate.rows.push({
|
|
...book.rows[0],
|
|
id: 'duplicate',
|
|
origin: 'effective',
|
|
});
|
|
assert.throws(() => recordAcquisitions(draft, duplicate), /dupla dedução/);
|
|
});
|
|
void test('import reuse fills only missing data, detects source drift and preserves A plus every prior map and frozen history', () => {
|
|
let { draft, book, imported } = bcFixture();
|
|
const origin = {
|
|
...newImportExemption('source', 'MZN'),
|
|
description: 'SYNTHETIC QA origemL1',
|
|
period: '2026-12',
|
|
importer: 'SYNTHETIC QA origem importador',
|
|
documentRef: 'ORIGINAL',
|
|
documentDate: '2026-12-12',
|
|
};
|
|
draft.modelo_a_snapshot = saveImportExemptionBook(draft, {
|
|
rows: [origin],
|
|
});
|
|
assert.equal(acquisitionImportSources(draft).length, 1);
|
|
const reused = reuseAcquisitionImport(
|
|
draft,
|
|
imported,
|
|
'importExemptions',
|
|
'source',
|
|
);
|
|
assert.equal(reused.documentRef, imported.documentRef);
|
|
assert.equal(reused.documentDate, imported.documentDate);
|
|
assert.equal(reused.bc.importer, imported.bc.importer);
|
|
assert.equal(reused.vat, '16');
|
|
book.rows[0] = reused;
|
|
book = recordAcquisitions(draft, book);
|
|
draft.modelo_a_snapshot = saveAcquisitionBook(draft, book);
|
|
const first = structuredClone(book.result);
|
|
book = recordAcquisitions(draft, book);
|
|
draft.modelo_a_snapshot = saveAcquisitionBook(draft, book);
|
|
const recovered = JSON.parse(JSON.stringify(draft));
|
|
assert.ok(acquisitionCurrent(recovered, acquisitionBook(recovered)));
|
|
assert.deepEqual(book.history, [first]);
|
|
const before = JSON.parse(frozen.modelo_a_snapshot),
|
|
after = JSON.parse(draft.modelo_a_snapshot);
|
|
for (const k of Object.keys(before).filter((k) => k !== 'version'))
|
|
for (const [id, value] of Object.entries(before[k]))
|
|
assert.deepEqual(after[k][id], value, k + id);
|
|
assert.ok(
|
|
acquisitionCurrent(
|
|
{ ...recovered, context_opportunity: 'opportunity:368' },
|
|
before.books['opportunity:368'],
|
|
),
|
|
);
|
|
const report = JSON.stringify(acquisitionReportRows(recovered));
|
|
for (const v of [
|
|
'CondiçõesB/C',
|
|
'40.00',
|
|
'120.00',
|
|
'importador',
|
|
'20(3)',
|
|
'8/1',
|
|
])
|
|
assert.ok(report.includes(v), v);
|
|
draft.modelo_a_snapshot = saveImportExemptionBook(draft, {
|
|
rows: [{ ...origin, description: 'Editado' }],
|
|
});
|
|
assert.equal(acquisitionCurrent(draft, book), false);
|
|
assert.equal(
|
|
evaluateAcquisition(book.rows[0], 'MZN', 'yes', draft).deductible,
|
|
null,
|
|
);
|
|
assert.deepEqual(book.history[0], first);
|
|
draft.modelo_a_snapshot = saveImportExemptionBook(draft, {
|
|
rows: [{ ...origin, treatment: 'service_in_customs_value' }],
|
|
});
|
|
const service = reuseAcquisitionImport(
|
|
draft,
|
|
book.rows[0],
|
|
'importExemptions',
|
|
'source',
|
|
);
|
|
assert.equal(
|
|
evaluateAcquisition(service, 'MZN', 'yes', draft).deductible,
|
|
null,
|
|
);
|
|
assert.match(
|
|
evaluateAcquisition(service, 'MZN', 'yes', draft).reason,
|
|
/não IVA próprio/,
|
|
);
|
|
const broken = JSON.parse(recovered.modelo_a_snapshot);
|
|
broken.books['opportunity:369'].rows[0].bc.facts = { bad: 'maybe' };
|
|
assert.throws(
|
|
() => readAcquisitionStore(JSON.stringify(broken)),
|
|
/CondiçõesB\/C/,
|
|
);
|
|
if (process.env.IVA_L2BC_CALCULATED)
|
|
fs.writeFileSync(
|
|
process.env.IVA_L2BC_CALCULATED,
|
|
JSON.stringify(recovered),
|
|
);
|
|
});
|
|
void test('beverages13(2) uses current c/d/h source and deduction-only scope without granting direct exemption or refund', () => {
|
|
for (const treatment of [
|
|
'onboard_seagoing_ship',
|
|
'onboard_international_aircraft',
|
|
'onboard_rescue_coastal_ship',
|
|
'onboard_departing_warship',
|
|
]) {
|
|
const { draft, row } = bcFixture();
|
|
const ops = salesBook(draft);
|
|
const rule = operationTreatments.find((t) => t.id === treatment);
|
|
ops.rows.push({
|
|
...newSalesOperation('beverages', '2026-12', 'MZN'),
|
|
description: 'SYNTHETIC QA bebidas de bordo',
|
|
establishment: 'Manica',
|
|
amount: '100',
|
|
basis: 'net',
|
|
treatment,
|
|
supplyCategory: 'beverages',
|
|
confirmed: rule.conditions.map((_, i) => i),
|
|
evidence: 'SYNTHETIC QA meio,destino,utilização e embarque',
|
|
});
|
|
draft.modelo_a_snapshot = saveSalesBook(draft, recordSales(draft, ops));
|
|
const r = row(
|
|
'beverages',
|
|
{},
|
|
{
|
|
expense: 'beverages',
|
|
beveragePath: 'deduction',
|
|
facts: {
|
|
beverageSpecial: 'yes',
|
|
onlyDeduction: 'yes',
|
|
notCoastalFishing: 'yes',
|
|
},
|
|
},
|
|
);
|
|
r.deductionLink = {
|
|
...createAcquisitionRightLink(draft, 'beverages'),
|
|
scopeConfirmed: 'yes',
|
|
allocationRef: 'SYNTHETIC QA parcela e operação',
|
|
noExcludedPayments: 'yes',
|
|
};
|
|
assert.equal(
|
|
evaluateAcquisition(r, 'MZN', 'yes', draft).deductible,
|
|
treatment === 'onboard_departing_warship' ? null : '16.00',
|
|
treatment,
|
|
);
|
|
assert.equal(
|
|
evaluateAcquisition(
|
|
{ ...r, bc: { ...r.bc, beveragePath: 'refund' } },
|
|
'MZN',
|
|
'yes',
|
|
draft,
|
|
).deductible,
|
|
treatment === 'onboard_departing_warship' ? null : '0.00',
|
|
);
|
|
assert.equal(
|
|
evaluateAcquisition(
|
|
{
|
|
...r,
|
|
bc: {
|
|
...r.bc,
|
|
facts: { ...r.bc.facts, onlyDeduction: '' },
|
|
},
|
|
},
|
|
'MZN',
|
|
'yes',
|
|
draft,
|
|
).deductible,
|
|
null,
|
|
);
|
|
if (treatment === 'onboard_rescue_coastal_ship')
|
|
assert.equal(
|
|
evaluateAcquisition(
|
|
{
|
|
...r,
|
|
bc: {
|
|
...r.bc,
|
|
facts: { ...r.bc.facts, notCoastalFishing: 'no' },
|
|
},
|
|
},
|
|
'MZN',
|
|
'yes',
|
|
draft,
|
|
).deductible,
|
|
null,
|
|
);
|
|
}
|
|
});
|