# `OpenExchangeRatesProvider` _(no submodules)_ ## Purpose `OpenExchangeRatesProvider` is the `@suaveplan/currency` provider for [openexchangerates.org](https://openexchangerates.org). It wraps the `/latest.json` bulk-fetch endpoint, validates the upstream payload, and surfaces every failure mode through the typed `CurrencyProviderError` hierarchy from `@suaveplan/currency-providers`. The provider is universal — it relies only on `globalThis.fetch` (or an injected `FetchLike`) and the slim-core `httpRequest()` helper, so it runs unchanged in Node, Bun, browsers, and Web Workers. ## Features - Implements `CurrencyProvider` with `name === "open-exchange-rates"`, `supportsFiat === true`, `supportsCrypto === false`. - Constructor takes the OXR `app_id` as `apiKey`. Empty string raises `CurrencyProviderError(MISSING_CONFIG)` synchronously. - Free-tier safe by default: any `RateFetchOptions.base !== "USD"` raises `CurrencyProviderError(UNSUPPORTED_BASE)` before any network call. Paid-plan callers opt in with `allowNonUsdBase: true`. - Routes every request through `@suaveplan/currency-providers` `httpRequest()` so non-2xx responses surface `CurrencyProviderError(HTTP_ERROR)` with `status` and `providerName` populated. Caller `AbortSignal` from `RateFetchOptions.signal` is forwarded. - Validates the upstream `{ base, rates, timestamp }` shape. Mismatches raise `CurrencyProviderError(PARSE_ERROR)`. - Detects the OXR 2xx error-payload shape (`{ error: true, status, message }`) and raises `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage` carrying the upstream `message` field. - Filters the rates map to `RateFetchOptions.targets`. Missing target codes raise `CurrencyProviderError(UNKNOWN_CURRENCY)`. - Converts the OXR Unix-seconds timestamp to milliseconds. - Injectable `FetchLike` for offline testing; defaults to `globalThis.fetch`. ## Usage: Basic Example ```typescript import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates"; const provider = new OpenExchangeRatesProvider({ apiKey: process.env.OXR_APP_ID ?? "", }); const result = await provider.fetchRates({ base: "USD" }); console.log(result.base, result.rates.EUR, result.timestamp); ``` ## Usage: Advanced Example Wiring the provider into `CurrencyConverter` with a paid-tier app_id, custom `baseUrl` for staging, and `AbortSignal`-driven cancellation: ```typescript import { CurrencyConverter } from "@suaveplan/currency"; import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates"; const oxr = new OpenExchangeRatesProvider({ apiKey: process.env.OXR_APP_ID!, allowNonUsdBase: true, baseUrl: process.env.OXR_BASE_URL ?? "https://openexchangerates.org/api", }); const converter = new CurrencyConverter({ provider: oxr, staleDurationMs: 30 * 60 * 1000, // 30 minutes }); const controller = new AbortController(); setTimeout(() => controller.abort(new Error("timeout")), 5000); const rates = await oxr.fetchRates({ base: "EUR", targets: ["USD", "GBP", "JPY"], signal: controller.signal, }); ``` The slim-core `CurrencyProviderRegistry` is also supported when the consumer wires multiple providers behind a single lookup surface — see `@suaveplan/currency-providers` README. ## API Reference ### `OpenExchangeRatesProviderConfig` Extends `ApiKeyConfig` from `@suaveplan/currency-providers`: - `apiKey: string` — required. The OXR `app_id`. - `baseUrl?: string` — defaults to `"https://openexchangerates.org/api"`. - `fetch?: FetchLike` — defaults to `globalThis.fetch`. - `allowNonUsdBase?: boolean` — defaults to `false`. When `false`, any non-USD base raises `UNSUPPORTED_BASE` before any network call. ### `class OpenExchangeRatesProvider` #### Properties - `readonly name: "open-exchange-rates"` - `readonly supportsFiat: true` - `readonly supportsCrypto: false` #### `constructor(config: OpenExchangeRatesProviderConfig)` Throws `CurrencyProviderError(MISSING_CONFIG)` when `config.apiKey === ""`. #### `fetchRates(options?: RateFetchOptions): Promise` Issues `GET ${baseUrl}/latest.json?app_id=&base=` via `httpRequest()`. Returns `{ base, rates, timestamp }` with `timestamp` in milliseconds. Throws: - `MISSING_CONFIG` — from the constructor when `apiKey === ""`. - `UNSUPPORTED_BASE` — when `options.base !== "USD"` and `allowNonUsdBase` is not set to `true`. - `HTTP_ERROR` — via `httpRequest()` for any non-2xx response. - `PARSE_ERROR` — when the response body is not JSON, not an object, or is missing one of `base` / `rates` / `timestamp` in the expected shape. - `UPSTREAM_ERROR` — when the response is 2xx but the body is the OXR error shape (`{ error: true, status, message }`). - `UNKNOWN_CURRENCY` — when `options.targets` contains a code not present in the upstream `rates` map. ## Implementation Notes - The OXR free tier restricts the base currency to USD; the provider enforces this client-side so the failure surfaces as a typed `UNSUPPORTED_BASE` rather than a generic upstream error. Paid plans pass `allowNonUsdBase: true` to opt out. - OXR returns `timestamp` in Unix seconds; the provider multiplies by 1000 so every `CurrencyProvider` answer is in milliseconds, matching `RateFetchResult.timestamp`'s contract. - OXR's error-payload shape is returned with a 2xx HTTP status, which would otherwise pass `httpRequest()`'s non-2xx check silently. The provider detects `{ error: true, … }` after parsing and surfaces `UPSTREAM_ERROR` with `status` and `upstreamMessage` populated. - The provider does not retry, dedupe, or rate-limit by itself — the slim core's `## Out of scope` defers those concerns to a cross-cutting follow-up wave so every sibling provider gains them in lockstep. - `apiKey` is `encodeURIComponent`-wrapped in the query string to prevent injection when callers pass keys containing reserved characters.