feat(payment): unify payment callback endpoint for all gateways and types

This commit is contained in:
hamed
2026-08-09 16:02:48 +03:30
parent a6a965a2aa
commit 2471c90cbb
10 changed files with 205 additions and 71 deletions
+27 -14
View File
@@ -58,7 +58,7 @@
**کمیسیون دامنه‌محور (post-action):** نماینده‌ی مبدأ از `payment.frontend_address` با `DomainContextResolver` تعیین می‌شود؛ کمیسیون (نوبت و اشتراک) فقط وقتی ثبت می‌شود که این نماینده فعال باشد **و** پزشک/کلینیک موضوع خرید `representation_id` همان نماینده را داشته باشد — جزئیات در `docs/api/representation.md` §قانون کمیسیون دامنه‌محور.
**یکدستیِ typeها:** هر سه نوع (`appointment`/`subscription`/`sms_wallet`) از همان `GET /payment/pay/{orderId}` عبور می‌کنند؛ `PaymentManager::callbackUrl()` پیشوند callback را بر اساس `type` انتخاب می‌کند. POST این endpointها فقط `Payment` pending می‌سازد و `pay_url` برمی‌گرداند (نه `redirect_url`).
**یکدستیِ typeها:** هر سه نوع (`appointment`/`subscription`/`sms_wallet`) از همان `GET /payment/pay/{orderId}` عبور می‌کنند و روی همان یک `POST|GET /api/v1/payment/callback` برمی‌گردند؛ `PaymentManager::callbackUrl()` دیگر بر اساس `type` شاخه نمی‌زند. POST این endpointها فقط `Payment` pending می‌سازد و `pay_url` برمی‌گرداند (نه `redirect_url`).
**افزودن درگاه جدید (Open/Closed):** یک کلاس جدید implements `PaymentGatewayInterface` بساز، در `GatewayFactory::$gateways` + `LABELS` ثبت کن. `PaymentController`/`PaymentManager` تغییر نمی‌کنند.
@@ -268,23 +268,37 @@ Initiate payment for an appointment. Returns a redirect URL to the payment gatew
---
## POST `/api/v1/payment/callback/{gateway}`
## GET `/api/v1/payment/callback/{gateway}`
## POST `/api/v1/payment/callback`
## GET `/api/v1/payment/callback`
Payment gateway callback. Called by the bank after user completes (or cancels) payment. هر درگاه callback مخصوص خودش را دارد؛ URL آن هنگام `initiate` از `APP_BASE_URL` ساخته می‌شود:
Payment gateway callback. Called by the bank after user completes (or cancels) payment.
**یک آدرس برای همه.** همهٔ درگاه‌ها (`mellat`، `sep`، `mock`) و همهٔ نوع‌های پرداخت
(`appointment`، `subscription`، `sms_wallet`) روی همین یک مسیر برمی‌گردند؛ درگاه و سفارش
به‌صورت query param می‌روند. URL هنگام `initiate` در `PaymentManager::callbackUrl()` از
`APP_BASE_URL` و ثابت `PaymentManager::CALLBACK_PATH` ساخته می‌شود:
```
{APP_BASE_URL}/api/v1/payment/callback/{gateway}?order_id={orderId}
{APP_BASE_URL}/api/v1/payment/callback?gateway={gateway}&order_id={orderId}
```
دلیل: مسیر ثابت می‌ماند، پس آدرسِ ثبت‌شده در پنل پذیرندگی بانک با اضافه‌شدن درگاه یا نوع
پرداخت جدید عوض نمی‌شود.
> **Breaking change.** دو مسیر قدیمی حذف شده‌اند و `404` می‌دهند:
> `POST|GET /api/v1/payment/callback/{gateway}` و
> `POST|GET /api/v1/subscription-payment/callback/{gateway}`.
> آدرس ثبت‌شده در پنل ملت و سپ باید به مسیر جدید به‌روز شود.
**نکته IPG ملت:** طبق راهنمای درگاه ملت، `callBackUrl` باید روی **دامنهٔ ثبت‌شدهٔ پذیرنده** باشد و **IP مجاز نیست** (در غیر این صورت کد پاسخ `62` — «مسیر back call در دامنهٔ ثبت‌شده نیست»). بنابراین `APP_BASE_URL` در پروداکشن باید دقیقاً `https://clinic-pro.ir` (دامنهٔ ثبت‌شده نزد ملت/شاپرک) باشد.
**Permission:** `PUBLIC`. **نکتهٔ مهم:** درگاه‌های **ملت و سپ** نتیجه را با **ریدایرکتِ مرورگرِ کاربر** (POST/GET) برمی‌گردانند، نه server-to-server؛ پس IP دریافتی، IPِ کاربر است و **allowlist شاپرک اعمال نمی‌شود** (برای `gateway ∈ {mellat, sep}` و نیز `test_mode`). در غیر این صورت هر callback واقعی — از جمله «لغو» توسط کاربر — با «دسترسی غیرمجاز» رد می‌شد. امنیت از طریق **چک ضد-دستکاری** (`RefId==gateway_token`، `SaleOrderId==payment.id`) و **verify سمت بانک** در `PaymentManager` تأمین می‌شود. `isAllowedCallbackIp` فقط برای درگاه‌های آیندهٔ server-to-server معنی دارد.
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `gateway` | string | `mellat` or `sep` |
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `order_id` | string | yes | شناسهٔ سفارش (`ORD-…`)؛ در نبودش `ResNum` خوانده می‌شود |
| `gateway` | string | no | `mellat` \| `sep` \| `mock`. در نبودش از فیلد `gateway` همان رکورد پرداخت خوانده می‌شود |
### Request (varies by gateway)
**Mellat POST fields:**
@@ -350,7 +364,7 @@ Initiate a subscription / wallet top-up payment (not tied to a specific appointm
}
```
> مثل appointment: کلاینت مرورگر را به `pay_url` هدایت می‌کند؛ init درگاه در `GET /api/v1/payment/pay/{orderId}` انجام می‌شود (نه در این POST). Callback این نوع به `/api/v1/subscription-payment/callback/` می‌رود.
> مثل appointment: کلاینت مرورگر را به `pay_url` هدایت می‌کند؛ init درگاه در `GET /api/v1/payment/pay/{orderId}` انجام می‌شود (نه در این POST). Callback این نوع هم به همان `/api/v1/payment/callback` می‌رود.
### Errors
| Code | HTTP | Description |
@@ -371,11 +385,10 @@ Initiate a subscription / wallet top-up payment (not tied to a specific appointm
---
## POST/GET `/api/v1/subscription-payment/callback/{gateway}`
## ~~POST/GET `/api/v1/subscription-payment/callback/{gateway}`~~ — حذف شد
Callback for subscription payments. Same behavior as appointment callback but credits wallet instead.
**Permission:** `PUBLIC`
پرداخت اشتراک callback اختصاصی ندارد. از [`/api/v1/payment/callback`](#post-apiv1paymentcallback)
استفاده کنید؛ نوع پرداخت از خودِ رکورد `Payment` خوانده می‌شود.
---
+4 -2
View File
@@ -168,9 +168,11 @@
---
## GET /api/v1/subscription-payment/callback/{gateway}
## POST|GET /api/v1/payment/callback
callback درگاه پرداخت — پس از پرداخت موفق، `ClinicSubscription` به صورت خودکار ایجاد می‌شود (بر اساس `period_uuid` ذخیره‌شده در metadata پرداخت).
callback مشترک همهٔ درگاه‌ها و همهٔ نوع‌های پرداخت — پس از پرداخت موفق، `ClinicSubscription` به صورت خودکار ایجاد می‌شود (بر اساس `period_uuid` ذخیره‌شده در metadata پرداخت).
مسیر اختصاصی قبلی `/api/v1/subscription-payment/callback/{gateway}` حذف شده و `404` می‌دهد. قرارداد کامل: [payment.md](payment.md#post-apiv1paymentcallback).
---