mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 05:23:38 +00:00
CbeExchangeProvider now parses every currency CBE quotes (USD, DJF, ...) from the single existing daily-rates fetch instead of hardcoding USD only, and ExchangeService.getRate gains a pivot step so a pair neither quoted directly nor as its inverse (e.g. USD->DJF) is derived by triangulating through the provider's base currency (ETB). Fallback rates and the load/save callbacks become per-currency instead of a single USD->ETB scalar. ExchangeService.getRateTable resolves a whole currency->target rate table in one call for pricing loops. No behavior change for existing USD/ETB callers. Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import { Inject, Injectable } from "@nestjs/common";
|
|
|
|
import { CbeExchangeProvider, CbeProviderStatus } from "./cbe.provider";
|
|
import { EXCHANGE_OPTIONS, ExchangeOptions } from "./exchange.options";
|
|
import { CURRENCY_CODES, CurrencyCode } from "./exchange.types";
|
|
|
|
/**
|
|
* Currency exchange service. Resolves the rate between any supported currency
|
|
* pair and converts amounts, backed by a rate provider (currently CBE).
|
|
*
|
|
* Resolution order for `getRate(from, to)`:
|
|
* 1. `from === to` → `1`.
|
|
* 2. Provider supplies the pair directly (e.g. CBE → USD→ETB).
|
|
* 3. Provider supplies the inverse → return `1 / inverse` (e.g. ETB→USD).
|
|
* 4. Neither leg is quoted directly (e.g. USD→DJF) → pivot through the
|
|
* provider's base currency, which quotes both.
|
|
*
|
|
* Configure via {@link ExchangeModule.forRoot} / `forRootAsync`.
|
|
*/
|
|
@Injectable()
|
|
export class ExchangeService {
|
|
private readonly provider: CbeExchangeProvider;
|
|
|
|
constructor(@Inject(EXCHANGE_OPTIONS) options: ExchangeOptions) {
|
|
this.provider = new CbeExchangeProvider(options);
|
|
}
|
|
|
|
/**
|
|
* Returns the rate to convert 1 unit of `from` into `to`
|
|
* (i.e. `amountInTo = amountInFrom * getRate(from, to)`).
|
|
*/
|
|
async getRate(from: CurrencyCode, to: CurrencyCode): Promise<number> {
|
|
if (from === to) {
|
|
return 1;
|
|
}
|
|
|
|
const direct = await this.provider.getBaseRate({ from, to });
|
|
if (direct !== null) {
|
|
return direct;
|
|
}
|
|
|
|
const inverse = await this.provider.getBaseRate({ from: to, to: from });
|
|
if (inverse !== null && inverse > 0) {
|
|
return 1 / inverse;
|
|
}
|
|
|
|
// Neither leg is quoted directly (e.g. USD↔DJF): pivot through the
|
|
// provider's base currency, which quotes both. Mathematically identical
|
|
// to converting via that base currency by hand.
|
|
const base = this.provider.baseCurrency;
|
|
if (from !== base && to !== base) {
|
|
const fromToBase = await this.provider.getBaseRate({ from, to: base });
|
|
const toToBase = await this.provider.getBaseRate({ from: to, to: base });
|
|
if (fromToBase !== null && toToBase !== null && toToBase > 0) {
|
|
return fromToBase / toToBase;
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`No exchange rate available for ${from}→${to} from provider ${this.provider.name}`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A conversion function for every source currency into `target`, resolved
|
|
* up front so a pricing loop never awaits per row. `fx(code)` is `1` for
|
|
* `code === target`, throws for a currency the provider cannot rate.
|
|
*/
|
|
async getRateTable(
|
|
target: CurrencyCode,
|
|
sources: readonly CurrencyCode[] = CURRENCY_CODES,
|
|
): Promise<Record<string, number>> {
|
|
const entries = await Promise.all(
|
|
sources.map(async (code) => [code, await this.getRate(code, target)] as const),
|
|
);
|
|
return Object.fromEntries(entries);
|
|
}
|
|
|
|
/**
|
|
* Health of the underlying rate feed — what was served last and whether it
|
|
* is currently failing. For operator-facing status displays.
|
|
*/
|
|
getProviderStatus(code: CurrencyCode = "USD"): CbeProviderStatus {
|
|
return this.provider.getStatus(code);
|
|
}
|
|
|
|
/** Converts `amount` from one currency to another using {@link getRate}. */
|
|
async convert(
|
|
amount: number,
|
|
from: CurrencyCode,
|
|
to: CurrencyCode,
|
|
): Promise<number> {
|
|
const rate = await this.getRate(from, to);
|
|
return amount * rate;
|
|
}
|
|
|
|
/**
|
|
* Convenience alias for `getRate('USD', 'ETB')`.
|
|
* @deprecated Prefer {@link getRate}; kept for existing callers.
|
|
*/
|
|
getUsdToEtbRate(): Promise<number> {
|
|
return this.getRate("USD", "ETB");
|
|
}
|
|
|
|
/** Convenience alias for `getRate('ETB', 'USD')`. */
|
|
getEtbToUsdRate(): Promise<number> {
|
|
return this.getRate("ETB", "USD");
|
|
}
|
|
}
|