136 lines
7.0 KiB
Markdown
136 lines
7.0 KiB
Markdown
# 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-<name>` 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.
|