> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neobank.proofof.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Money objects for every monetary field

> Every monetary response field is a Money, NonNegativeMoney, or SignedMoney object; no bare decimal beside a sibling currency survives.

# 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. `MandateView` publishes its five caps as `Money` objects and drops
  the `currency` constant 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.amount` is an unsigned magnitude whose direction is carried by
  `kind`, now a closed `Literal` over 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.

The decimal types declare one JSON schema for both validation and
serialization, so the generated contract carries exactly one schema per money
type. The account normalizer fails closed on negative balances, and the
activity projection's published identifier is keyed on the immutable
transaction hash when one exists, so the identifier no longer changes when the
provider later assigns a row id.

This record also clarifies ADR 0002's scope: its decision enumerates the
provider-neutral models — account, beneficiary, quote, transfer, mandate,
card, operation, error — and the Perflo connection lifecycle was never among
them. `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.

## Rejected alternatives

Bare decimals with a documented sibling-currency convention: the convention is
invisible to the type system, and the two shapes had already diverged inside
one API.

Two money schemas with card amounts forced to unsigned magnitudes: card
transactions carry direction in the sign and have no kind vocabulary, so the
unsigned form deletes information. Money safety outranks shape coherence.

A single signed money schema everywhere: it would stop the type system from
rejecting a negative cap or balance at the boundary, weakening validation to
gain uniformity.
