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