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