Primitive spec

Payments

The money-in primitive. Payments models the customer's payment instrument, the attempt to collect, the hold on funds, the capture, the settlement, and the dispute — each as its own entity, matched to how real payment processors actually work.

Status: Tokenized Stripe driver (PaymentIntent capture + status mapper) and tokenized Finix driver (Authorization → Transfer → Reversal) shipped. Refund and void thread credentials correctly; Transaction snapshots the consumer-entity link. Card-present (EMV) Slice 1 adds connection tokens + a captureMode on the Transaction for in-person chip payments. Manual capture (authorize-now / capture-later, partial + tip), stored-credential / MIT reuse, processor routing + failover, and signature-verified per-processor webhooks (dedupe + dispute ingestion) are wired, and disputes carry a full evidence-and-outcome state machine. Newest: booking deposits collect real cards through a Stripe Payment Element flow (intent → webhook settlement → booking confirmed), and a June 10 hardening pass made the money path tell the truth — statuses follow the processor, never run ahead of it. Adyen and an Odin driver land in subsequent slices.

What it owns

Payments owns the lifecycle of a charge attempt — from a customer's tokenised payment method to the cents that hit a merchant's bank account. It deliberately does not own a unified "Charge" object. Each processor (Stripe, Finix, Adyen) has its own native lifecycle; collapsing them into one row creates a duplicated state machine that drifts from whatever the processor actually says. Each layer is its own entity; the denormalised "Charge" view exists only as a read endpoint.

POS terminals live in Trantor as DiscreteResources, not in Payments. Tokenisation is the only allowed handle on card data; raw PAN never enters the system.

Concepts

ProcessorConfiguration
Per-tenant credentials and config for a specific processor (Stripe account, Finix merchant, Adyen MID). Credentials are envelope-encrypted at rest.
PaymentMethod
A tokenised payment instrument attached to a Terminus Person — card, bank account, digital wallet, gift card. Stores only the processor's token, plus last4, brand, expiry, and a fingerprint. Never the PAN.
PaymentIntent
The attempt to collect money. Status machine mirrors Stripe's industry shape: requires_payment_method → requires_confirmation → requires_action → processing → succeeded, with canceled as the terminal branch. captureMethod chooses automatic capture or hold-now-capture-later.
Authorization
A hold on funds without capture. Created when captureMethod=manual. Has its own expiry (most networks: 7 days). Captured into a Transaction; voided releases the hold.
Transaction
An actual movement of money — capture, refund, void, or adjustment. Refunds are transactions with negative amountCents and a parentTransactionId pointing at the original capture.
Settlement
What hits the merchant's bank account. One row per processor batch. Carries grossCents, refundsCents, feeCents, adjustmentCents, and the derived netCents. Mismatches against the processor's claimed net surface for operator review.
Dispute
A chargeback with timeline, evidence, and outcome. Status flows through warning, evidence response, network review, and final win / loss / accept. Evidence is uploaded per network spec.
Composite "Charge" view
The denormalised tree a UI wants — intent + auth + transactions + settlement — lives only as a read endpoint at /payments/v1/charges/{intentId}. Not a persisted entity.
Card-present (EMV)
In-person chip payments on a physical terminal. The reader is a Trantor DiscreteResource; Payments mints a short-lived connection token the terminal SDK uses to talk to the processor directly, so card data never touches Foundation. A captureMode of CARD_PRESENT (vs. the default CARD_NOT_PRESENT) records where the card was taken. Slice 1 ships the connection-token + capture-mode plumbing; the per-processor terminal drivers follow.
Booking deposits (Payment Element)
The deposit-to-confirm loop, end to end. POST /payments/v1/intents with a bookingId resolves the booking's open deposit requirement (the Daneel needs-ledger entry) and opens a hosted-element PaymentIntent, returning the client_secret + publishable key for the browser — no card data near Foundation. A Stripe Payment Element widget in the render-runtime collects the card on the public booking flow, and the signature-verified payment_intent.succeeded webhook settles the deposit and confirms the booking.

API surface

Endpoints are versioned under /payments/v1/. PCI scope is constrained to token handling; never the raw card.

Payments endpoints in the Foundation API reference OpenAPI 3.1 schema for Payments with request/response shapes, parameters, and a try-it client.
MethodPathPurpose
POST / GET / DELETE/payments/v1/payment-methodsAttach, list, or revoke tokenised methods for a Person.
POST/payments/v1/intentsCreate a PaymentIntent. With a bookingId, opens a hosted-element deposit intent for the booking.
POST/payments/v1/intents/{id}/attach-methodAttach a PaymentMethod to an intent.
POST/payments/v1/intents/{id}/confirmConfirm. Runs the driver. Returns requires_action if SCA / 3DS is needed.
POST/payments/v1/intents/{id}/cancelCancel an intent.
POST/payments/v1/authorizations/{id}/captureCapture an authorized hold. Body specifies amountCents.
POST/payments/v1/authorizations/{id}/voidVoid an authorization pre-capture.
POST/payments/v1/transactions/{id}/refundIssue a refund leg against a capture.
POST/payments/v1/readers/{resourceId}/connection-tokenMint a short-lived connection token for a card-present terminal.
GET/payments/v1/settlements/{id}Inspect a settlement batch with linked transactions.
GET / POST/payments/v1/disputes/{id}View a dispute or upload evidence per network spec.
POST/payments/v1/webhooks/{processorType}Per-processor webhook inbound. Signature verified, deduped, dispatched.
GET/payments/v1/charges/{intentId}Composite read view: intent + auth + transactions + settlement.

Example: a tip-on-card flow

Authorize $40 at swipe, then capture $45 (with $5 tip) after the customer signs:

POST /payments/v1/intents
{
  "amountCents":  4000,
  "currency":     "USD",
  "captureMethod":"manual",
  "customerPersonId": "per_alice…",
  "sourceType":   "hober_order",
  "sourceId":     "ord_01JAZB…"
}
→ 201  PaymentIntent  pi_01JAZD…  status=requires_payment_method
POST /payments/v1/intents/pi_01JAZD…/attach-method  { "paymentMethodId": "pm_visa_4242" }
POST /payments/v1/intents/pi_01JAZD…/confirm
→ 200  status=succeeded   Authorization  auth_01JAZE…   amountCents=4000
POST /payments/v1/authorizations/auth_01JAZE…/capture
{ "amountCents": 4500 }
→ 200  Transaction  txn_01JAZF…   type=capture   amountCents=4500
        Authorization status=captured

How it fits with the rest

flowchart LR
  Ho[Hober tender] --> Pa(Payments)
  WH[Processor webhooks] --> Pa
  Pa -. owner .-> Te[Terminus]
  Pa -. device .-> T[Trantor]
  Pa -- settled --> M[Mallow]
  Pa -- events --> D[Daneel]
  Pa -- alerts --> Sp[Speaker]
            

Terminus owns the Person that PaymentMethods belong to. Hober OrderPayments reference Transactions by id. On settlement, Mallow writes the cash-receipt ledger entry — cents + currency + transaction ref cross the boundary, never processor detail. Trantor POS Terminals carry the processor's device registration in metadata. Speaker sends payment-failed and dispute-opened notifications via consent-gated dispatch. Daneel workflows subscribe to payments.* events for automation (auto-retry, alert ops, dispute response routing).