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>
21 KiB
Payment Platform — Integration & Contract Reference
Normative contract reference for Torbat Pay (
paymentservice).
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_sourcedefaults from routing policy when omitted.CREDIT_PROVIDERfields are ignored until bundlepayment.future.credit_provideris active and Torbat Credit adapter is registered — validators reject if source isCREDIT_PROVIDERbefore 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 orinstallment_contract_referencefrom 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": [],
}
10. Related documents
- Payment Roadmap
- ADR-020 · ADR-021
- Module Registry — payment
- Provider Reference (PSP entries added at 14.1)
- Event Catalog