# Payment Platform — Integration & Contract Reference > **Normative contract reference** for Torbat Pay (`payment` service). > Architecture: [ADR-020](../architecture/adr/ADR-020.md), [ADR-021](../architecture/adr/ADR-021.md) > Roadmap: [payment-roadmap.md](../payment-roadmap.md) All contracts are **versioned**. Implementations MUST accept unknown additive JSON fields. Breaking changes require a new major version and a registered phase. --- ## 1. Architectural verification checklist | Requirement | How Torbat Pay satisfies it | | --- | --- | | **API-first** | All vertical integration via REST `/api/v1/*` and token-gated `/internal/v1/*`; no shared DB | | **Event-first** | Domain transitions publish `payment.*` via transactional outbox ([ADR-006](../architecture/adr/ADR-006.md)) | | **Database-per-service** | Sole owner of `payment_db` ([ADR-001](../architecture/adr/ADR-001.md)) | | **Feature toggles** | L3 `PaymentFeatureToggle` in Payment; operational flags within bundles | | **Permission tree** | `payment.*` bundle-gated leaves; see §8 | | **Bundle isolation** | Inactive bundles → hidden APIs, capabilities, permissions ([ADR-021](../architecture/adr/ADR-021.md)) | | **Multi-tenant isolation** | `tenant_id` on all rows; callback routing validated ([ADR-003](../architecture/adr/ADR-003.md)) | | **Tenant enable/disable** | Core L1 + PaymentWorkspace.status | | **Choose own PSP** | BYO `PspConnection` + `PaymentProviderAssignment` | | **Choose Torbat Pay Merchant** | `default_payment_mode=torbat_pay_merchant` + `MerchantAccount` | | **Change provider without code** | Update `PaymentProviderAssignment` / `PspRoutingPolicy` only | --- ## 2. Three-layer licensing model ### L1 — Core service entitlement | Feature key | Meaning | | --- | --- | | `payment.module.enabled` | Tenant may use Torbat Pay at all | Check: `POST /api/v1/tenants/{tenant_id}/features/check` (Core) — see [services-contracts.md](services-contracts.md). Payment caches result; invalidates on `feature_access.changed`. ### L2 — Payment bundles (owned by Payment) | Bundle code | Capabilities unlocked | Typical mode | | --- | --- | --- | | `payment.byo_psp.basic` | PSP connections, routing, payment requests, callbacks, ledger | BYO-PSP | | `payment.torbat_pay.merchant` | Merchant accounts, facilitator routing, payment requests, callbacks, ledger | Torbat Pay Merchant | | `payment.marketplace.splits` | Split rules, settlement batches | Facilitator + marketplace | | `payment.advanced.refunds` | Refund APIs (14.6+) | Either mode | | `payment.advanced.reconciliation` | Reconciliation + accounting intents (14.9+) | Either mode | | `payment.future.subscriptions` | SubscriptionBillingContract (reserved) | Either mode | | `payment.future.installments` | InstallmentPlanContract (reserved) | Either mode — **Payment schedule shell only** | | `payment.future.credit_provider` | CREDIT_PROVIDER routing via external credit adapters | Either mode — **not implemented** | | `payment.future.wallet_topup` | WalletTopUpContract with Loyalty (reserved) | Either mode | Aggregates: `PaymentBundleDefinition`, `TenantPaymentBundle` (activation refs Core entitlement only). ### L3 — Payment feature toggles (owned by Payment) | Toggle key | Default | Requires bundle | | --- | --- | --- | | `payment.capture.enabled` | true when workspace enabled | any active L2 | | `payment.refunds.enabled` | false | `payment.advanced.refunds` | | `payment.splits.enabled` | false | `payment.marketplace.splits` | | `payment.installments.enabled` | false | `payment.future.installments` | | `payment.subscriptions.enabled` | false | `payment.future.subscriptions` | | `payment.wallet_topup.enabled` | false | `payment.future.wallet_topup` | | `payment.credit_provider.enabled` | false | `payment.future.credit_provider` | | `payment.fraud_hooks.enabled` | false | any active L2 | --- ## 3. Provider assignment & routing ### PaymentProviderAssignment Links a workspace (and optional scope) to the active payment source: ```json { "assignment_id": "uuid", "tenant_id": "uuid", "payment_workspace_id": "uuid", "scope_type": "workspace|branch|vertical", "scope_ref_id": "uuid|null", "source_type": "psp_connection|merchant_account", "source_id": "uuid", "priority": 1, "is_active": true } ``` **Changing PSP or credit provider:** deactivate old assignment, activate new — routing policy selects at request time. No adapter code change in verticals. ### Payment source types (routing — reserved) | Value | Description | Adapter | Status | | --- | --- | --- | --- | | `PSP` | BYO payment gateway | `PspAdapter` | Phase 14.1+ | | `MERCHANT_FACILITATOR` | Torbat Pay Merchant | Facilitator adapter | Phase 14.2+ | | `CREDIT_PROVIDER` | External credit/BNPL/financing | `CreditProviderAdapter` | **Reserved — not implemented** | When `payment_source=CREDIT_PROVIDER`, Payment routes to an external **Credit Provider** (primary reserved: **Torbat Credit**) using the same assignment and adapter registry pattern as PSPs. ### Credit provider architectural references (opaque refs only) | Field | Type | Owner of truth | | --- | --- | --- | | `credit_provider_id` | uuid | Payment registry → external provider | | `financing_reference` | string | Torbat Credit | | `credit_authorization_reference` | string | Torbat Credit | | `installment_contract_reference` | string | Torbat Credit | ### PspRoutingPolicy ```json { "policy_id": "uuid", "tenant_id": "uuid", "rules": [ { "priority": 1, "provider_assignment_id": "uuid", "conditions": { "currency": ["IRR"], "payment_source": ["PSP", "MERCHANT_FACILITATOR", "CREDIT_PROVIDER"], "min_amount_minor": 0, "max_amount_minor": null, "channel": ["web", "mobile", "pos"] } } ], "failover_enabled": true } ``` --- ## 4. Contract catalog ### 4.1 PaymentIntentContract v1 (Phase 14.3+) **Purpose:** Vertical initiates payment; Payment returns redirect/token payload. **HTTP:** `POST /api/v1/payment-requests` **Headers:** `Authorization`, `X-Tenant-ID`, `Idempotency-Key` (required) ```json { "contract_version": "payment_intent.v1", "amount_minor": 150000, "currency": "IRR", "idempotency_key": "string", "source": { "service": "hospitality|marketplace|sports_center|experience|crm|delivery|accounting|other", "ref_type": "pos_ticket|order|membership|invoice|checkout_session|quote|other", "ref_id": "uuid" }, "payer": { "contact_ref": "uuid|null", "user_ref": "uuid|null", "mobile": "string|null" }, "payment_mode": "byo_psp|torbat_pay_merchant|default", "payment_source": "PSP|MERCHANT_FACILITATOR|CREDIT_PROVIDER", "provider_assignment_id": "uuid|null", "credit_provider_id": "uuid|null", "financing_reference": "string|null", "credit_authorization_reference": "string|null", "installment_contract_reference": "string|null", "return_url": "https://...", "metadata": {}, "line_items": [ { "description": "string", "amount_minor": 150000, "quantity": 1 } ] } ``` **Response 201:** ```json { "payment_request_id": "uuid", "status": "pending|redirect_issued", "redirect_url": "https://...", "token": "string|null", "expires_at": "ISO-8601" } ``` **Notes:** - `payment_source` defaults from routing policy when omitted. - `CREDIT_PROVIDER` fields are **ignored** until bundle `payment.future.credit_provider` is active and Torbat Credit adapter is registered — validators reject if source is `CREDIT_PROVIDER` before implementation. - Verticals pass refs only; Torbat Credit owns authorization lifecycle. **Events:** `payment.request.created`, `payment.request.redirect_issued`, `payment.request.paid`, `payment.request.failed` --- ### 4.2 CheckoutSessionContract v1 (Phase 14.8+) **Purpose:** Higher-level checkout wrapper for embeddable/widget flows. **HTTP:** `POST /api/v1/checkout-sessions` ```json { "contract_version": "checkout_session.v1", "source": { "service": "string", "ref_type": "string", "ref_id": "uuid" }, "amount_minor": 0, "currency": "IRR", "success_url": "https://...", "cancel_url": "https://...", "metadata": {}, "payment_intent_overrides": {} } ``` Completing session creates/links a PaymentIntent internally. Verticals SHOULD prefer CheckoutSession for UX flows; low-level verticals MAY call PaymentIntent directly. --- ### 4.3 CallbackIngressContract v1 (Phase 14.4+) **Purpose:** PSP → Payment webhook ingress (adapter-normalized). **HTTP:** `POST /api/v1/callbacks/{provider_code}/{connection_id}` Adapter normalizes to: ```json { "contract_version": "callback_ingress.v1", "provider_code": "zarinpal|idpay|...", "connection_id": "uuid", "provider_transaction_id": "string", "payment_request_id": "uuid|null", "raw_status": "string", "normalized_status": "paid|failed|pending", "amount_minor": 0, "currency": "IRR", "signature": "string", "received_at": "ISO-8601" } ``` **Events:** `payment.callback.received`, `payment.callback.verified`, `payment.request.paid` --- ### 4.4 SettlementIntentContract v1 (Phase 14.9+) **Purpose:** Payment → Accounting (no journal in Payment). **Event:** `payment.accounting_intent.created` ```json { "contract_version": "settlement_intent.v1", "intent_id": "uuid", "tenant_id": "uuid", "intent_type": "capture|fee|facilitator_payout|reconciliation_adjustment", "payment_transaction_id": "uuid", "amount_minor": 0, "currency": "IRR", "accounting_refs": { "customer_ref": "uuid|null", "revenue_account_code_ref": "string|null" }, "idempotency_key": "string", "occurred_at": "ISO-8601" } ``` Accounting consumes and posts via Posting Engine ([ADR-010](../architecture/adr/ADR-010.md)). --- ### 4.5 RefundIntentContract v1 (Phase 14.6+ — schema reserved) **HTTP:** `POST /api/v1/refund-requests` ```json { "contract_version": "refund_intent.v1", "payment_transaction_id": "uuid", "amount_minor": 0, "reason": "string", "source_ref": { "service": "string", "ref_type": "string", "ref_id": "uuid" }, "idempotency_key": "string" } ``` **Events:** `payment.refund.requested`, `payment.refund.succeeded`, `payment.refund.failed` --- ### 4.6 SplitAllocationContract v1 (Phase 14.7+ — schema reserved) Embedded in capture or post-capture allocation: ```json { "contract_version": "split_allocation.v1", "payment_transaction_id": "uuid", "allocations": [ { "party_ref": { "type": "merchant_account|seller_ref|platform", "id": "uuid" }, "amount_minor": 0, "description": "string" } ], "remainder_to": "platform|primary_merchant" } ``` **Events:** `payment.split.allocated`, `payment.settlement_batch.created` --- ### 4.7 SubscriptionBillingContract v1 (Future — schema reserved) ```json { "contract_version": "subscription_billing.v1", "subscription_ref": { "service": "string", "ref_id": "uuid" }, "billing_cycle": "monthly|annual|custom", "amount_minor": 0, "currency": "IRR", "payment_method_assignment_id": "uuid", "trial_days": 0 } ``` Recurring charge execution deferred to future Payment phase; contract stable for Marketplace/CRM/Experience planning. --- ### 4.8 WalletTopUpContract v1 (Future — schema reserved) Payment captures external money; Loyalty owns wallet ledger ([ADR-011](../architecture/adr/ADR-011.md)). ```json { "contract_version": "wallet_topup.v1", "loyalty_wallet_account_ref": "uuid", "amount_minor": 0, "currency": "IRR", "payment_intent_id": "uuid" } ``` **Event (handoff):** `payment.transaction.recorded` with `metadata.intent_kind=wallet_topup` — Loyalty subscribes. --- ### 4.9 InstallmentPlanContract v1 (Future — Payment schedule shell only) > **Not the Credit Engine.** Installment **calculation**, credit **scoring**, **KYC**, **BNPL** underwriting, and financing agreements are exclusive to the future **Torbat Credit** service (`credit`). This contract is a Payment-side **schedule shell** linked to a payment request or `installment_contract_reference` from Torbat Credit. ```json { "contract_version": "installment_plan.v1", "source_ref": { "service": "string", "ref_type": "order|invoice", "ref_id": "uuid" }, "total_amount_minor": 0, "currency": "IRR", "installments": [ { "sequence": 1, "due_at": "ISO-8601", "amount_minor": 0, "payment_request_ref": "uuid|null" } ], "provider_assignment_id": "uuid", "payment_source": "PSP|CREDIT_PROVIDER", "credit_provider_id": "uuid|null", "installment_contract_reference": "string|null" } ``` Payment records due dates and links to capture requests; Torbat Credit owns financing terms and authorization. --- ### 4.10 CreditProviderAdapter pattern (Future — architecture reserved) Torbat Credit (`credit` service, commercial product **Torbat Credit**) integrates as an **external Credit Provider** — invoked by Payment identically to a PSP adapter: | Adapter method | Purpose | | --- | --- | | `authorize_financing()` | Reserve credit / BNPL authorization | | `capture_financing()` | Capture against authorization | | `verify_authorization()` | Reconcile status | | `parse_callback()` | Normalize credit provider webhooks | Registry entry example: ```json { "provider_code": "torbat_credit", "provider_family": "credit_bnpl", "service_ref": "credit", "adapter_version": "1", "supports_payment_source": "CREDIT_PROVIDER" } ``` Payment calls Torbat Credit via **internal service API + adapter protocol** — never direct DB access. --- ### 4.11 Reserved event namespace — `credit.*` (Torbat Credit only) The **`credit.*` event namespace is reserved** for the future independent **Torbat Credit** service. Payment **does not** publish or consume `credit.*` events in current or HIGH-priority phases. Examples (documentation only — no implementation): | event_type | Owner | | --- | --- | | `credit.application.submitted` | Torbat Credit | | `credit.authorization.approved` | Torbat Credit | | `credit.authorization.declined` | Torbat Credit | | `credit.financing.created` | Torbat Credit | | `credit.installment.due` | Torbat Credit | | `credit.installment.paid` | Torbat Credit | Payment integrates via adapter callbacks and opaque refs; cross-service coordination uses `payment.*` events on the Payment side. --- ## 5. Service compatibility matrix | Service | Consumes (API) | Consumes (Events) | Provides to Payment | Compatible | Notes | | --- | --- | --- | --- | --- | --- | | **Core** | — | `feature_access.changed` | Entitlement check API | ✅ | L1 gate; service registry | | **Identity** | — | — | `user_ref` validation (optional) | ✅ | Payer refs only | | **Tenant (Core)** | — | `tenant.suspended` | Tenant id context | ✅ | Suspend → workspace suspend | | **Accounting** | Posting intent ingest (future) | `payment.accounting_intent.created` | COA/account refs | ✅ | No journal in Payment | | **CRM** | — | optional | `contact_ref` | ✅ | Payer linkage | | **Communication** | Send notify (client) | — | Templates | ✅ | Receipt/refund SMS | | **Hospitality** | PaymentIntent/Checkout | `payment.request.paid` | `pos_ticket` refs | ✅ | Replace POS PSP embed long-term | | **Delivery** | PaymentIntent (optional) | `payment.request.paid` | COD job refs | ✅ | Optional COD | | **Sports Center** | PaymentIntent | `payment.request.paid` | membership refs | ✅ | No local PSP | | **Marketplace** | Checkout + Split | `payment.*` | seller/order refs | ✅ | Splits need bundle L2 | | **Experience** | CheckoutSession | `payment.request.paid` | form/checkout refs | ✅ | Paid forms/widgets | | **Loyalty** | WalletTopUp (future) | `payment.transaction.recorded` | wallet refs | ✅ | Wallet ledger stays Loyalty | | **Torbat Credit** (future) | CreditProviderAdapter from Payment | `credit.*` (Credit service only) | authorization/financing refs | ✅ | Scoring/KYC/BNPL exclusive to Credit | | **Healthcare** | PaymentIntent (future) | `payment.request.paid` | appointment refs | ✅ | Same pattern as Beauty | | **Beauty Business** | PaymentIntent (future) | `payment.request.paid` | appointment refs | ✅ | Same pattern | | **Automation** | — | `payment.transaction.recorded` | — | ✅ | Trigger workflows | **No breaking changes required** in listed services before `payment-14.0`; vertical connector phases adopt contracts incrementally (Payment 14.8). --- ## 6. Event catalog (Payment — planned) | event_type | When | Primary consumers | | --- | --- | --- | | `payment.workspace.enabled` | Workspace activated | Core audit | | `payment.workspace.disabled` | Payment disabled for tenant | Verticals cache | | `payment.bundle.activated` | L2 bundle on | Frontend capabilities | | `payment.feature_toggle.changed` | L3 toggle | Internal | | `payment.psp_connection.activated` | BYO PSP live | — | | `payment.merchant_account.activated` | Facilitator ready | — | | `payment.provider_assignment.changed` | PSP switch | — | | `payment.request.created` | Intent created | Vertical | | `payment.request.paid` | Verified paid | Hospitality, Marketplace, … | | `payment.request.failed` | Failed/expired | Vertical | | `payment.transaction.recorded` | Ledger write | Accounting prep, Loyalty | | `payment.refund.succeeded` | Refund done | Vertical, Communication | | `payment.split.allocated` | Split computed | Marketplace | | `payment.accounting_intent.created` | Settlement intent | Accounting | | `payment.reconciliation.completed` | Recon done | Admin | Full envelope: [event-catalog.md](event-catalog.md#payment-platform-planned). --- ## 7. Internal service API (token-gated) | Scope | Path | Purpose | | --- | --- | --- | | `payment.requests.create` | POST `/internal/v1/payment-requests` | Vertical server-side initiate | | `payment.transactions.read` | GET `/internal/v1/payment-transactions/{id}` | Status poll | | `payment.capabilities.read` | GET `/internal/v1/capabilities` | Service discovery | Requires Internal Service Token ([services-contracts.md](services-contracts.md)) + `X-Tenant-ID`. --- ## 8. Permission tree (foundation + growth) ``` payment.* ├── payment.workspaces.{view,manage,enable,disable} ├── payment.bundles.{view,manage} ├── payment.feature_toggles.{view,manage} ├── payment.configurations.{view,manage} ├── payment.settings.{view,manage} ├── payment.psp_providers.view ├── payment.psp_connections.{view,create,update,delete,test,manage} [bundle: byo_psp.basic] ├── payment.psp_routing.{view,manage} [bundle: byo_psp.basic] ├── payment.provider_assignments.{view,manage} ├── payment.merchant_accounts.{view,create,update,submit,activate,suspend,close,manage} [bundle: torbat_pay.merchant] ├── payment.requests.{view,create,cancel,manage} ├── payment.callbacks.view ├── payment.transactions.{view,export} ├── payment.ledger.view ├── payment.refunds.{view,create,manage} [bundle: advanced.refunds] ├── payment.splits.{view,manage} [bundle: marketplace.splits] ├── payment.settlement_batches.{view,manage} ├── payment.connectors.manage ├── payment.checkout_sessions.{view,create} ├── payment.reconciliation.{view,run,manage} [bundle: advanced.reconciliation] ├── payment.accounting_intents.view ├── payment.analytics.{view,refresh} ├── payment.fraud_rules.manage └── payment.audit.view ``` --- ## 9. Capability discovery (`GET /capabilities`) Response includes (when implemented): ```json { "service": "payment", "phase": "14.x", "workspace_status": "enabled|disabled", "default_payment_mode": "byo_psp|torbat_pay_merchant", "active_bundles": ["payment.byo_psp.basic"], "feature_toggles": { "payment.refunds.enabled": false }, "contract_versions": { "payment_intent": "v1", "checkout_session": "v1", "settlement_intent": "v1", "installment_plan": "v1", "credit_authorization": "v1" }, "payment_sources_supported": ["PSP", "MERCHANT_FACILITATOR"], "payment_sources_reserved": ["CREDIT_PROVIDER"], "credit_providers_registered": [], } ``` --- ## 10. Related documents - [Payment Roadmap](../payment-roadmap.md) - [ADR-020](../architecture/adr/ADR-020.md) · [ADR-021](../architecture/adr/ADR-021.md) - [Module Registry — payment](../module-registry.md#payment) - [Provider Reference](provider-reference.md) (PSP entries added at 14.1) - [Event Catalog](event-catalog.md)