Files
edr-platform/apps/edr-freight-api/src/modules/exchange-settings/exchange-multi-currency.spec.ts
ghost2023 21dc28a708 feat(exchange-settings): one fallback rate per currency
exchange_settings was a single row holding the USD->ETB fallback only.
Restructured to one row per foreign currency (adds a currency column,
migration 3850000000000) so DJF gets its own fallback rate, source and
sync timestamp instead of a parallel column. Service/controller/DTO
follow: get/loadFallbackRate/saveFallbackRate/setManualRate all take a
currency now, GET /exchange-settings returns the list, and
PATCH /exchange-settings/:currency sets one. Per-currency manual-rate
ceiling (USD ~10,000, DJF ~100) replaces the old fixed bound.

Adds a spec exercising the multi-currency CBE parse and the USD<->DJF
pivot against a fixture payload.

Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
2026-09-04 11:52:18 +03:00

103 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CbeExchangeProvider, ExchangeService } from '@edr/api-common';
/**
* The CBE feed quotes every currency it publishes against ETB in one fetch —
* this is a fixture of that shape (trimmed to USD + DJF, the two the app
* actually reads). Verified live against the real feed on 2026-09-04.
*/
const CBE_FIXTURE = [
{
Date: '2026-09-04',
ExchangeRate: [
{
transactionalSelling: 163.4365,
transactionalBuying: 160.2319,
currency: { CurrencyCode: 'USD' },
},
{
transactionalSelling: 0.9203,
transactionalBuying: 0.9022,
currency: { CurrencyCode: 'DJF' },
},
// CBE publishes 0 for a currency it isn't quoting cash-selling that
// day — must not be picked up as a usable rate.
{ transactionalSelling: 0, currency: { CurrencyCode: 'ZZZ' } },
],
},
];
function mockFetchOnce(payload: unknown): jest.Mock {
const fn = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(payload),
});
(global as unknown as { fetch: typeof fetch }).fetch = fn as never;
return fn;
}
describe('CbeExchangeProvider — multi-currency', () => {
it('parses every quoted currency out of one fetch, not just USD', async () => {
const fetchMock = mockFetchOnce(CBE_FIXTURE);
const provider = new CbeExchangeProvider({});
const usdToEtb = await provider.getBaseRate({ from: 'USD', to: 'ETB' });
const djfToEtb = await provider.getBaseRate({ from: 'DJF', to: 'ETB' });
expect(usdToEtb).toBeCloseTo(163.4365);
expect(djfToEtb).toBeCloseTo(0.9203);
// Both rates came from the SAME cached fetch — one HTTP call serves
// every currency, not one per currency.
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('skips a currency CBE reports as 0 (unquoted that day) — throws with no fallback configured', async () => {
mockFetchOnce(CBE_FIXTURE);
const provider = new CbeExchangeProvider({});
await expect(provider.getBaseRate({ from: 'ZZZ' as never, to: 'ETB' })).rejects.toThrow(
/No CBE rate available for ZZZ/,
);
});
it('only ever answers for X→ETB — everything else is derived upstream', async () => {
mockFetchOnce(CBE_FIXTURE);
const provider = new CbeExchangeProvider({});
await expect(provider.getBaseRate({ from: 'ETB', to: 'USD' })).resolves.toBeNull();
await expect(provider.getBaseRate({ from: 'USD', to: 'DJF' })).resolves.toBeNull();
});
});
describe('ExchangeService — USD↔DJF pivot', () => {
it('derives USD→DJF by pivoting through ETB, the providers base currency', async () => {
mockFetchOnce(CBE_FIXTURE);
const service = new ExchangeService({});
const rate = await service.getRate('USD', 'DJF');
// 163.4365 / 0.9203 — same arithmetic as converting via ETB by hand.
expect(rate).toBeCloseTo(163.4365 / 0.9203, 4);
expect(rate).toBeCloseTo(177.59, 1);
});
it('derives the inverse, DJF→USD, from the same pivot', async () => {
mockFetchOnce(CBE_FIXTURE);
const service = new ExchangeService({});
const rate = await service.getRate('DJF', 'USD');
expect(rate).toBeCloseTo(0.9203 / 163.4365, 6);
});
it('getRateTable resolves every supported currency into the target in one call', async () => {
mockFetchOnce(CBE_FIXTURE);
const service = new ExchangeService({});
const fx = await service.getRateTable('DJF');
expect(fx.DJF).toBe(1);
expect(fx.USD).toBeCloseTo(163.4365 / 0.9203, 4);
expect(fx.ETB).toBeCloseTo(1 / 0.9203, 4);
});
});