import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { emptyFxSelection, selectFx, fxTempoSource, } from '../../resources/js/lib/iva-fx-selection.ts'; import { newCurrencyEvent, evaluateCurrencyEvent, recordCurrencyEvents, saveCurrencyEventBook, currencyEventBook, currencyEventCurrent, } from '../../resources/js/lib/iva-currency-events.ts'; const selection = () => ({ ...emptyFxSelection(), nature: 'commercial', submissionDate: '2026-10-23', bulletinDate: '2026-10-23', bulletinRef: 'SYNTHETIC QA boletimBM de referência,23/10/2026', source: 'https://www.bancomoc.mz/pt/areas-de-actuacao/mercados/mercado-cambial/', sourceCurrency: 'USD', direction: 'mzn_per_unit', bulletinPerspective: 'SYNTHETIC QA bancos perante público', relation: 'same', buy: '63.27', sell: '64.54', mean: '63.91', }); for (const [nature, relation, column, rate] of [ ['purchase', 'same', 'buy', '63.27'], ['sale', 'same', 'sell', '64.54'], ['purchase', 'opposite', 'sell', '64.54'], ['sale', 'opposite', 'buy', '63.27'], ['commercial', '', 'mean', '63.91'], ]) void test(`P104 ${nature}/${relation || 'médiaBM'} escolhe ${column} sem inverter cliente`, () => { const result = selectFx({ ...selection(), nature, relation }); assert.equal(result.column, column); assert.equal(result.rate, rate); assert.equal(result.date, '2026-10-23'); }); void test('P104 falta de boletim/data/perspectiva não autoriza taxa actual,média própria ou coluna oposta presumida', () => { for (const patch of [ { bulletinDate: '2026-10-22' }, { mean: '' }, { source: 'https://example.com/cambio' }, { submissionDate: '2026-02-30' }, { nature: '' }, { nature: 'purchase', relation: '' }, { nature: 'sale', bulletinPerspective: '' }, ]) assert.throws(() => selectFx({ ...selection(), ...patch })); assert.equal( selectFx({ ...selection(), source: 'SYNTHETIC QA exemplar oficial arquivado', issuer: 'Banco de Moçambique', }).column, 'mean', ); const exception = { ...selection(), mode: 'exception', exceptionRef: 'SYNTHETIC QA disposição fiscal específica', exceptionScope: 'SYNTHETIC QA apenas operaçãoA', exceptionDate: '2026-10-20', exceptionRate: '62.12345678', }; assert.equal(selectFx(exception).rate, '62.12345678'); assert.equal(selectFx(exception).date, '2026-10-20'); assert.throws(() => selectFx({ ...exception, exceptionRef: '' })); }); const frozen = JSON.parse( fs.readFileSync( 'tests/frontend/fixtures/iva-offer-workflow.json', 'utf8', ), ), prior = JSON.parse(frozen.modelo_a_snapshot), draft = { ...frozen, context_opportunity: 'opportunity:365', project_title: 'SYNTHETIC QA — P104 operacional / Manica', }; function row() { const s = selection(), applied = selectFx(s); return { ...newCurrencyEvent('commercial', 'MZN'), treatment: 'foreign_scenario', description: 'SYNTHETIC QA operação comercial', period: '2026-10', origin: 'effective', operationRef: 'SYNTHETIC QA operação já declarada', documentRef: 'SYNTHETIC QA factura', documentDate: '2026-10-23', scope: 'SYNTHETIC QA operaçãoA', evidence: 'SYNTHETIC QA factos', factsConfirmed: 'yes', amount: '100.25', sourceCurrency: 'USD', fxRate: applied.rate, fxDate: applied.date, fxSource: applied.source, fxRuleRef: fxTempoSource, fxDirection: s.direction, fxSelection: { ...s, applied }, }; } void test('P104 original/convertido,direcção e taxa guardados;semIVA,nova dedução ou alteração automática', () => { const r = row(), v = evaluateCurrencyEvent(r); assert.equal(v.status, 'determined'); assert.equal(v.scenarioAmount, '6406.9775'); assert.equal(v.baseAmount, null); assert.equal(v.vat, null); const reverse = { ...r, fxDirection: 'units_per_mzn', fxSelection: { ...r.fxSelection, direction: 'units_per_mzn' }, }; const applied = selectFx(reverse.fxSelection); reverse.fxSelection.applied = applied; reverse.fxSource = applied.source; assert.equal(evaluateCurrencyEvent(reverse).scenarioAmount, '1.568612'); assert.equal( evaluateCurrencyEvent({ ...r, fxRate: '999' }).status, 'indeterminate', ); assert.equal( evaluateCurrencyEvent({ ...r, fxSelection: { ...r.fxSelection, submissionDate: '2026-10-24' }, }).status, 'indeterminate', ); }); void test('P104 versões antigas permanecem condicionadas e127fechos/P058/ofertas permanecem iguais', () => { const r = row(), legacy = { ...r }; delete legacy.fxSelection; assert.equal(evaluateCurrencyEvent(legacy).status, 'conditional'); let b = recordCurrencyEvents(draft, { rows: [r] }); const saved = structuredClone(b.result); const changed = { ...b, rows: [{ ...r, amount: '200' }] }; assert.equal(currencyEventCurrent(draft, changed), false); assert.deepEqual(b.result, saved); b = recordCurrencyEvents(draft, changed); assert.deepEqual(b.history[0], saved); draft.modelo_a_snapshot = saveCurrencyEventBook(draft, b); assert.deepEqual(currencyEventBook(draft), b); const next = JSON.parse(draft.modelo_a_snapshot); assert.deepEqual( Object.fromEntries( Object.entries(prior).filter(([k]) => k !== 'currencyEvents'), ), Object.fromEntries( Object.entries(next).filter(([k]) => k !== 'currencyEvents'), ), ); for (const [k, v] of Object.entries(prior.currencyEvents ?? {})) assert.deepEqual(next.currencyEvents[k], v); if (process.env.IVA_FX_SELECTION_FIXTURE) fs.writeFileSync( 'tests/frontend/fixtures/iva-fx-selection.json', JSON.stringify(draft), ); });