← 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`.
- `refunds` — 1: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