176 lines
5.1 KiB
Markdown
176 lines
5.1 KiB
Markdown
# OpenSpec Change: enterprise-currency
|
|
|
|
- **Change ID**: enterprise-currency
|
|
- **Package**: `@suaveplan/currency`
|
|
- **Category**: finance
|
|
- **Target path**: `finance/currency/currency`
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-24
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/currency.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Deliver `@suaveplan/currency` on the genesis branch from scratch at enterprise quality. The package is a currency conversion engine with pluggable `CurrencyProvider` interface, TTL-based `RateCache`, automatic BFS cross-rate triangulation (shortest-path over the cached-rate graph), inverse-rate calculation, per-provider circuit breaker, `AbortSignal` cancellation, batch convert, and a `setVirtualRate` live-update API. Returns typed `ConversionResult` with provenance (`providerName`, `triangulatedThrough`). Ships two reference providers: `InMemoryProvider` and `createStaticProvider`.
|
|
|
|
---
|
|
|
|
## Current-state audit (genesis)
|
|
|
|
Does not exist on genesis — fresh build.
|
|
|
|
---
|
|
|
|
## Available workspace deps on genesis
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` (`workspace:^`) | Typed `CURRENCY.*` error codes |
|
|
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `EventBusLike` |
|
|
| `@suaveplan/events` (`workspace:^`) | Virtual-rate change events |
|
|
| `@suaveplan/retry` (`workspace:^`) | Circuit-breaker + backoff |
|
|
| `@suaveplan/logger` (`workspace:^`) | Structured logs |
|
|
| `zod` (`catalog:forms`) | Options validation |
|
|
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters |
|
|
| `tslib` (`catalog:build`) | Runtime helpers |
|
|
|
|
Dev dep: `@suaveplan/money workspace:^` (for `MoneyLike` type in tests).
|
|
|
|
---
|
|
|
|
## Public API surface (from main branch enterprise spec)
|
|
|
|
```ts
|
|
class CurrencyConverter {
|
|
constructor(config: ConverterConfig)
|
|
convert(from: string, to: string, amount: MoneyLike | number, options?: ConvertOptions): Promise<ConversionResult>
|
|
convertAmount(from: string, to: string, amount: number, options?: ConvertOptions): Promise<ConversionResult>
|
|
convertBatch(requests: Array<{from: string; to: string; amount: number}>): Promise<ConversionResult[]>
|
|
setVirtualRate(symbol: string, rate: number): void
|
|
invalidateCache(from?: string, to?: string): void
|
|
}
|
|
|
|
class RateCache {
|
|
get(from: string, to: string): ExchangeRate | undefined
|
|
set(from: string, to: string, rate: ExchangeRate): void
|
|
prune(): void
|
|
invalidate(from?: string, to?: string): void
|
|
}
|
|
|
|
function createStaticProvider(rates: Record<string, Record<string, number>>): CurrencyProvider
|
|
class InMemoryProvider implements CurrencyProvider { setRates(rates: ExchangeRate[]): void }
|
|
|
|
interface ConversionResult {
|
|
amount: number
|
|
rate: number
|
|
inverseRate: number
|
|
rateAgeMs: number
|
|
providerName: string
|
|
triangulatedThrough: string[] | null
|
|
}
|
|
|
|
interface ConverterConfig {
|
|
providers: CurrencyProvider[]
|
|
defaultBase?: string
|
|
cacheTtlMs?: number
|
|
maxRetries?: number
|
|
logger?: LoggerLike
|
|
}
|
|
|
|
interface CurrencyProvider {
|
|
name: string
|
|
supportsFiat: boolean
|
|
supportsCrypto: boolean
|
|
fetchRates(options: RateFetchOptions): Promise<RateFetchResult>
|
|
isHealthy?(): boolean
|
|
}
|
|
```
|
|
|
|
### Error codes
|
|
|
|
```ts
|
|
type CurrencyErrorCode =
|
|
| 'CURRENCY_PROVIDER_UNAVAILABLE'
|
|
| 'CURRENCY_UNSUPPORTED_PAIR'
|
|
| 'CURRENCY_TRIANGULATION_IMPOSSIBLE'
|
|
| 'CURRENCY_CONVERT_ABORTED'
|
|
| 'CURRENCY_INVALID_CONFIG'
|
|
|
|
function registerCurrencyErrorCodes(): void
|
|
```
|
|
|
|
---
|
|
|
|
## New additions for genesis
|
|
|
|
- **BFS triangulation** in `src/engine/triangulate.ts`
|
|
- **Circuit breaker** per provider in `src/engine/circuit-breaker.ts` using `@suaveplan/retry`
|
|
- **`convertBatch`** — coalesces required rate fetches
|
|
- **`setVirtualRate` + change events** via `@suaveplan/events`
|
|
- **`AbortSignal` on `convert`**
|
|
- **Zod schemas** for `ConverterConfig`, `ConvertOptions`
|
|
- **OTel** spans per convert, cache-hit counter, triangulation-path histogram
|
|
|
|
---
|
|
|
|
## File layout
|
|
|
|
```text
|
|
finance/currency/currency/
|
|
package.json
|
|
tsconfig.json
|
|
vitest.config.ts
|
|
src/
|
|
index.ts
|
|
error-codes.ts
|
|
constants/index.ts
|
|
types/
|
|
currency.types.ts
|
|
currency.types.md
|
|
engine/
|
|
converter/
|
|
converter.ts
|
|
converter.md
|
|
converter.types.ts
|
|
converter.test.ts
|
|
index.ts
|
|
rate-cache/
|
|
rate-cache.ts
|
|
rate-cache.md
|
|
rate-cache.test.ts
|
|
index.ts
|
|
triangulate/
|
|
triangulate.ts
|
|
triangulate.md
|
|
triangulate.test.ts
|
|
index.ts
|
|
circuit-breaker/
|
|
circuit-breaker.ts
|
|
circuit-breaker.md
|
|
circuit-breaker.test.ts
|
|
index.ts
|
|
provider/
|
|
in-memory-provider/
|
|
in-memory-provider.ts
|
|
in-memory-provider.md
|
|
in-memory-provider.test.ts
|
|
index.ts
|
|
static-provider/
|
|
static-provider.ts
|
|
static-provider.md
|
|
static-provider.test.ts
|
|
index.ts
|
|
tests/
|
|
integration.test.ts
|
|
property.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Impact
|
|
|
|
Downstream: `@suaveplan/currency-providers` (implements `CurrencyProvider`), `@suaveplan/money` (integration for `MoneyLike`).
|
|
|
|
**Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/retry`, `@suaveplan/logger`.
|