# 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)