Files
clinicpro/docs/api/payment.md
T

10 KiB

Payment API

Prefix: /api/v1/payment, /api/v1/subscription-payment
Supported Gateways: mellat (Mellat Bank SOAP) | sep (SEP REST)


GET /api/v1/payment/config

دریافت تنظیمات عمومی پرداخت — برای نمایش وضعیت درگاه آزمایشی در frontend.

Permission: IS_AUTHENTICATED_FULLY

Response 200

{
  "success": true,
  "data": {
    "test_mode": true,
    "appointment_fee_rials": 150000
  }
}
Field Type Description
test_mode boolean true = درگاه آزمایشی فعال است — backend از MockGateway استفاده می‌کند و پول واقعی کسر نمی‌شود
appointment_fee_rials integer مبلغ هر نوبت به ریال (از تنظیمات سایت، کلید appointment_fee_rials). frontend برای نمایش «مبلغ قابل پرداخت» از این می‌خواند؛ مبلغِ واقعیِ تراکنش هم در backend از همین کلید خوانده می‌شود (نه از client)

نکته: این endpoint هیچ اطلاعات حساسی (terminal_id، password، ...) را expose نمی‌کند. تنها یک boolean برای مصرف frontend است.


GET /api/v1/my/payments

List the authenticated user's own payments (derived from the token — there is no userId in the URL). Used by the public dashboard's transactions tab.

Permission: IS_AUTHENTICATED_FULLY

Query Parameters

Param Type Default Description
page integer 1 Page number
limit integer 20 Items per page (max 100)
status string Optional filter: pending / success / failed / canceled / refunded

Response 200 (paginated)

