287 lines
9.5 KiB
JavaScript
287 lines
9.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
newCurrencyEvent,
|
|
currencyEventTreatments,
|
|
evaluateCurrencyEvent,
|
|
recordCurrencyEvents,
|
|
saveCurrencyEventBook,
|
|
currencyEventBook,
|
|
currencyEventCurrent,
|
|
currencyEventReportRows,
|
|
switchCurrencyEvent,
|
|
} from '../../resources/js/lib/iva-currency-events.ts';
|
|
const frozen = JSON.parse(
|
|
fs.readFileSync(
|
|
'tests/frontend/fixtures/iva-currency-events-preserved.json',
|
|
'utf8',
|
|
),
|
|
);
|
|
const draft = {
|
|
...frozen,
|
|
currency: 'MZN',
|
|
context_opportunity: 'opportunity:357',
|
|
project_title: 'SYNTHETIC QA — Moeda e eventos / Manica',
|
|
};
|
|
function row(t) {
|
|
return {
|
|
...newCurrencyEvent(t, 'MZN'),
|
|
treatment: t,
|
|
description: 'SYNTHETIC QA ' + t,
|
|
period: '2026-12',
|
|
origin: 'projected',
|
|
operationRef: 'SYNTHETIC QA operação existente',
|
|
documentRef: 'SYNTHETIC QA documento/pressuposto',
|
|
documentDate: '2026-12-12',
|
|
scope: 'SYNTHETIC QA conjunto/parte/operação/período',
|
|
evidence: 'SYNTHETIC QA pressupostos expressos',
|
|
eventDate: '2026-12-10',
|
|
eventRef:
|
|
'SYNTHETIC QA evento específico/artigo7,excepções próprias conferidas',
|
|
otherDate:
|
|
t === 'consignment_deadline'
|
|
? '2026-06-13'
|
|
: t === 'contract_effects'
|
|
? '2026-12-01'
|
|
: '2026-12-12',
|
|
reviewDate: '2026-12-31',
|
|
factsConfirmed: 'yes',
|
|
invoiceRequired: 'yes',
|
|
invoiceState: 'issued',
|
|
invoiceDate: '2026-12-12',
|
|
invoiceRef: 'SYNTHETIC QA factura',
|
|
legalDeadline: '2026-12-17',
|
|
deadlineRef:
|
|
'SYNTHETIC QA prazo/disposição/calendário identificados,não regra vigente presumida',
|
|
paymentState: t === 'advance_payment' ? 'received' : 'none',
|
|
paymentDate: '2026-12-09',
|
|
paymentRef: 'SYNTHETIC QA recebimento identificado',
|
|
amount: '1000.00',
|
|
paymentAmount: '200.00',
|
|
sourceCurrency: 'USD',
|
|
fxRate: '64.5',
|
|
fxDate: '2026-12-10',
|
|
fxSource: 'SYNTHETIC QA taxa/exemplar',
|
|
fxRuleRef: 'SYNTHETIC QA pressuposto da taxa/data,não base fiscal',
|
|
fxDirection: 'mzn_per_unit',
|
|
};
|
|
}
|
|
for (const rule of currencyEventTreatments)
|
|
void test(rule.branch + ' ' + rule.id + ' own event and noVAT/base', () => {
|
|
const r = row(rule.id),
|
|
v = evaluateCurrencyEvent(r);
|
|
assert.equal(
|
|
v.status,
|
|
rule.branch === 'P104' ? 'conditional' : 'determined',
|
|
);
|
|
assert.equal(v.baseAmount, null);
|
|
assert.equal(v.vat, null);
|
|
assert.equal(v.rate, null);
|
|
if (rule.branch === 'P104') assert.equal(v.scenarioAmount, '64500.00');
|
|
else {
|
|
assert.equal(
|
|
v.dueEvents.at(-1).date,
|
|
['import_clearance', 'import_auction'].includes(rule.id)
|
|
? '2026-12-10'
|
|
: '2026-12-12',
|
|
);
|
|
assert.equal(
|
|
v.eventDate,
|
|
rule.id === 'advance_payment'
|
|
? null
|
|
: rule.id === 'consignment_deadline'
|
|
? '2026-12-10'
|
|
: '2026-12-10',
|
|
);
|
|
}
|
|
for (const f of ['operationRef', 'documentRef', 'scope', 'evidence'])
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, [f]: '' }).status,
|
|
'indeterminate',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, factsConfirmed: '' }).status,
|
|
'indeterminate',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, factsConfirmed: 'no' }).status,
|
|
'not_applicable',
|
|
);
|
|
});
|
|
void test('P131/P132 invoice intime/late/absent,partial payment and no duplicate remainder', () => {
|
|
const r = row('goods_available');
|
|
let v = evaluateCurrencyEvent({ ...r, paymentState: 'received' });
|
|
assert.deepEqual(
|
|
v.dueEvents.map((x) => [x.date, x.amount]),
|
|
[
|
|
['2026-12-09', '200.00'],
|
|
['2026-12-12', '800.00'],
|
|
],
|
|
);
|
|
v = evaluateCurrencyEvent({ ...r, invoiceDate: '2026-12-20' });
|
|
assert.equal(v.dueEvents[0].date, '2026-12-17');
|
|
v = evaluateCurrencyEvent({ ...r, invoiceState: 'not_issued' });
|
|
assert.equal(v.dueEvents[0].date, '2026-12-17');
|
|
v = evaluateCurrencyEvent({
|
|
...r,
|
|
invoiceState: 'not_issued',
|
|
reviewDate: '2026-12-11',
|
|
paymentState: 'received',
|
|
});
|
|
assert.equal(v.dueEvents.length, 1);
|
|
assert.equal(v.remainingPending, true);
|
|
v = evaluateCurrencyEvent({
|
|
...r,
|
|
paymentState: 'received',
|
|
paymentDate: '2026-12-19',
|
|
invoiceDate: '2026-12-20',
|
|
});
|
|
assert.deepEqual(
|
|
v.dueEvents.map((x) => [x.date, x.amount]),
|
|
[['2026-12-17', '1000.00']],
|
|
);
|
|
v = evaluateCurrencyEvent({
|
|
...r,
|
|
paymentState: 'received',
|
|
paymentAmount: '1000',
|
|
});
|
|
assert.equal(v.dueEvents.length, 1);
|
|
assert.equal(v.dueEvents[0].amount, '1000.00');
|
|
for (const patch of [
|
|
{ paymentState: '' },
|
|
{ legalDeadline: '' },
|
|
{ deadlineRef: '' },
|
|
{ invoiceState: '' },
|
|
{ paymentState: 'received', paymentAmount: '1001' },
|
|
{ paymentState: 'received', amount: '' },
|
|
])
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, ...patch }).status,
|
|
'indeterminate',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, invoiceRequired: 'no', amount: '' })
|
|
.dueEvents[0].amount,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({
|
|
...r,
|
|
eventDate: '2026-12-20',
|
|
reviewDate: '2026-12-11',
|
|
invoiceState: 'not_issued',
|
|
legalDeadline: '2026-12-28',
|
|
paymentState: 'received',
|
|
}).dueEvents[0].date,
|
|
'2026-12-09',
|
|
);
|
|
});
|
|
void test('own events,consignmentterm,servicecharge,contractexceptions and dates', () => {
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...row('consignment_deadline'), otherDate: '' })
|
|
.status,
|
|
'indeterminate',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({
|
|
...row('consignment_deadline'),
|
|
invoiceRequired: 'no',
|
|
}).dueEvents[0].date,
|
|
'2026-12-10',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({
|
|
...row('service_early_charge'),
|
|
otherDate: '2026-12-09',
|
|
}).status,
|
|
'indeterminate',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({
|
|
...row('contract_effects'),
|
|
otherDate: '2026-12-11',
|
|
}).status,
|
|
'indeterminate',
|
|
);
|
|
for (const field of ['reviewDate', 'eventDate', 'documentDate'])
|
|
assert.equal(
|
|
evaluateCurrencyEvent({
|
|
...row('goods_available'),
|
|
[field]: '2026-02-30',
|
|
}).status,
|
|
'indeterminate',
|
|
);
|
|
const r = row('own_goods'),
|
|
sw = switchCurrencyEvent(r, 'foreign_scenario');
|
|
assert.equal(sw.eventDate, r.eventDate);
|
|
assert.equal(sw.fxSource, r.fxSource);
|
|
assert.equal(sw.factsConfirmed, '');
|
|
});
|
|
void test('P104 conditionalconversion direction/exactrounding/missingparams never fiscalbase', () => {
|
|
const r = row('foreign_scenario');
|
|
assert.equal(
|
|
evaluateCurrencyEvent({
|
|
...r,
|
|
fxDirection: 'units_per_mzn',
|
|
fxRate: '2',
|
|
amount: '100',
|
|
}).scenarioAmount,
|
|
'50.00',
|
|
);
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, amount: '0.000001', fxRate: '1.5' })
|
|
.scenarioAmount,
|
|
'0.000002',
|
|
);
|
|
for (const patch of [
|
|
{ fxRate: '0' },
|
|
{ fxRate: '-1' },
|
|
{ fxSource: '' },
|
|
{ fxDate: '' },
|
|
{ fxRuleRef: '' },
|
|
{ fxDirection: '' },
|
|
{ sourceCurrency: 'MZN' },
|
|
])
|
|
assert.equal(
|
|
evaluateCurrencyEvent({ ...r, ...patch }).status,
|
|
'indeterminate',
|
|
);
|
|
assert.equal(evaluateCurrencyEvent(r).baseAmount, null);
|
|
});
|
|
void test('snapshot/history/report preserve every delivered book and contextual scope', () => {
|
|
const rows = currencyEventTreatments.map((t) => row(t.id)),
|
|
a = recordCurrencyEvents(draft, { rows }),
|
|
next = { ...draft, modelo_a_snapshot: saveCurrencyEventBook(draft, a) },
|
|
b = recordCurrencyEvents(next, a);
|
|
next.modelo_a_snapshot = saveCurrencyEventBook(next, b);
|
|
assert.equal(b.history.length, 1);
|
|
assert.equal(currencyEventCurrent(next, b), true);
|
|
assert.equal(
|
|
currencyEventCurrent(next, { ...b, rows: rows.slice(1) }),
|
|
false,
|
|
);
|
|
assert.equal(currencyEventReportRows(next).length, 15);
|
|
assert.deepEqual(
|
|
currencyEventBook({ ...next, context_opportunity: 'opportunity:358' }),
|
|
{ rows: [] },
|
|
);
|
|
const store = JSON.parse(next.modelo_a_snapshot);
|
|
delete store.currencyEvents;
|
|
assert.deepEqual(store, JSON.parse(frozen.modelo_a_snapshot));
|
|
assert.throws(() =>
|
|
recordCurrencyEvents(next, { rows: [rows[0], rows[0]] }),
|
|
);
|
|
assert.throws(() =>
|
|
currencyEventBook({
|
|
...next,
|
|
modelo_a_snapshot: JSON.stringify({ ...store, currencyEvents: [] }),
|
|
}),
|
|
);
|
|
if (process.env.IVA_CURRENCY_EVENTS_FIXTURE)
|
|
fs.writeFileSync(
|
|
process.env.IVA_CURRENCY_EVENTS_FIXTURE,
|
|
JSON.stringify(next, null, 2) + '\n',
|
|
);
|
|
});
|