# stapel-currencies 0.1.9

Currencies and exchange rates: a Currency catalog keyed by ISO 4217 code with Decimal rates relative to a configurable base currency, cross-rate conversion (convert()) exposed as a comm Function, a pluggable rate-provider seam (ECB by default), a public read-only HTTP API, and a Celery task / management commands to refresh rates.

Contract: axes 2 · surface 3 · extension points 2 · operations 2 · error codes 45.
Generated from docs/capabilities.json by `stapel-llms-txt` — do not edit; drift-gated by `make contract-check`.

## Configuration axes — what a product switches on
Settings keys; `default` is what you get by saying nothing. Turning an axis off unmounts the operations it gates.
- BASE_CURRENCY [enum, default "USD"] — Base currency for exchange rates
  ISO 4217 code every Currency.value rate is relative to. The base currency always converts at rate 1. Changing it reinterprets stored rates — re-run update_exchange_rates after switching (conf.py, MODULE.md Extension points table).
- CONVERSION_DECIMAL_PLACES [enum, default 2] — Conversion result precision
  Decimal places conversion results are quantized to (ROUND_HALF_UP) in services.convert and the currencies.convert Function (conf.py, MODULE.md).

## Usage surface — call these before writing your own
This is the answer to "does Stapel already have something for X?". `instead of` names the outside symbol this one displaces.
### gate_function
- convert — stapel_currencies.services.convert
  instead of: Currency.value ratio computed and rounded by hand, float-based exchange-rate arithmetic
  Convert a Decimal amount between two active currencies via the base-currency cross rate, quantized to CONVERSION_DECIMAL_PLACES (ROUND_HALF_UP). Call this instead of reading Currency.value rows and dividing/multiplying by hand — the base-currency short-circuit, the active-currency check and the rounding rule are each an easy place to drift from the framework's answer.
- update_exchange_rates — stapel_currencies.services.update_exchange_rates
  instead of: hand-rolled rate-source fetch + Currency.save() loop
  Fetch rates from the configured RATE_PROVIDER and update the Currency rows in one call — the same seam the Celery task and the update_exchange_rates management command already use. Call it from your own scheduler or admin action instead of re-implementing the fetch-then-save loop.
### factory
- get_rate_provider — stapel_currencies.services.get_rate_provider
  Instantiate the configured STAPEL_CURRENCIES['RATE_PROVIDER'] (validated as a RateProvider subclass). Reach for this instead of resolving/import_string-ing the setting yourself whenever you need today's rates without persisting them — e.g. a preview endpoint that shows what update_exchange_rates would change.

## Extension points — what a product replaces, fork-free
- DEFAULT_CURRENCIES [catalog_override]
  Seed list read by the load_default_currencies management command (code/display_name/symbol/value dicts); replaces the default 16-currency list wholesale (conf.py, MODULE.md).
- RATE_PROVIDER [dotted_path]
  REPLACE, single strategy: implement the RateProvider ABC (providers.py) and point the setting at it — default ECBRateProvider (ECB eurofxref-daily.xml, rebased when base != EUR); enforced as a RateProvider subclass by get_rate_provider() (conf.py, MODULE.md 'Rate providers').

## Fits with — fleet dependencies
- stapel-core (required) — AppSettings config layer, comm Function (currencies.convert), error registry — the only stapel-* dependency (pyproject.toml: 'Depends on stapel-core only'; conf.py:11, functions.py:19, errors.py:6)

## HTTP operations (2) — call by operationId, never by a typed path
Paths are relative to `/currencies/api/v1/`.
### Currencies and exchange rates
- GET / — currencies_api_v1_list
- GET /{code}/ — currencies_api_v1_retrieve

## Error codes (45) — the StapelError envelope
Render `t(code, params)`; branch UX on the remediation. Localized text lives in docs/errors.<lang>.md, not here.
- error.400.bad_request [400] fix_input
- error.400.captcha_invalid [400] retry
- error.400.captcha_required [400] retry
- error.400.expected_list [400] fix_input
- error.400.field.blank [400] fix_input {field}
- error.400.field.does_not_exist [400] fix_input {field}
- error.400.field.invalid [400] fix_input {field}
- error.400.field.invalid_choice [400] fix_input {field}
- error.400.field.max_length [400] fix_input {field,max_length}
- error.400.field.max_value [400] fix_input {field,max_value}
- error.400.field.min_length [400] fix_input {field,min_length}
- error.400.field.min_value [400] fix_input {field,min_value}
- error.400.field.null [400] fix_input {field}
- error.400.field.required [400] fix_input {field}
- error.400.field.unique [400] fix_input {field}
- error.400.invalid_ad_id [400] fix_input
- error.400.invalid_amount [400] fix_input
- error.400.unknown_currency [400] fix_input
- error.400.validation_error [400] fix_input
- error.400.verification_failed [400] verify
- error.400.verification_invalid_factor [400] verify
- error.401.unauthorized [401] reauthenticate
- error.402.payment_required [402] retry
- error.403.forbidden [403] retry
- error.403.network_blocked [403] contact_support
- error.403.verification_enrollment_required [403] verify
- error.403.verification_required [403] verify
- error.404.ad_not_found [404] retry
- error.404.not_found [404] retry
- error.404.verification_challenge_not_found [404] verify
- error.405.method_not_allowed [405] retry
- error.406.not_acceptable [406] retry
- error.408.request_timeout [408] retry
- error.409.conflict [409] fix_input
- error.410.gone [410] retry
- error.413.payload_too_large [413] retry
- error.415.unsupported_media_type [415] retry
- error.422.unprocessable_entity [422] wait_and_retry
- error.423.locked [423] wait_and_retry
- error.423.verification_locked [423] wait_and_retry
- error.429.rate_limit [429] wait_and_retry {retry_after_minutes}
- error.429.too_many_requests [429] wait_and_retry
- error.500.internal [500] contact_support
- error.502.rate_fetch_failed [502] retry
- error.503.mandate_unavailable [503] retry
