TorbatYar/docs/reference/payment-contracts.md
Mortezakoohjani 9fac160258 feat(payment): ship Torbat Pay MVP phases 14.0-14.5
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>
2026-07-27 17:57:04 +03:30

21 KiB

Payment Platform — Integration & Contract Reference

Normative contract reference for Torbat Pay (payment service).
Architecture: ADR-020, ADR-021
Roadmap: 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)
Database-per-service Sole owner of payment_db (ADR-001)
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)
Multi-tenant isolation tenant_id on all rows; callback routing validated (ADR-003)
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.

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:

{
  "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

{
  "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)

{
  "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:

{
  "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

{
  "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:

{
  "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

{
  "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).


4.5 RefundIntentContract v1 (Phase 14.6+ — schema reserved)

HTTP: POST /api/v1/refund-requests

{
  "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:

{
  "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)

{
  "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).

{
  "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.

{
  "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:

{
  "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.


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) + 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):

{
  "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": [],
}