[← Payments overview](index.md)

# 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](../data-model/06-payments-ledger-and-refunds.md) and [BNPL](../data-model/08-bnpl.md).

- **`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.
