Add independent payment-service (port 8012, payment_db) with foundation licensing, BYO-PSP, merchant accounts, idempotent requests, callbacks, and immutable ledger. Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
12 KiB
Markdown
214 lines
12 KiB
Markdown
# ADR-021: Payment Platform Licensing, Routing & Integration Contracts
|
|
|
|
| Field | Value |
|
|
| --- | --- |
|
|
| Status | Accepted |
|
|
| Date | 2026-07-27 |
|
|
| Deciders | Platform Architecture |
|
|
| Supersedes | — |
|
|
| Superseded by | — |
|
|
| Extends | [ADR-020](ADR-020.md) |
|
|
|
|
## Context
|
|
|
|
[ADR-020](ADR-020.md) establishes Torbat Pay as an independent service. Before Phase `payment-14.0` implementation, the platform must define **non-breaking** contracts for:
|
|
|
|
- Tenant enable/disable of Payment without vertical code changes
|
|
- Core service entitlements vs Payment bundle licensing vs operational feature toggles
|
|
- BYO-PSP vs Torbat Pay Merchant mode selection and provider reassignment
|
|
- Versioned API/event contracts for payment intents, checkout, callbacks, settlement, and future refund/split/subscription/wallet/installment flows
|
|
- Compatibility with Core, Identity, Tenant, Accounting, CRM, Communication, Hospitality, Delivery, Sports Center, Marketplace, and future consumers
|
|
|
|
Hospitality ([ADR-017](ADR-017.md)) and Experience ([ADR-016](ADR-016.md)) already use **bundle licensing + feature toggles + Core entitlement coordination**. Payment must follow the same three-layer model to avoid rework.
|
|
|
|
## Decision
|
|
|
|
### 1. Three-layer access control (mandatory)
|
|
|
|
| Layer | Owner | Purpose | Example keys |
|
|
| --- | --- | --- | --- |
|
|
| **L1 — Core entitlement** | Core Platform | Commercial plan / tenant module enablement | `payment.module.enabled` |
|
|
| **L2 — Payment bundle** | Payment (`payment_db`) | Licensed capability packs within Payment | `byo_psp_basic`, `torbat_pay_merchant`, `marketplace_splits` |
|
|
| **L3 — Payment feature toggle** | Payment (`payment_db`) | Operational flags within an active bundle | `refunds_enabled`, `installments_enabled` |
|
|
|
|
Rules:
|
|
|
|
1. L1 must pass before any Payment API mutates state (except health/capabilities discovery when documented).
|
|
2. L2 gates API surfaces, permissions exposure, and `/capabilities` flags — **hidden bundles must not expose routes, menus, or permission leaves** (same rule as ADR-017).
|
|
3. L3 gates optional engines without new Core plan features.
|
|
4. Payment listens to Core `feature_access.changed` (event) to invalidate entitlement cache; Payment does not compute Core plans locally.
|
|
|
|
### 2. Tenant payment workspace (enable / disable)
|
|
|
|
Each tenant has at most one **PaymentWorkspace** in `payment_db`:
|
|
|
|
| Field | Purpose |
|
|
| --- | --- |
|
|
| `status` | `disabled` \| `enabled` \| `suspended` |
|
|
| `default_payment_mode` | `byo_psp` \| `torbat_pay_merchant` |
|
|
| `core_entitlement_ref` | Opaque ref to Core feature/subscription check (no FK) |
|
|
|
|
- **Enable Payment:** Core L1 `payment.module.enabled` + workspace `enabled` + at least one L2 bundle active.
|
|
- **Disable Payment:** workspace `disabled` — reject new payment requests; in-flight callbacks still processed (14.4+).
|
|
- **Mode switch:** update `default_payment_mode` + `PaymentProviderAssignment` — **no vertical or adapter code change**; routing policy resolves provider at runtime.
|
|
|
|
### 3. Provider assignment & routing (no code change on switch)
|
|
|
|
| Aggregate | Role |
|
|
| --- | --- |
|
|
| `PspProviderRegistration` | Platform catalog of adapter types (ZarinPal, IDPay, …) |
|
|
| `CreditProviderRegistration` | Platform catalog of **external credit/financing adapters** (Torbat Credit, future third-party BNPL) — **registration shell only until phased** |
|
|
| `PspConnection` | Tenant BYO-PSP credential binding |
|
|
| `CreditProviderConnection` | Tenant connection to external credit provider (future; same assignment pattern as PSP) |
|
|
| `MerchantAccount` | Torbat Pay Merchant sub-merchant (facilitator mode) |
|
|
| `PaymentProviderAssignment` | Active provider binding for a workspace/branch/context |
|
|
| `PspRoutingPolicy` | Priority, failover, amount/currency/channel/**payment_source** rules |
|
|
|
|
Tenants change PSP or credit provider by updating **assignment + routing policy** only. Adapters are selected by `provider_code` from registry — adding Torbat Credit or a new PSP is a new adapter registration, not a vertical change.
|
|
|
|
**Payment source types (routing dimension, reserved in v1):**
|
|
|
|
| `payment_source` | Meaning | Provider adapter |
|
|
| --- | --- | --- |
|
|
| `PSP` | Standard gateway capture (BYO-PSP) | `PspAdapter` |
|
|
| `MERCHANT_FACILITATOR` | Torbat Pay Merchant mode | Facilitator adapter |
|
|
| `CREDIT_PROVIDER` | External credit/BNPL/financing authorization | `CreditProviderAdapter` (**not implemented**; architecture reserved) |
|
|
|
|
Payment MUST invoke Torbat Credit through the **same provider adapter pattern** as PSPs (`initiate`, `verify`, `callback` hooks on `CreditProviderAdapter`). Payment stores **opaque refs only** — never credit scores, KYC payloads, or installment calculation logic.
|
|
|
|
### 4. Merchant ownership
|
|
|
|
- **MerchantAccount** and facilitator settlement profiles are owned solely by Payment.
|
|
- Verticals store `merchant_account_ref` or `payment_workspace_ref` UUIDs only — never PSP merchant IDs from gateways.
|
|
- Torbat Pay Merchant mode uses **platform-scoped facilitator credentials** + tenant sub-merchant identifier; BYO mode uses tenant `PspConnection` only.
|
|
|
|
### 5. Versioned integration contracts (stable v1 before implementation)
|
|
|
|
All cross-service payment integration uses versioned contracts documented in [payment-contracts.md](../../reference/payment-contracts.md):
|
|
|
|
| Contract | Version | Phase introduced | Breaking change policy |
|
|
| --- | --- | --- | --- |
|
|
| PaymentIntentContract | v1 | 14.3 | Additive fields only in v1.x; v2 requires new phase |
|
|
| CheckoutSessionContract | v1 | 14.8 | Same |
|
|
| CallbackIngressContract | v1 | 14.4 | Same |
|
|
| SettlementIntentContract | v1 | 14.9 | Same |
|
|
| RefundIntentContract | v1 | 14.6 | Reserved in v1 schema |
|
|
| SplitAllocationContract | v1 | 14.7 | Reserved in v1 schema |
|
|
| SubscriptionBillingContract | v1 | Future | Schema slot in reference doc |
|
|
| WalletTopUpContract | v1 | Future | Loyalty consumer; Payment captures only |
|
|
| InstallmentPlanContract | v1 | Future (Payment) | **Payment-side schedule shell only** — see §12 |
|
|
| CreditAuthorizationContract | v1 | Future (Payment ↔ Credit) | Schema slot; authorization refs only |
|
|
|
|
Verticals depend on **contract version + capability flags**, not Payment internal models.
|
|
|
|
### 12. Torbat Credit — reserved independent service (architecture only)
|
|
|
|
A future independent service is reserved:
|
|
|
|
| Field | Value |
|
|
| --- | --- |
|
|
| Service id | `credit` |
|
|
| Commercial product | **Torbat Credit** |
|
|
| Database | `credit_db` (reserved) |
|
|
| Event namespace | `credit.*` (**reserved for Torbat Credit** — Payment does not publish `credit.*`) |
|
|
|
|
**Boundary — InstallmentPlan ≠ Credit Engine:**
|
|
|
|
| Concern | Owner |
|
|
| --- | --- |
|
|
| Installment **schedule shell** linked to a payment request (due dates, amounts, linkage refs) | Payment (`InstallmentPlanContract` v1) |
|
|
| Installment **calculation**, credit **scoring**, **KYC/KYB**, **BNPL** underwriting, financing agreements, collections | **Torbat Credit** (`credit` service) — exclusive |
|
|
|
|
Payment MAY store opaque architectural reference fields on intents and transactions:
|
|
|
|
- `credit_provider_id` — registry id of external credit provider (e.g. Torbat Credit adapter)
|
|
- `financing_reference` — opaque id returned by credit provider for a financing agreement
|
|
- `credit_authorization_reference` — authorization/hold id from credit provider
|
|
- `installment_contract_reference` — link to Torbat Credit contract; **not** Payment-owned installment math
|
|
|
|
**Integration pattern:** Torbat Credit is consumed by Payment as an **external Credit Provider** — same adapter registry, routing policy, and assignment mechanics as PSPs. No shared database. No credit domain logic inside Payment.
|
|
|
|
**Implementation status:** Architecture references only after `payment-arch` patch. No `credit` service code, no `CREDIT_PROVIDER` routing execution, no `credit.*` event producers until Torbat Credit is registered in a future phase.
|
|
|
|
### 6. API-first and event-first (mandatory)
|
|
|
|
- **API-first:** All vertical actions go through documented REST (or internal token-gated) endpoints; no shared DB reads.
|
|
- **Event-first:** State changes that verticals care about (`paid`, `failed`, `refunded`, `settled`) publish `payment.*` events via transactional outbox ([ADR-006](ADR-006.md)). Verticals may use API poll or event subscribe — never both as source of truth without idempotency keys.
|
|
|
|
### 7. Database-per-service ([ADR-001](ADR-001.md))
|
|
|
|
Payment is sole owner of `payment_db`. Forbidden: cross-DB FKs, importing vertical models, storing vertical order rows in Payment.
|
|
|
|
### 8. Permission tree
|
|
|
|
Prefix `payment.*`. Permission leaves are **bundle-gated** (L2). Inheritance: `manage` ⊃ `create|update|delete` ⊃ `view`. Service-to-service scopes use internal tokens with `payment.requests.create` etc.
|
|
|
|
Foundation permission catalog registered in Phase 14.0; leaves grow per phase without renaming roots (additive only).
|
|
|
|
### 9. Bundle isolation
|
|
|
|
Inactive bundles:
|
|
|
|
- Return 404 or 403 (consistent per API template) for bundle-gated routes
|
|
- Omit from `/capabilities`
|
|
- Hide permission leaves from catalog export
|
|
- Do not publish bundle-specific events
|
|
|
|
### 10. Multi-tenant isolation ([ADR-003](ADR-003.md))
|
|
|
|
Every business row includes `tenant_id`. Callback URLs include tenant-routable connection id; verification rejects cross-tenant callback routing. Internal service calls require `X-Tenant-ID` + scoped token.
|
|
|
|
### 11. Service compatibility (read-only integration)
|
|
|
|
| Service | Integration | Payment must not |
|
|
| --- | --- | --- |
|
|
| **Core** | Entitlement check API; `feature_access.changed` | Own plans/subscriptions |
|
|
| **Identity** | `user_ref` / `payer_ref` on intents | Admin users or OIDC |
|
|
| **Tenant (Core)** | Workspace tied to Core tenant id | Duplicate tenant registry |
|
|
| **Accounting** | SettlementIntentContract events/API | Create JournalEntry |
|
|
| **CRM** | `contact_ref`, `organization_ref` on payer | Own CRM aggregates |
|
|
| **Communication** | Receipt/refund notify client | Own SMS/email providers |
|
|
| **Hospitality** | Checkout refs from POS/QR; subscribe to `payment.request.paid` | Own `pos_payment` PSP fields long-term |
|
|
| **Delivery** | Optional COD payment ref | Own delivery settlement |
|
|
| **Sports Center** | Membership/competition payment refs | Own membership billing |
|
|
| **Marketplace** | Checkout + split refs (14.7+) | Own vendor catalog |
|
|
| **Experience** | Paid form/checkout widget refs | Own payment capture |
|
|
| **Loyalty** | WalletTopUpContract (future) | Own wallet ledger |
|
|
| **Torbat Credit** (future) | CreditAuthorization via `CreditProviderAdapter`; subscribe to `credit.*` | Own scoring, KYC, BNPL, financing engine |
|
|
|
|
No existing service code is modified during architecture-only phases; verticals adopt Payment client contracts in their own future connector phases.
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- Tenants can enable/disable Payment, switch PSP, and switch BYO vs Merchant mode without redeploying verticals
|
|
- Future refund/split/subscription/wallet/**credit-provider** flows extend v1 contracts without breaking checkout integrations
|
|
- Torbat Credit reserved as external provider — Payment can route like any PSP when implemented
|
|
- Clear compatibility matrix for all platform services
|
|
|
|
### Negative
|
|
|
|
- Three-layer gating adds checks on every mutating API
|
|
- Contract documentation must stay synchronized with `/capabilities`
|
|
|
|
### Neutral
|
|
|
|
- Architecture phase (`payment-arch`) completes before `payment-14.0` code
|
|
- International PSPs remain adapter-only until explicitly phased
|
|
|
|
## Alternatives Considered
|
|
|
|
1. Core-only entitlement (no Payment bundles) — rejected; cannot express Torbat Pay commercial packs or hide split/refund APIs independently of Core plans.
|
|
2. PSP credentials in Core — rejected; violates ADR-020 and PCI isolation.
|
|
3. Vertical-owned checkout contracts — rejected; duplicates Marketplace/Hospitality/Sports integrations.
|
|
|
|
## Related Documents
|
|
|
|
- [ADR-020](ADR-020.md)
|
|
- [Payment Contracts Reference](../../reference/payment-contracts.md)
|
|
- [Payment Roadmap](../../payment-roadmap.md)
|
|
- [Module Boundaries](../module-boundaries.md)
|
|
- [Authorization Architecture](../authorization-architecture.md)
|
|
- [Services Contracts](../../reference/services-contracts.md)
|