{
  "success": true,
  "data": [
    {
      "uuid": "pay-uuid-...",
      "order_id": "ORD-84FB82E12A4A45CE",
      "amount_rials": 590000,
      "status": "success",
      "gateway": "mellat",
      "type": "appointment",
      "reference_id": "...",
      "appointment_uuid": "appt-uuid-...",
      "created_at": 1781521834
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}

Items at data (flat array), total at meta.totalRecords. Ordered by created_at DESC.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token

POST /api/v1/payment/appointment

Initiate payment for an appointment. Returns a redirect URL to the payment gateway.

Permission: AUTH — must be the appointment owner (patient)

Request Body (application/json)

{
  "appointment_uuid": "appt-uuid-...",
  "gateway": "mellat",
  "frontend_address": "https://myapp.com/payment/result"
}
Field Type Required Description
appointment_uuid string (UUID) Appointment to pay for
gateway string "mellat" or "sep"
frontend_address string Redirect URL after payment (overrides default)

Response 200

{
  "success": true,
  "data": {
    "payment_uuid": "pay-uuid-...",
    "redirect_url": "https://bpm.shaparak.ir/pgwchannel/...",
    "order_id": "CLINICPRO-1717000000-ABC123"
  }
}

مبلغ نوبت: مبلغِ پرداخت از کلید appointment_fee_rials تنظیمات سایت خوانده می‌شود (نه از client و نه hardcode). برای تغییر، در /admin/settings ویرایش کنید.

On successful callback for an appointment payment, the booking is transitioned pending → confirmed (its 15-minute expires_at is cleared) and a confirmation SMS is dispatched to the patient's mobile. If the booking already lapsed to expired before payment confirmed, it is not re-confirmed (the transition is rejected) — handle refund out of band.

پورسانت نماینده: اگر پزشک نوبت representation_id داشته باشد و appointment_commission_enabled=1 باشد، پس از confirm شدن CommissionService هزینه پنل پیامک و مالیات را کسر و سهم نماینده را به کیف‌پولش واریز می‌کند (ردیف FinancialBreakdown ثبت می‌شود). برای پرداخت اشتراک هم اگر upgrade_commission_enabled=1 و پزشک/کلینیک representation_id داشته باشد همین منطق با درصد upgrade_commission_percent اعمال می‌شود. کلیدهای تنظیمات و ترتیب محاسبه در docs/api/admin.md.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not the patient
ERR_PAYMENT_003 422 Appointment not in payable state
ERR_PAYMENT_002 422 Invalid amount
ERR_PAYMENT_001 503 Payment gateway unavailable

POST /api/v1/payment/callback/{gateway}

GET /api/v1/payment/callback/{gateway}

Payment gateway callback. Called by the bank after user completes (or cancels) payment.

Permission: PUBLIC — called by the gateway, not the user

Path Parameters

Param Type Description
gateway string mellat or sep

Request (varies by gateway)

Mellat POST fields:

ResCode=0&SaleOrderId=...&SaleReferenceId=...&RefId=...

SEP POST fields:

Status=2&RRN=...&RefNum=...&TerminalId=...&TraceNo=...

Response

After verifying the gateway result, the backend redirects the user back to the origin site (frontend_address) with the outcome appended as query params:

{frontend_address}?payment_uuid={uuid}&status={status}
  • Success (verify ok): payment → success, then the type-specific action runs (appointment → confirmed, subscription → activated, sms_wallet → credited).
  • User canceled (e.g. Mellat ResCode=17, SEP State=CanceledByUser, mock cancel=1): payment → canceled. The gateway circuit-breaker is not marked as failed (it's a user choice, not a gateway fault).
  • Failed (any other unsuccessful verify): payment → failed, circuit-breaker records a failure.

If frontend_address is empty, a JSON body { success, payment } is returned instead of a redirect.

Notes

  • On appointment success: status → confirmed, its 15-minute expires_at cleared, confirmation SMS dispatched.
  • Payment record stores: order_id, amount_rials, status, gateway, reference_id, frontend_address, callback_ip.
  • Same flow for all clients (the main site and every consumer site) — the only per-client difference is frontend_address, which is validated against an allowlist (see below) to prevent open redirects.

POST /api/v1/subscription-payment

Initiate a subscription / wallet top-up payment (not tied to a specific appointment).

Permission: AUTH

Request Body

{
  "gateway": "mellat",
  "amount_rials": 1000000,
  "frontend_address": "https://myapp.com/wallet/result"
}
Field Type Required Description
gateway string "mellat" or "sep"
amount_rials integer Amount in Rials (min: 10,000)
frontend_address string Redirect URL after payment

Response 200

{
  "success": true,
  "data": {
    "payment_uuid": "pay-uuid-...",
    "redirect_url": "https://bpm.shaparak.ir/pgwchannel/...",
    "order_id": "CLINICPRO-SUB-1717000000-XYZ"
  }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_PAYMENT_002 422 Invalid amount
ERR_PAYMENT_001 503 Gateway unavailable

POST/GET /api/v1/subscription-payment/callback/{gateway}

Callback for subscription payments. Same behavior as appointment callback but credits wallet instead.

Permission: PUBLIC


GET /api/v1/payment/{uuid}

Get payment status and details.

Permission: AUTH — must be the payment owner or ROLE_ADMIN

Path Parameters

Param Type Description
uuid string (UUID) Payment UUID

Response 200

{
  "success": true,
  "data": {
    "uuid": "pay-uuid-...",
    "order_id": "CLINICPRO-1717000000-ABC123",
    "amount_rials": 500000,
    "status": "paid",
    "gateway": "mellat",
    "ref_id": "123456789",
    "appointment_uuid": "appt-uuid-...",
    "created_at": 1717000000,
    "paid_at": 1717000120
  }
}

Payment Status Values:

Value Description
pending Transaction created, awaiting payment
success Successfully paid and verified
failed Gateway returned a failure
canceled User canceled at the gateway, or the payment window lapsed (booking expired)
refunded Refunded

canceled is set in two cases: (1) the gateway callback reports a user cancellation, and (2) the appointment's 15-minute payment window lapses — the scheduled expiry job marks the booking expired and its pending payment canceled.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not the owner
ERR_NOT_FOUND_001 404 Payment not found

Sandbox (test) mode & origin allowlist

Test mode: when payment_test_mode (in admin settings) is 1, every initiation resolves to the internal MockGateway — no bank call is made, the transaction is simulated and verified inside the system, and the user is redirected back to frontend_address exactly like a real payment. GET /api/v1/payment/config exposes this as test_mode.

Origin allowlist: frontend_address (the origin site the user returns to) must match an allowed host, to prevent open redirects. The allowlist is read from the payment_allowed_frontend_hosts site setting (comma-separated hosts), falling back to the ALLOWED_FRONTEND_HOSTS env var when the setting is empty. Manage it via PATCH /api/v1/admin/settings — so a new consumer site can be allowed without a code or .env change. A non-allowed host yields 422 ERR_VALIDATION_001 (field: frontend_address).