← Payments overview

Integration Notes & Schema Touchpoints

8. Integration notes

8.1 SnappPay (اسنپ‌پی) — primary

API-based with an IPG redirect. Endpoint paths are VERIFIED against the open-source Laravel package and match exactly:

POST  api/online/v1/oauth/token          → OAuth bearer token
GET   api/online/offer/v1/eligible       → eligibility / credit check on the customer
POST  api/online/payment/v1/token        → payment token  → redirect customer to SnappPay
POST  api/online/payment/v1/verify       → verify after callback
POST  api/online/payment/v1/settle       → settle (capture the merchant lump)
POST  api/online/payment/v1/revert       → full reversal
POST  api/online/payment/v1/cancel       → cancel
POST  api/online/payment/v1/update       → partial (new amount strictly lower)
GET   api/online/payment/v1/status       → status

Credentials issued only after a signed contract + business-license review: user_name, password, client_id, client_secret, merchant/customer number, security code, base_url. Sandbox availability is plausible (issued by sales) but TO BE CONFIRMED — the public package does not evidence it.

WARNING: the SnapPayInc/open-api-java-sdk GitHub repo is the unrelated CANADIAN SnapPay (snappay.ca, CAD) — do NOT use it. Likewise, English searches for "digipay split payment" return DigiPay.Guru, an unrelated white-label vendor — not the Iranian Digipay.

8.2 Digipay (دیجی‌پی) — secondary / fallback

Unified UPG gateway, server-side + hosted redirect:

POST  /digipay/api/tickets/business?type=…   → ticket + redirectUrl  (type MUST match product)
        (callback to merchant)
POST  /digipay/api/purchases/verify          → verify (re-check amount + providerId before trusting)
POST  /digipay/api/purchases/deliver?type=…  → delivery confirmation (Credit=5 / BNPL=13) — GATE ON EVV CHECK-OUT
POST  /digipay/api/refunds?type=…            → refund (providerId, amount, saleTrackingCode)
GET   /digipay/api/refunds/{InquiryId}       → poll refund status
POST  /digipay/api/reverse                   → manual reverse (~25 min, IPG/DPG only)

Type codes (VERIFIED, first-party): IPG=0, Wallet=11, Credit=5, BNPL=13, Credit-Card=24 — persist the gateway type per transaction; deliver/refund calls must carry the matching code. Each purchase supports EITHER refund OR manual reverse, not both — store a mutually-exclusive reversal-mode flag. For a service, the "delivery" is the completed visit, so gate deliver on the nurse's EVV check-out. A BNPL refund returns to the customer's Digipay credit/wallet (or bank/SHEBA), not the original card.

8.3 Cross-cutting integration rules

  • Webhook idempotency: every PSP/BNPL callback is at-least-once and retried. Upsert into payment_webhook_events keyed UNIQUE(external_event_id) first, inside the same transaction that mutates money state, and no-op on duplicate — prevents double-confirm / double-settle / double-refund.
  • Never trust the callback alone — always verify server-side and re-check amount + providerId/reference before treating funds as captured.
  • Amounts in IRR Rials as BIGINT everywhere; SnappPay/Digipay quote in Toman at the API boundary — store a currency field on the BNPL row and convert only at the boundary, never internally.
  • State-machine guard on BNPL status transitions (eligible → token_issued → verified → settled → reverted) so callbacks/retries cannot double-settle or double-refund.

9. Schema touchpoints

Final, aligned table/field names (these supersede installment_plans / installment_entries). The canonical entity definitions live in the data model: payments, ledger & refunds and BNPL.

  • bnpl_transactions (new, replaces installment_plans; installment_entries CUT) — 1:1 with a payment_transaction. Fields: payment_transaction_id FK UNIQUE, provider_code, merchant_of_record, external_payment_token, external_transaction_id, eligibility_status, order_amount_irr, settled_amount_irr (net of provider commission), bnpl_commission_irr, currency (IRR/TOMAN), status (eligible/token_issued/verified/settled/reverted/cancelled/failed), installment_count (default 4, informational only), settled_at, revert_transaction_id, reverted_amount_irr, reverted_at, refund_channel, callback_payload_json.
  • payment_transactions — keep full gateway response + Shaparak reference; ADD a filtered UNIQUE(gateway_reference_code) WHERE NOT NULL and a filtered UNIQUE(booking_id) WHERE status='succeeded' (single capture per booking; idempotent retries).
  • payment_webhook_events (new) — provider_code, event_type, external_event_id UNIQUE, payload_json, signature_valid, processing_status (received/processed/failed/ignored), related_payment_transaction_id NULL, received_at, processed_at.
  • refunds1:N per payment_transaction (the original "1:1" claim is wrong); ADD platform_fee_refunded_irr, nurse_payout_refunded_irr (fee-leg decomposition), refund_channel (psp_card/bnpl_revert/manual_bank), external_revert_reference, expected_customer_refund_eta; app invariant Σ refunded ≤ captured.
  • ledger_entries (new) — transaction_group_id, account_type (escrow_held/platform_revenue/nurse_payable/refund_payable/bnpl_fee_expense/nurse_clawback_receivable), nurse_id NULL, direction, amount_irr, booking_id NULL, source_ref_type, source_ref_id, memo, created_at. Append-only; balanced per group.
  • nurse_clawbacks (new) — nurse_id, booking_id, refund_id, amount_irr, status (pending/recovered/written_off), recovered_in_payout_id NULL, created_at, resolved_at.
  • payment_gateways — encrypted provider config in config_json / secrets: SnappPay client_id, client_secret/username+password, merchant number, security code, base_url, sandbox flag. Never store credentials per-transaction.

Supporting changes: bookings gets the three-way split (gross_price_irr, balinyaar_commission_irr, nurse_payout_amount) and dispute_window_ends_at; payout_released BIT is CUT (derive from nurse_payout_booking_links + ledger). nurse_payouts gets gross_earnings_irr, clawback_applied_irr, net_amount_irr. An invoices table (minimal) captures the commission VAT line.

↑ Back to top