Files
2026-05-27 23:29:53 +01:00

136 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.