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.
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, withcanceledas the terminal branch.captureMethodchooses 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
amountCentsand aparentTransactionIdpointing at the original capture. - Settlement
- What hits the merchant's bank account. One row per processor batch. Carries
grossCents,refundsCents,feeCents,adjustmentCents, and the derivednetCents. 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.
| Method | Path | Purpose |
|---|---|---|
| POST / GET / DELETE | /payments/v1/payment-methods | Attach, list, or revoke tokenised methods for a Person. |
| POST | /payments/v1/intents | Create a PaymentIntent. |
| POST | /payments/v1/intents/{id}/attach-method | Attach a PaymentMethod to an intent. |
| POST | /payments/v1/intents/{id}/confirm | Confirm. Runs the driver. Returns requires_action if SCA / 3DS is needed. |
| POST | /payments/v1/intents/{id}/cancel | Cancel an intent. |
| POST | /payments/v1/authorizations/{id}/capture | Capture an authorized hold. Body specifies amountCents. |
| POST | /payments/v1/authorizations/{id}/void | Void an authorization pre-capture. |
| POST | /payments/v1/transactions/{id}/refund | Issue 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).