Docs

Billing

Checkout creates an order from the plan row, then pays (or immediately fulfills zero-price plans). Fulfillment is idempotent.

Flow

text
Plans → Checkout → Create/reuse Order
   ↓
amount = plan.priceAmount (never from the client)
   ↓
If amount is 0 → fulfillPaidOrder → billing result
Else → payment provider (ZarinPal or mock)
   ↓
Callback / verify
   ↓
fulfillPaidOrder (idempotent) → assign plan

Orders

An order snapshots plan name, amount, and currency. Status: pending, paid, failed, cancelled. Idempotency keys prevent duplicate pending checkouts for the same attempt.

Billing history is under /billing. There is no admin “mark as paid” shortcut — paid state comes from verified payment or zero-price fulfill.

Providers

  • ZarinPal — when ZARINPAL_MERCHANT_ID is set. Sandbox defaults outside production unless ZARINPAL_SANDBOX overrides.
  • Mock — in-app simulated gateway at /billing/pay/[orderId] when merchant is unset in non-production, or when PAYMENT_PROVIDER=mock.

Browser callback GET /api/payment/callback is untrusted until server-to-server verify succeeds.