> ## 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.

# One beneficiary vocabulary

> The contract speaks beneficiary everywhere - one noun, one join name, one ownership polarity, a total projection, and validated purpose codes.

# ADR 0014: Speak one beneficiary vocabulary in the contract

## Context

The beneficiary domain published two vocabularies. The neobank resource was
`beneficiary`-named while its metadata routes and schemas were
`recipient`-named, mirroring the provider's fiat routes. One fact appeared
under two names with inverted polarity — `own_account` on write,
`is_external` on read. The join between a create request and the schema
catalogue crossed the term boundary under two names, with the write side
aliased to `schema`, a word that also shadows the JSON Schema keyword.
`purpose_code` was accepted unvalidated and failed at the provider as a
never-retryable error after the local operation was persisted. A projection
emitted optional fields only when the provider row carried them, so a null
was ambiguous. And `BankDetails.beneficiary_name` used the resource's noun
for the opposite party — the holder of the customer's own account.

## Decision

One noun. `BeneficiaryCountry`, `BeneficiarySchemaView`, and
`BeneficiaryField` replace the `Recipient*` schemas, and the metadata routes
nest under the resource as `/v1/beneficiaries/countries` and
`/v1/beneficiaries/schemas`. Both static sub-paths are declared before the
parameterized single read, and a test pins the ordering.

`payout_schema_id` names the join on both sides — attribute and wire — with
the `schema` alias gone. `is_external` is the only spelling of ownership;
the provider mapping lives in the adapter's create call. The stored column
renames with the model, and a revision carries the rename.

`GET /v1/beneficiaries/{beneficiary_id}` exists and serves the same enriched
list read as the collection, filtered to one row. The upstream single read
returns only the identifier, provider context, and status, so serving the
route from it would reintroduce the null ambiguity this record deletes; that
call stays internal to mandate grant verification.

`BeneficiaryView` is a total projection: every field is always emitted and
null means the provider has communicated no value, never that a read
omitted the field. `destination_masked` and `is_external` persist on the
row — `is_external` from the create itself, both thereafter under the same
three-state reconciliation as every other provider-owned field — so the
guarantee holds on the single read as well as the collection, and an
omitting provider read keeps the persisted values.
The internal three-state reconciliation protocol — value sets, explicit
null clears, absent keeps — is deliberately untouched, because collapsing
absent into clear silently erases persisted fields on read.

The schema catalogue itself never carries purpose codes: the provider
publishes a rail's accepted codes behind its withdrawal-limits read, and an
empty or unreadable answer there is advisory. `purpose_codes` on
`BeneficiarySchemaView` is therefore nullable — null means the provider
declared no codes and stays the enforcer; a non-empty list is
authoritative. Creation
requires the `recipient_metadata` capability, because purpose codes are
validated server-side against the chosen schema after the idempotency early
return, so a replay of an already-succeeded create never issues a provider
call. Four outcomes are distinguished: an unknown schema, a supplied code a
declared list does not accept, an absent code where a declared list
requires one, and an unreadable schema list, which fails the create closed.

`BankDetails.beneficiary_name` becomes `account_holder_name`, resolving the
noun collision this rename would otherwise deepen.

Worker shims accept the previous payload and context keys so operations
enqueued before this change drain; they are removed once no pre-rename
beneficiary operation remains in a non-terminal state.

## Consequences

The contract carries no `recipient`-named path, operation, parameter,
schema, or property, and no property or parameter named `schema`; a
scanning test enforces the ban with one documented allowlist entry — the
`recipient_metadata` property of `PerfloCapabilitiesView`, which names a
provider capability rather than a contract noun. The generated inventory no
longer classifies the three provider routes the adapter never calls as
used, and it classifies the withdrawal-limits read as used. Beneficiary
creation costs at most two provider reads before the operation is persisted
— the schema catalogue and, when the chosen rail's codes are undeclared
there, one withdrawal-limits read for that rail alone. The catalogue route
resolves undeclared rails' codes in parallel. The single
beneficiary read shares the collection's cost profile: it performs the full
provider list read and commits reconciliation, so a provider failure
surfaces as a provider error rather than a local 404.

## Rejected alternatives

Emitting every key unconditionally from the adapter normalizer: it is the
obvious fix for null ambiguity and causes silent data loss, because the
reconciliation loop treats an absent key as "keep" and an explicit null as
"clear" — unconditional emission collapses the two and one list read would
permanently null persisted fields. The ambiguity is fixed in the public
projection instead.

Serving the single read from the provider's one-row endpoint: it returns
only identifier, context, and status, which would make the single read less
total than the list it claims to filter.

Renaming the provider connection lifecycle nouns as part of the same sweep:
ADR 0013 records why `perflo-connections` and its views keep the provider's
name — the customer is consenting to Perflo by name.
