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-sdkGitHub 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_eventskeyedUNIQUE(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
verifyserver-side and re-checkamount+providerId/reference before treating funds as captured. - Amounts in IRR Rials as
BIGINTeverywhere; SnappPay/Digipay quote in Toman at the API boundary — store acurrencyfield 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, replacesinstallment_plans;installment_entriesCUT) — 1:1 with apayment_transaction. Fields:payment_transaction_idFK 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 filteredUNIQUE(gateway_reference_code) WHERE NOT NULLand a filteredUNIQUE(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_idNULL,received_at,processed_at.refunds— 1:N perpayment_transaction(the original "1:1" claim is wrong); ADDplatform_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_idNULL,direction,amount_irr,booking_idNULL,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_idNULL,created_at,resolved_at.payment_gateways— encrypted provider config inconfig_json/ secrets: SnappPayclient_id,client_secret/username+password, merchant number, security code,base_url,sandboxflag. 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.