-
Notifications
You must be signed in to change notification settings - Fork 0
Data Model Payments
Tiana_ edited this page May 30, 2026
·
1 revision
Part of the Data Model reference. Schema:
payments. Companion to Domain-Model, Architecture-Overview.
| Table | Role |
|---|---|
payments.payments |
Payment lifecycle record (mutable, versioned) |
payments.payment_events |
Append-only event log per payment |
payments.processed_webhooks |
Inbound webhook deduplication |
CREATE TABLE IF NOT EXISTS payments.payments (
id UUID PRIMARY KEY,
idempotency_key VARCHAR(255) NOT NULL,
from_account_id UUID,
to_account_id UUID,
counterparty_name VARCHAR(255),
counterparty_iban VARCHAR(34),
counterparty_bic VARCHAR(11),
counterparty_account VARCHAR(64),
counterparty_country CHAR(2),
amount NUMERIC(38, 18) NOT NULL,
currency CHAR(3) NOT NULL,
state VARCHAR(32) NOT NULL,
ledger_transaction_id UUID,
provider_id VARCHAR(64),
provider_ref VARCHAR(255),
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL DEFAULT 6,
retryable BOOLEAN NOT NULL DEFAULT true,
next_retry_at TIMESTAMPTZ,
last_error TEXT,
reject_reason VARCHAR(64),
compliance_case_id UUID,
metadata JSONB NOT NULL DEFAULT '{}',
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by VARCHAR(64) NOT NULL,
CONSTRAINT uq_payments_idempotency_key UNIQUE (idempotency_key),
CONSTRAINT uq_payments_provider_ref UNIQUE (provider_id, provider_ref),
CONSTRAINT ck_payments_state CHECK (state IN (
'CREATED','PROCESSING','COMPLETED','FAILED',
'PERMANENTLY_FAILED','REJECTED','PENDING_REVIEW','CANCELLED'
)),
CONSTRAINT ck_payments_amount_positive CHECK (amount > 0),
CONSTRAINT ck_payments_currency CHECK (currency ~ '^[A-Z]{3}$'),
CONSTRAINT ck_payments_counterparty CHECK (
(to_account_id IS NOT NULL AND counterparty_name IS NULL)
OR (to_account_id IS NULL AND counterparty_name IS NOT NULL)
),
CONSTRAINT ck_payments_attempts CHECK (attempts >= 0 AND attempts <= max_attempts)
);
CREATE INDEX IF NOT EXISTS idx_payments_state
ON payments.payments(state);
CREATE INDEX IF NOT EXISTS idx_payments_from_account
ON payments.payments(from_account_id) WHERE from_account_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_payments_to_account
ON payments.payments(to_account_id) WHERE to_account_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_payments_created
ON payments.payments(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_payments_retry_pending
ON payments.payments(next_retry_at)
WHERE state = 'FAILED' AND retryable = true;
CREATE INDEX IF NOT EXISTS idx_payments_compliance_case
ON payments.payments(compliance_case_id) WHERE compliance_case_id IS NOT NULL;
CREATE OR REPLACE TRIGGER trg_payments_updated_at
BEFORE UPDATE ON payments.payments
FOR EACH ROW EXECUTE FUNCTION shared.set_updated_at();Query patterns:
- Get payment by ID: PK lookup, p99 < 5ms
- List payments with status filter:
idx_payments_state+idx_payments_created - Retry scheduler poll:
idx_payments_retry_pending(partial, PENDING rows only) - Compliance case lookup:
idx_payments_compliance_case(partial) - Provider webhook correlation:
uq_payments_provider_ref
Notes:
-
idis stored as UUID and exposed at the API aspay_<crockford-base32>(see ADR-0010). -
versionsupports optimistic locking (@Version) on the mutable payment aggregate. -
idempotency_keyis the client-supplied key (cross-referenced withplatform.idempotency_keys). The unique constraint here guards at the payments level; theidempotency_keystable guards at the HTTP request level. - No cross-schema foreign keys:
ledger_transaction_idandcompliance_case_idare stored as plain UUIDs without FK constraints, because services may be split to separate databases in later phases.
CREATE TABLE IF NOT EXISTS payments.payment_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
payment_id UUID NOT NULL,
type VARCHAR(64) NOT NULL,
payload JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
actor_type VARCHAR(16) NOT NULL,
actor_id VARCHAR(64) NOT NULL,
correlation_id UUID,
CONSTRAINT fk_payment_events_payment FOREIGN KEY (payment_id)
REFERENCES payments.payments(id) ON DELETE RESTRICT,
CONSTRAINT ck_payment_events_actor_type CHECK (actor_type IN ('USER','SERVICE','SYSTEM')),
CONSTRAINT ck_payment_events_type CHECK (type IN (
'CREATED','DECISION_APPROVED','DECISION_REJECTED','DECISION_REVIEW_REQUIRED',
'LEDGER_POSTED','SENT_TO_PROVIDER','PROVIDER_ACK','PROVIDER_TIMEOUT',
'COMPLETED','FAILED','RETRY_SCHEDULED','PERMANENTLY_FAILED',
'REVERSED','MANUAL_OVERRIDE','CANCELLED'
))
);
CREATE INDEX IF NOT EXISTS idx_payment_events_payment_occurred
ON payments.payment_events(payment_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_payment_events_correlation
ON payments.payment_events(correlation_id) WHERE correlation_id IS NOT NULL;Query patterns:
- Load event history for a payment:
idx_payment_events_payment_occurred - Correlation trace:
idx_payment_events_correlation(partial, non-null only)
Notes:
- Append-only. No updates, no deletes, no
@Version. -
payloadstores the full event data snapshot as JSONB for audit replay.
CREATE TABLE IF NOT EXISTS payments.processed_webhooks (
provider_id VARCHAR(64) NOT NULL,
provider_event_id VARCHAR(255) NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
payment_id UUID,
PRIMARY KEY (provider_id, provider_event_id)
);
CREATE INDEX IF NOT EXISTS idx_processed_webhooks_received
ON payments.processed_webhooks(received_at DESC);Query patterns:
- Dedup check on inbound webhook: PK lookup
(provider_id, provider_event_id), O(1) - Cleanup:
DELETE WHERE received_at < NOW() - INTERVAL '30 days', usesidx_processed_webhooks_received
Notes:
-
provider_idis a logical provider identifier (e.g.,sandbox,generic-bank). No real provider names are embedded in the DDL. - Combined with
platform.outbox_eventson the outbound side, this table closes the at-least-once delivery loop for inbound webhooks.
v0.1/
020-payments-payments.xml
021-payments-events.xml
022-payments-processed-webhooks.xml
- Data-Model - conventions, ERD, cross-cutting sections
- Data-Model-Ledger - ledger schema (accounts, transactions, entries)
- Data-Model-Platform - outbox, idempotency, webhook subscriptions
- Domain-Model - Payment aggregate definition
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO