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

551 lines
19 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {
newTravel,
newTravelCost,
newTravelPeriod,
travelConditions,
travelCostKinds,
evaluateTravel,
recordTravel,
saveTravelBook,
travelBook,
travelCurrent,
travelReportRows,
switchTravelResources,
} from '../../resources/js/lib/iva-travel.ts';
import {
newTravelEvent,
evaluateTravelEvent,
recordTravelEvents,
saveTravelEventBook,
travelEventBook,
travelEventCurrent,
travelEventReportRows,
switchTravelEvent,
} from '../../resources/js/lib/iva-travel-events.ts';
const frozen = JSON.parse(
fs.readFileSync(
'tests/frontend/fixtures/iva-travel-events-preserved.json',
'utf8',
),
),
prior = JSON.parse(frozen.modelo_a_snapshot);
const draft = {
...frozen,
context_opportunity: 'opportunity:360',
project_title: 'SYNTHETIC QA — Viagens e eventos / Manica',
};
function cost(id, amount, kind = 'direct') {
return {
...newTravelCost(id),
kind,
description: 'SYNTHETIC QA ' + kind,
amount,
ref: 'SYNTHETIC QA documento/custo ' + id,
thirdParty: 'yes',
directBenefit: 'yes',
};
}
function trip(id, period, gross, costAmount, patch = {}) {
return {
...newTravel(id, 'MZN'),
description: 'SYNTHETIC QA viagem ' + id,
period,
operatorRef: 'SYNTHETIC QA operador1',
operationRef: 'SYNTHETIC QA op ' + id,
documentRef: 'SYNTHETIC QA contrato/registo',
documentDate: '2026-10-01',
scope: 'SYNTHETIC QA pacote/parcela/período identificado',
evidence: 'SYNTHETIC QA factos/pressupostos',
agencyCountry: 'MZ',
agencyRef: 'SYNTHETIC QA sede fornecedora MZ',
resources: 'third_party',
retailer: 'no',
grossPrice: gross,
domesticTaxable: 'yes',
taxRef: 'SYNTHETIC QA parcela tributável nacional/divisor17(1)',
conditions: Object.fromEntries(
travelConditions.map(([k]) => [k, 'yes']),
),
costs: [cost(id + '-cost', costAmount)],
...patch,
};
}
function period(id, month, operator = 'SYNTHETIC QA operador1', mode = 'none') {
return {
...newTravelPeriod(id),
period: month,
operatorRef: operator,
scope: 'SYNTHETIC QA registo completo do âmbito simulado',
complete: 'yes',
registerRef: 'SYNTHETIC QA livro/previsão completo',
priorMode: mode,
};
}
const trips = [
trip('A', '2026-10', '1160', '580'),
trip('B', '2026-10', '1160', '1740'),
trip('C', '2026-11', '1160', '2320'),
trip('D', '2026-12', '2320', '580'),
trip('mixed', '2026-12', '2320', '580', {
operatorRef: 'SYNTHETIC QA operador2',
resources: 'mixed',
thirdPartyGross: '1160',
allocationRef:
'SYNTHETIC QA repartição contratual sem percentagem presumida',
}),
trip('own', '2026-12', '1160', '0', { resources: 'own' }),
trip('intermediary', '2026-12', '1160', '0', {
conditions: { operator: 'yes', singlePrice: 'yes', ownName: 'no' },
}),
trip('retail', '2026-12', '1160', '0', {
retailer: 'yes',
newPackage: 'no',
}),
trip('retail_new', '2026-12', '1160', '580', {
operatorRef: 'SYNTHETIC QA operador4',
retailer: 'yes',
newPackage: 'yes',
packageRef: 'SYNTHETIC QA conjunto novo/serviços substanciais',
}),
trip('foreign', '2026-12', '1160', '580', { agencyCountry: 'foreign' }),
];
trips[0].costs.push(
...travelCostKinds
.filter(([k]) =>
[
'exchange',
'advertising',
'communications',
'sales_commission',
].includes(k),
)
.map(([kind], i) => cost('A-' + kind, String((i + 1) * 10), kind)),
);
const periods = [
period('oct', '2026-10'),
period('nov', '2026-11', undefined, 'previous'),
period('dec', '2026-12', undefined, 'previous'),
period('mixed', '2026-12', 'SYNTHETIC QA operador2'),
period('retail_new', '2026-12', 'SYNTHETIC QA operador4'),
];
let travel = recordTravel(draft, { rows: trips, periods });
draft.modelo_a_snapshot = saveTravelBook(draft, travel);
for (const row of trips)
void test('P129 ' + row.id + ' regime and component scope', () => {
const v = evaluateTravel(row, 'MZN');
assert.equal(
v.regime,
row.id === 'mixed'
? 'mixed'
: ['own', 'intermediary', 'retail'].includes(row.id)
? 'general'
: row.id === 'foreign'
? 'outside'
: 'special',
);
assert.equal(
v.status,
['own', 'intermediary', 'retail', 'foreign'].includes(row.id)
? 'not_applicable'
: 'determined',
);
if (row.id === 'mixed') {
assert.equal(v.specialGross, '1160.00');
assert.equal(v.generalGross, '1160.00');
}
});
void test('P129 period-global margin offsets package gains/losses; excludes four legal costs; no individual-margin aggregation', () => {
const p = travel.result.periods;
assert.equal(p[0].grossReceipts, '2320.00');
assert.equal(p[0].directCosts, '2320.00');
assert.equal(p[0].excludedCosts, '100.00');
assert.equal(p[0].baseAmount, '0.00');
assert.equal(p[0].carryForward, '0.00');
assert.equal(p[1].grossDifference, '-1160.00');
assert.equal(p[1].carryForward, '1160.00');
assert.equal(p[2].priorExcess, '1160.00');
assert.equal(p[2].baseAmount, '500.00');
assert.equal(p[2].carryForward, '0.00');
assert.equal(p[3].baseAmount, '500.00');
for (const x of p) {
assert.equal(x.vat, null);
assert.equal(x.deduction, null);
assert.equal(x.baseDivisorRate, '16%');
}
});
void test('P129 unknown amounts, scope, cost benefit, currency and previous excess do not become zero', () => {
for (const patch of [
{ grossPrice: '' },
{ currency: 'USD' },
{ taxRef: '' },
{ domesticTaxable: '' },
{ agencyCountry: '' },
{ documentDate: '2026-02-30' },
{ operatorRef: '' },
{ resources: '' },
{ conditions: { operator: 'yes', singlePrice: '', ownName: 'yes' } },
{ costs: [{ ...trips[0].costs[0], directBenefit: '' }] },
])
assert.equal(
evaluateTravel({ ...trips[0], ...patch }, 'MZN').status,
'indeterminate',
);
for (const patch of [
{ complete: 'no' },
{ priorMode: '' },
{ priorMode: 'previous' },
{ registerRef: '' },
])
assert.equal(
recordTravel(draft, {
rows: [trips[0]],
periods: [{ ...periods[0], ...patch }],
}).result.periods[0].status,
'indeterminate',
);
assert.equal(
evaluateTravel({ ...trips[4], thirdPartyGross: '9999' }, 'MZN').status,
'indeterminate',
);
assert.equal(
evaluateTravel({ ...trips[8], packageRef: '' }, 'MZN').status,
'indeterminate',
);
const unknown = { ...trips[1], grossPrice: '' };
assert.equal(
recordTravel(draft, {
rows: [trips[0], unknown],
periods: [periods[0]],
}).result.periods[0].status,
'indeterminate',
);
const both = { ...trips[1], origin: 'effective' };
assert.equal(
recordTravel(draft, { rows: [trips[0], both], periods: [periods[0]] })
.result.periods[0].status,
'indeterminate',
);
});
void test('P129 explicit prior excess and nearest-unit rounding; cost exclusions never become deduction', () => {
const t = { ...trips[0], grossPrice: '1.75', costs: [] },
p = {
...periods[0],
priorMode: 'manual',
priorAmount: '0',
priorRef: 'SYNTHETIC QA fecho anterior sem excesso',
};
assert.equal(
recordTravel(draft, { rows: [t], periods: [p] }).result.periods[0]
.baseAmount,
'2.00',
);
const indirect = { ...trips[0], costs: [cost('indirect', '500', 'other')] };
indirect.costs[0].directBenefit = 'no';
assert.equal(evaluateTravel(indirect, 'MZN').eligibleCosts, '0.00');
assert.equal(evaluateTravel(indirect, 'MZN').excludedCosts, '500.00');
const correction = {
...periods[2],
priorMode: 'manual',
priorAmount: '500',
priorRef: 'SYNTHETIC QA correcção referenciada',
};
const pending = recordTravel(draft, {
rows: trips,
periods: [periods[0], periods[1], correction],
});
assert.equal(pending.result.periods[2].status, 'indeterminate');
assert.match(pending.result.periods[2].reason, /Nada foi sobrescrito/);
const confirmed = recordTravel(draft, {
rows: trips,
periods: [
periods[0],
periods[1],
{ ...correction, priorOverride: 'yes' },
],
});
assert.equal(confirmed.result.periods[2].priorExcess, '500.00');
assert.equal(confirmed.result.periods[2].baseAmount, '1069.00');
});
function event(id, patch = {}) {
return {
...newTravelEvent(id, 'MZN'),
treatment: 'travel_special',
travelRowId: 'A',
description: 'SYNTHETIC QA evento ' + id,
period: '2026-12',
operationRef: trips[0].operationRef,
documentRef: 'SYNTHETIC QA documento evento',
documentDate: '2026-12-01',
reviewDate: '2026-12-31',
scope: 'SYNTHETIC QA parcela viagem/bens identificados',
evidence: 'SYNTHETIC QA factos/pressupostos',
factsConfirmed: 'yes',
fullPaymentState: 'full',
fullPaymentDate: '2026-12-05',
fullPaymentRef: 'SYNTHETIC QA liquidação integral',
startState: 'started',
startDate: '2026-12-10',
startRef: 'SYNTHETIC QA primeiro serviço',
invoiceRequired: 'no',
paymentState: 'received',
paymentDate: '2026-12-05',
paymentAmount: '1160',
paymentRef: 'SYNTHETIC QA recebimento',
amount: '1160',
...patch,
};
}
const events = [
event('full_first'),
event('start_first', {
fullPaymentDate: '2026-12-20',
paymentDate: '2026-12-20',
}),
event('same_day', {
fullPaymentDate: '2026-12-10',
paymentDate: '2026-12-10',
}),
event('partial_8c', {
fullPaymentState: 'partial',
startState: 'not_started',
paymentAmount: '232',
invoiceRequired: 'yes',
invoiceState: 'not_issued',
legalDeadline: '2026-12-20',
deadlineRef:
'SYNTHETIC QA prazo identificado,sem cinco dias por defeito',
reviewDate: '2026-12-10',
}),
event('partial_then_full', {
fullPaymentDate: '2026-12-08',
paymentAmount: '232',
invoiceRequired: 'yes',
invoiceState: 'issued',
invoiceDate: '2026-12-15',
invoiceRef: 'SYNTHETIC QA factura',
legalDeadline: '2026-12-20',
deadlineRef: 'SYNTHETIC QA prazo identificado',
}),
event('invoice_before_start', {
fullPaymentState: 'none',
startState: 'not_started',
paymentState: 'none',
invoiceRequired: 'yes',
invoiceState: 'issued',
invoiceDate: '2026-12-02',
invoiceRef: 'SYNTHETIC QA factura antecipada',
legalDeadline: '2026-12-20',
deadlineRef: 'SYNTHETIC QA prazo identificado',
}),
event('travel_deferred', {
fullPaymentState: 'none',
startState: 'not_started',
paymentState: 'none',
}),
];
for (const clause of ['a', 'b', 'c', 'd'])
events.push(
event('exit_' + clause, {
treatment: 'suspensive_exit',
travelRowId: '',
regimeClause: clause,
entryDate: '2026-11-01',
entryRef: 'SYNTHETIC QA colocação regime/bens',
regimeStatus: 'ceased',
exitDate: '2026-12-10',
exitRef: 'SYNTHETIC QA cessação efectiva',
cessationConfirmed: 'yes',
}),
);
events.push({ ...events[7], id: 'still_held', regimeStatus: 'still' });
for (const row of events)
void test(
'P133 ' + row.id + ' special event separate from tax amounts',
() => {
const v = evaluateTravelEvent(draft, row);
assert.equal(
v.status,
['travel_deferred', 'still_held'].includes(row.id)
? 'deferred'
: 'determined',
);
assert.equal(v.baseAmount, null);
assert.equal(v.vat, null);
assert.equal(v.deduction, null);
if (row.id.startsWith('exit_')) {
assert.equal(v.eventDate, '2026-12-10');
assert.equal(v.dueEvents[0].amount, null);
}
},
);
void test('P133 full/start timing and partial/full/early invoice never duplicate amounts', () => {
assert.equal(evaluateTravelEvent(draft, events[0]).eventDate, '2026-12-05');
assert.equal(evaluateTravelEvent(draft, events[1]).eventDate, '2026-12-10');
assert.match(
evaluateTravelEvent(draft, events[2]).eventTiming,
/ordem intradiária não apurada/,
);
const early = evaluateTravelEvent(draft, events[3]);
assert.equal(early.eventDate, null);
assert.deepEqual(
early.dueEvents.map((x) => [x.date, x.amount]),
[['2026-12-05', '232.00']],
);
assert.equal(early.remainingPending, true);
const full = evaluateTravelEvent(draft, events[4]);
assert.equal(full.eventDate, '2026-12-08');
assert.deepEqual(
full.dueEvents.map((x) => [x.date, x.amount]),
[
['2026-12-05', '232.00'],
['2026-12-08', '928.00'],
],
);
assert.equal(full.remainingPending, false);
const invoice = evaluateTravelEvent(draft, events[5]);
assert.equal(invoice.eventDate, null);
assert.equal(invoice.dueEvents[0].date, '2026-12-02');
const invoiceFirst = { ...events[4], invoiceDate: '2026-12-03' };
assert.deepEqual(
evaluateTravelEvent(draft, invoiceFirst).dueEvents.map((x) => [
x.date,
x.amount,
]),
[['2026-12-03', '1160.00']],
);
});
void test('P133 absence, conflicting facts, future/invalid dates and regime transfer do not invent events', () => {
for (const patch of [
{ travelRowId: '' },
{ operationRef: 'another' },
{ fullPaymentState: '' },
{ startState: '' },
{ fullPaymentState: 'partial', paymentAmount: '1160' },
{ fullPaymentDate: '2026-12-99' },
{ startDate: '2027-01-01' },
{ fullPaymentState: 'none', paymentState: 'received' },
{ factsConfirmed: '' },
])
assert.equal(
evaluateTravelEvent(draft, { ...events[0], ...patch }).status,
'indeterminate',
);
for (const patch of [
{ regimeClause: '' },
{ entryRef: '' },
{ entryDate: '2026-12-20' },
{ regimeStatus: '' },
{ cessationConfirmed: 'no' },
{ exitRef: '' },
{ exitDate: '2027-01-01' },
])
assert.equal(
evaluateTravelEvent(draft, { ...events[7], ...patch }).status,
'indeterminate',
);
assert.equal(
evaluateTravelEvent(
{ ...draft, context_opportunity: 'opportunity:361' },
events[0],
).status,
'indeterminate',
);
assert.equal(
evaluateTravelEvent(draft, { ...events[0], travelRowId: 'own' }).status,
'not_applicable',
);
});
void test('P133 classification dependency is guarded; unknown margin amounts do not block an independently documented event', () => {
const changed = {
...travel,
rows: travel.rows.map((r) =>
r.id === 'A' ? { ...r, grossPrice: '' } : r,
),
},
d = { ...draft, modelo_a_snapshot: saveTravelBook(draft, changed) };
assert.equal(evaluateTravelEvent(d, events[0]).status, 'indeterminate');
d.modelo_a_snapshot = saveTravelBook(d, recordTravel(d, changed));
assert.equal(evaluateTravelEvent(d, events[0]).status, 'determined');
const b = recordTravelEvents(draft, { rows: events }),
fresh = { ...draft, modelo_a_snapshot: saveTravelEventBook(draft, b) };
assert.equal(travelEventCurrent(fresh, b), true);
fresh.modelo_a_snapshot = saveTravelBook(
fresh,
recordTravel(fresh, travel),
);
assert.equal(travelEventCurrent(fresh, b), false);
assert.equal(b.result.rows[0].source.revision, 1);
});
void test('Private save/recovery/history/report preserve123 delivered books, digital limitation and partial drafts', () => {
const t = recordTravel(draft, travel),
d = { ...draft, modelo_a_snapshot: saveTravelBook(draft, t) },
a = recordTravelEvents(d, { rows: events });
d.modelo_a_snapshot = saveTravelEventBook(d, a);
const b = recordTravelEvents(d, a);
d.modelo_a_snapshot = saveTravelEventBook(d, b);
assert.equal(t.history.length, 1);
assert.equal(b.history.length, 1);
assert.equal(travelReportRows(d).length, 15);
assert.equal(travelEventReportRows(d).length, 12);
assert.equal(travelCurrent(d, t), true);
assert.equal(travelEventCurrent(d, b), true);
const report = travelEventReportRows(d);
assert.doesNotMatch(report[3][3], /SYNTHETIC QA liquidação integral/);
assert.doesNotMatch(report[3][3], /SYNTHETIC QA primeiro serviço/);
assert.doesNotMatch(report[11][3], /SYNTHETIC QA cessação efectiva/);
assert.equal(b.rows[3].fullPaymentRef, 'SYNTHETIC QA liquidação integral');
assert.equal(b.rows[11].exitRef, 'SYNTHETIC QA cessação efectiva');
const store = JSON.parse(d.modelo_a_snapshot);
delete store.travelOperations;
delete store.travelSpecialEvents;
assert.deepEqual(store, prior);
assert.deepEqual(
travelBook({ ...d, context_opportunity: 'opportunity:361' }),
{ rows: [], periods: [] },
);
assert.deepEqual(
travelEventBook({ ...d, context_opportunity: 'opportunity:361' }),
{ rows: [] },
);
assert.throws(() =>
recordTravel(d, { rows: [trips[0], trips[0]], periods }),
);
assert.throws(() =>
recordTravelEvents(d, { rows: [events[0], events[0]] }),
);
assert.throws(() =>
travelBook({
...d,
modelo_a_snapshot: JSON.stringify({
...store,
travelOperations: [],
}),
}),
);
assert.throws(() =>
travelEventBook({
...d,
modelo_a_snapshot: JSON.stringify({
...store,
travelSpecialEvents: [],
}),
}),
);
const switched = switchTravelResources(trips[4], 'own');
assert.equal(switched.allocationRef, trips[4].allocationRef);
assert.deepEqual(switched.costs, trips[4].costs);
assert.equal(switched.conditions.singlePrice, '');
assert.equal(
switchTravelEvent(events[0], 'suspensive_exit').fullPaymentRef,
events[0].fullPaymentRef,
);
if (process.env.IVA_TRAVEL_EVENTS_FIXTURE)
fs.writeFileSync(
process.env.IVA_TRAVEL_EVENTS_FIXTURE,
JSON.stringify(d, null, 2) + '\n',
);
});