# Add @suaveplan/currency-provider-fixer — Fixer.io fiat-rates sibling provider **Package**: `@suaveplan/currency-provider-fixer` **Status**: PROPOSED **Type**: new package (genesis-original sibling of slimmed `@suaveplan/currency-providers` core) - **Design**: ./design.md - **Spec delta**: ./specs/currency-provider-fixer/spec.md ## Why The 2026-05-17 slim refactor of `@suaveplan/currency-providers` (`openspec/archive/2026-05-17-slim-currency-providers/`) split the nine concrete providers out of the umbrella package and committed to landing each as a dedicated `@suaveplan/currency-provider-` sibling consuming the slim core (registry / factory / `httpRequest` / `composeAbortSignal` / configs / `CurrencyProviderError`). This change lands the **Fixer.io** sibling — the first API-keyed fiat-rates provider in the follow-up wave. Fixer.io is the most widely deployed third-party fiat-rates API after ECB (Reuters / payment-aggregator backbone for years); shipping it early gives downstream commerce / accounting consumers an API-keyed alternative to the key-free ECB feed (`@suaveplan/currency-provider-ecb`, landing alongside) that supports 170+ currencies and is gateway-friendly. ## What changes ### ADDED - **New package** `@suaveplan/currency-provider-fixer` (universal-tier, `0.1.0`, `sideEffects: false`). - **`FixerProvider` class** implementing the `CurrencyProvider` contract from `@suaveplan/currency` (`name = "fixer"`, `supportsFiat = true`, `supportsCrypto = false`). Constructor takes `ApiKeyConfig` (from the slim core's `./types/configs`). Optional `baseUrl` override defaults to `https://data.fixer.io/api`. Optional injectable `FetchLike` (from `@suaveplan/types`) defaults to `globalThis.fetch`. - **`fetchRates(options?)`** delegates HTTP to the slim core's `httpRequest` helper (caller-signal composition, non-2xx → typed `HTTP_ERROR`, response pass-through), then parses the upstream JSON envelope into the universal `RateFetchResult` shape (`{ base, rates, timestamp }`). - **Typed error mapping** from upstream `{ success: false, error: { code, type, info } }` envelopes to the slim core's `CURRENCY_PROVIDER_*` codes: - empty `apiKey` at construction → `MISSING_CONFIG` - non-2xx HTTP response → `HTTP_ERROR` (raised by `httpRequest`) - malformed / non-object JSON → `PARSE_ERROR` - upstream code `105` (`base_currency_access_restricted`) → `UNSUPPORTED_BASE` - upstream code `201` (`invalid_base_currency`) or `202` (`invalid_currency_codes`) → `UNKNOWN_CURRENCY` - any other `success: false` envelope → `UPSTREAM_ERROR` carrying `upstreamMessage` - **No auto-register.** The package exports the class; callers wire it via `new FixerProvider({ apiKey })` or `registry.register(new FixerProvider({ apiKey }))` per the slim-core design. There is no top-level side effect. ### KEPT (unchanged from slim core) - The `CurrencyProvider` contract, `RateFetchOptions`, `RateFetchResult` remain owned by `@suaveplan/currency`. - `CURRENCY_PROVIDER_CODES`, `CurrencyProviderError`, `httpRequest`, `composeAbortSignal`, and `ApiKeyConfig` remain owned by `@suaveplan/currency-providers`. ## Source mapping This package is a migration of the upstream `FixerProvider` previously bundled in `@suaveplan/currency-providers`. The source was carved out by the 2026-05-17 slim refactor and is reconstituted here against the slim-core contracts. | Component | Source | | --- | --- | | `FixerProvider` class skeleton | `../suaveplan/packages/finance/currency-providers/src/fiat/fixer.ts` L1–L67 | | README quick-start example | upstream `fixer.md` L17–L37 | | Free-tier EUR-only constraint | upstream `fixer.md` L52–L54 + Fixer.io API docs error code `105` | | Upstream JSON envelope shape (`success` / `error.code` / `error.info` / `base` / `rates` / `timestamp`) | upstream `fixer.ts` L47–L53 + Fixer.io public docs | | HTTP / abort-signal / non-2xx mapping | `@suaveplan/currency-providers/http/http` (slim core) | | Error code surface | `@suaveplan/currency-providers/error-codes/error-codes` (slim core) | The genesis-native upgrades over the upstream skeleton: - Replaces the bare `throw new Error(...)` in upstream with `CurrencyProviderError` carrying stable codes from the slim core's registry (upstream `fixer.ts` L42 / L57 → `MISSING_CONFIG` / `PARSE_ERROR` / `UNSUPPORTED_BASE` / `UNKNOWN_CURRENCY` / `UPSTREAM_ERROR`). - Routes HTTP through the slim core's shared `httpRequest` so caller-supplied `AbortSignal` from `RateFetchOptions.signal` is honoured (upstream version did not). - Validates `apiKey` at construction (upstream silently accepted an empty string and let the upstream API reject it on the wire). - Adds the `FetchLike` injection seam from `@suaveplan/types` (upstream used the bare `globalThis.fetch`). ## Parity Checklist | Capability | Upstream | Migrated | Notes | | --- | --- | --- | --- | | `name`/`supportsFiat`/`supportsCrypto` identity flags | ✅ | ✅ | | | `ApiKeyConfig` constructor (apiKey + optional baseUrl) | ✅ | ✅ | Inherited from slim core. | | Default URL `https://data.fixer.io/api` | ✅ | ✅ | | | `fetchRates({ base, targets })` query construction | ✅ | ✅ | `access_key`, `base`, `symbols`. | | Default base `EUR` when none supplied | ✅ | ✅ | | | `success: false` envelope detection | ✅ | ⬆️ | Now mapped to typed codes via upstream `error.code` discriminant instead of a bare `throw new Error()`. | | Non-2xx → typed error | ✅ | ⬆️ | Routed through slim core's `httpRequest`; raises `HTTP_ERROR` with populated `status` + `providerName`. | | Timestamp seconds → ms conversion | ✅ | ✅ | `data.timestamp * 1000`. | | Symbol filter via `targets` | ✅ | ✅ | | | `apiKey` non-empty validation at construction | 🚫 | ⬆️ | New: raises `MISSING_CONFIG` instead of letting the API reject on the wire. | | `AbortSignal` propagation via `RateFetchOptions.signal` | 🚫 | ⬆️ | New: caller signal composed by slim-core `httpRequest`. | | `FetchLike` injection seam | 🚫 | ⬆️ | New: optional `config.fetch` honoured for testability and proxying. | No upstream capability is dropped. Every 🚫 row is an ADDITION on top of upstream. ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/currency-provider-fixer` to enumerate. - No current dependents (package is new). The slim core `@suaveplan/currency-providers` is depended on by this package, not the reverse — no consumer rewrite is required. ## Out of scope - Other sibling providers (ECB, OXR, Binance, CoinGecko, CMC, CryptoCompare, Kraken, Virtual) — each ships under its own OpenSpec change per AGENTS.md §0c. - Historical rates (`/{date}` endpoint), time-series (`/timeseries`), fluctuation (`/fluctuation`), conversion (`/convert`) endpoints — the `CurrencyProvider` contract today exposes only `fetchRates`; the additional endpoints will land once the contract is extended (carried over from the 2026-05-17 archive's `## Out of scope`). - Rate-limit / retry / aggregator wrappers — same deferral as the slim core.