# FixerProvider _(no submodules)_ ## Purpose `FixerProvider` is the [Fixer.io](https://fixer.io/) sibling provider for the slim `@suaveplan/currency-providers` core. It implements the `CurrencyProvider` contract from `@suaveplan/currency`, fetches the latest fiat exchange rates from the Fixer `/latest` REST endpoint, and maps the upstream JSON envelope into the canonical `RateFetchResult` shape the converter expects. Fixer.io requires an API key on every request. Free-tier accounts are restricted to `base = "EUR"` and a fixed monthly request quota; paid plans unlock arbitrary base currencies and higher throughput. The provider surfaces the free-tier restriction as a typed `UNSUPPORTED_BASE` error so consumers can dispatch on the discriminator rather than parsing the upstream message string. ## Features - Implements `CurrencyProvider` with stable identity flags (`name = "fixer"`, `supportsFiat = true`, `supportsCrypto = false`). - Constructor takes the slim core's shared `ApiKeyConfig` (`apiKey`, optional `baseUrl`, optional injectable `fetch`). An empty `apiKey` raises `CURRENCY_PROVIDER.MISSING_CONFIG` synchronously before any network call is issued, preventing wasted quota. - Delegates HTTP transport to the slim core's `httpRequest` helper — `AbortSignal` composition, non-2xx → typed `HTTP_ERROR` mapping, and `providerName` / `status` carry on the raised error. - Maps the upstream `{ success: false, error: { code, info } }` envelope to typed codes: `105` → `UNSUPPORTED_BASE`, `201` / `202` → `UNKNOWN_CURRENCY`, any other code → `UPSTREAM_ERROR`. The upstream `error.info` is preserved on `upstreamMessage` so diagnostics survive. - Distinguishes parse failures (non-JSON, non-object JSON, success envelope missing required fields) under a single `PARSE_ERROR` code. - Universal — no `node:` / `bun:` / DOM imports; runs unchanged under Node 18+, Bun, modern browsers, and Web Workers. ## Usage: Basic ```ts import { FixerProvider } from "@suaveplan/currency-provider-fixer"; const provider = new FixerProvider({ apiKey: process.env.FIXER_KEY! }); // Free-tier: base is implicitly EUR. const result = await provider.fetchRates(); console.log(result.base, result.rates.GBP, result.timestamp); ``` ## Usage: Advanced Wire the provider through the slim core's `CurrencyProviderRegistry` and use it as the primary source for a `CurrencyConverter`. The injected `FetchLike` makes the provider trivial to unit-test against a fixture fetch closure: ```ts import { CurrencyConverter } from "@suaveplan/currency"; import { CurrencyProviderRegistry } from "@suaveplan/currency-providers"; import { FixerProvider } from "@suaveplan/currency-provider-fixer"; const registry = new CurrencyProviderRegistry(); registry.register( new FixerProvider({ apiKey: process.env.FIXER_KEY!, // Optional: proxy through your own gateway for caching / auditing. baseUrl: "https://rates.internal/fixer/api", // Optional: inject a wrapped fetch for retries or logging. fetch: async (url, init) => { const response = await globalThis.fetch(url, init); return response; }, }), ); const converter = new CurrencyConverter({ provider: registry.get("fixer"), staleDurationMs: 3_600_000, }); // Free-tier accounts MUST keep base = "EUR" — supplying any other base // raises CurrencyProviderError(UNSUPPORTED_BASE) via the typed mapping // from upstream error code 105. const rate = await converter.getRate("EUR", "GBP"); ``` ## API Reference ### `class FixerProvider implements CurrencyProvider` - `readonly name = "fixer"` - `readonly supportsCrypto = false` - `readonly supportsFiat = true` #### `constructor(config: ApiKeyConfig)` Validates `config.apiKey` is non-empty (throws `CurrencyProviderError(MISSING_CONFIG)` otherwise) and caches `config.baseUrl ?? "https://data.fixer.io/api"` and `config.fetch ?? globalThis.fetch`. **Throws**: `CurrencyProviderError(MISSING_CONFIG)` when `apiKey === ""`. #### `fetchRates(options?: RateFetchOptions): Promise` Builds `?access_key=…&base=…[&symbols=…]`, delegates to the slim core's `httpRequest`, parses the JSON envelope, and returns the canonical `RateFetchResult` with `timestamp` converted from Unix seconds to milliseconds. **Parameters** (all optional): `base` (default `"EUR"`), `targets` (array of currency codes — joined into `symbols`), `signal` (`AbortSignal` forwarded to the underlying fetch). **Returns**: `{ base, rates, timestamp }` where `timestamp` is milliseconds since epoch. **Throws** (all `CurrencyProviderError`): - `MISSING_CONFIG` — at construction when `apiKey` is empty. - `HTTP_ERROR` — non-2xx upstream response (via `httpRequest`). - `PARSE_ERROR` — body is not JSON, parsed value is not an object, or a `success: true` envelope is missing `base` / `rates` / `timestamp`. - `UNSUPPORTED_BASE` — upstream code `105` (free-tier base restriction). - `UNKNOWN_CURRENCY` — upstream code `201` (`invalid_base_currency`) or `202` (`invalid_currency_codes`). - `UPSTREAM_ERROR` — any other `success: false` envelope, including the case where `error` is absent. ## Implementation Notes - Fixer.io returns HTTP 200 even for application-level errors; the provider must therefore parse every body and discriminate on `success`. The non-2xx path (`HTTP_ERROR`) is reserved for genuine network / gateway failures. - The upstream `error.code` integer is the discriminator, not `error.type` — the integer is one comparison and avoids string allocation. The human-readable `error.info` is preserved on `upstreamMessage` for diagnostics. - The `apiKey` non-empty guard at construction is suaveplan-native — the upstream skeleton allowed an empty string and let the API reject on the wire, wasting the request against the monthly quota. - `fetchRates` forwards `options.signal` through `httpRequest` so the caller can cancel an in-flight request from outside the provider; this too is suaveplan-native (upstream did not propagate the signal). - The package is universal and free of side effects (`sideEffects: false`). Importing the package never mutates any global registry — the caller decides whether to register the instance with `CurrencyProviderRegistry`.