ADR 0013: Publish every monetary field as a money object
Context
The contract modelled money four ways.ActivityView carried Money objects;
QuoteView mixed Money and NonNegativeMoney objects; AccountView,
CardView, and CardTransactionView carried bare decimal strings beside a
sibling currency field; MandateView carried five bare decimals under one
currency constant. The same fact — an amount in a currency — had object and
bare-decimal structures inside one API, and the generated contract
published two names for one schema (Money-Input and Money-Output) because
the decimal types declared a serialization-only JSON schema.
Domain E of the contract architecture plan adds purchase amounts that can
settle at zero, so the shape of an observed amount had to be settled before
that surface exists.
Decision
Every monetary response field is a money object. No bare decimal beside a sibling currency survives in a response, and a regression test holds every bare decimal in the generated contract to a named allowlist. Three money schemas exist, chosen per field by what the value can be:Money(amount > 0) where zero is impossible: caps, limits, requested amounts.MandateViewpublishes its five caps asMoneyobjects and drops thecurrencyconstant that governed five siblings.NonNegativeMoney(amount >= 0) for every observed or settled amount: account balances (nullable — null means the provider reports no such balance), card balances, card transaction fees, and activity amounts.ActivityView.amountis an unsigned magnitude whose direction is carried bykind, now a closedLiteralover the adapter’s seven-value map.SignedMoney(any finite decimal) for card ledger entries only: a card transaction amount carries its direction in the sign and has no kind field to move it to. Forcing it through an unsigned schema would delete a fact.
perflo-connections, PerfloConnectionView, PerfloCapabilitiesView,
and OnboardingView.perflo_* keep their names because the customer is
authorizing Perflo specifically and consenting to it by name. No superseding
ADR is required.
Consequences
AccountView, CardView, CardTransactionView, MandateView, and
ActivityView all changed shape, and both generated OpenAPI documents and the
web consumers changed with them. Web formatting reads money.amount and
money.currency from one object instead of pairing two fields by convention.
The mandate projection wraps its stored bare-decimal caps into United States
dollar money objects in an explicit view function; the stored columns are
unchanged. Card rows still persist a flat balance and currency pair; the card
view owns the published shape.
Request bodies keep bare decimals where the currency is fixed by rule:
MandateCreate’s five caps and MandateExecutionCreate.amount are Perflo cash
United States dollar amounts by construction, while QuoteCreate.source
already carries a Money object because there the customer chooses the
currency. The published request schemas now document the string form only;
JSON numbers are still accepted at runtime.
The activity identifier is stable for a row first observed by hash that later
gains a provider id. A row first observed with an id that later gains a hash
would still change its identifier; the observed provider behaviour makes that
direction theoretical, and the in-page deduplication treats id and hash as one
identity space either way.