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: v0 ships ProcessorConfiguration, Transaction, and a MockProcessorDriver. The expanded entity model (PaymentMethod, PaymentIntent, Authorization, Settlement, Dispute) and real Stripe / Finix / Adyen drivers are designed and being implemented in 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.

API surface

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

MethodPathPurpose
POST / GET / DELETE/payments/v1/payment-methodsAttach, list, or revoke tokenised methods for a Person.
POST/payments/v1/intentsCreate a PaymentIntent.
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.
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).