feat(payment): implement PaymentManager for handling payment logic and callbacks
- Refactor PaymentController to delegate payment processing to PaymentManager. - Add findByOrderIdForUpdate method in PaymentRepository for pessimistic locking. - Create PaymentLog entity and repository for auditing payment actions. - Implement startGatewayHandoff and processCallback methods in PaymentManager. - Introduce transaction handling and logging for payment verification. - Update payment flow to ensure idempotency and prevent race conditions. - Enhance security by logging sensitive actions without exposing credentials. - Update database schema with migration for payment_logs table. - Document changes in payment flow architecture.
This commit is contained in:
+57
-2
@@ -5,6 +5,59 @@
|
||||
|
||||
---
|
||||
|
||||
## معماری (Flow & مسئولیتها)
|
||||
|
||||
**اصل:** Frontend هرگز مستقیم با بانک صحبت نمیکند و منطق پرداخت را نگه نمیدارد. Backend تنها مرجع معتبر (Source of Truth) است.
|
||||
|
||||
```
|
||||
[Frontend] کلیک پرداخت
|
||||
│ (۱) XHR authenticated: POST /api/v1/payment/appointment {appointment_uuid, gateway, frontend_address}
|
||||
▼
|
||||
[PaymentController::initiateAppointment] ← فقط Orchestration + Validation
|
||||
│ اعتبارسنجی: وجود نوبت، مالکیت کاربر، وضعیت قابلپرداخت، آدرس بازگشت مجاز، فعال بودن درگاه (GatewayFactory)
|
||||
│ ساخت Payment(pending) + ذخیره؛ برمیگرداند pay_url (هیچ ارتباطی با بانک اینجا نیست)
|
||||
▼
|
||||
[Frontend] مرورگر full-page redirect → pay_url
|
||||
│ (۲) GET /api/v1/payment/pay/{orderId} (عمومی)
|
||||
▼
|
||||
[PaymentController::pay]
|
||||
│ GatewayFactory::resolve(نام درگاه) → درگاه (تست→Mock)
|
||||
│ CircuitBreaker چک؛ gateway->initiate(amount, orderId, callbackUrl) ← ارتباط با بانک
|
||||
│ ذخیرهٔ token؛ انتقال به بانک: 302 (GET) یا فرم auto-submit POST (ملت)
|
||||
▼
|
||||
[درگاه بانک / شاپرک] پرداخت کاربر
|
||||
│ (۳) بازگشت به callbackUrl بکاند
|
||||
▼
|
||||
[PaymentController::callback] (عمومی، محدود به IP شاپرک مگر تست)
|
||||
│ gateway->verify()؛ بررسی مبلغ؛ جلوگیری از replay (reference_id یکتا)؛ ست وضعیت
|
||||
│ post-action: confirm نوبت / فعالسازی اشتراک / شارژ کیفپول + کمیسیون + پیامک
|
||||
│ (۴) RedirectResponse → frontend_address?payment_uuid=..&status=.. (همان دامنهٔ مبدأ)
|
||||
▼
|
||||
[Frontend] /payment/result → نمایش وضعیت
|
||||
```
|
||||
|
||||
**کلاسها و مسئولیتها:**
|
||||
|
||||
| کلاس | مسئولیت (SRP) |
|
||||
|------|----------------|
|
||||
| `PaymentController` | فقط Orchestration: دریافت request، اعتبارسنجی مالکیت/سفارش، فراخوانی `PaymentManager`، ساخت پاسخ/redirect HTTP (`autoSubmitForm`, `redirectToFrontend`, IP-check, Open-Redirect guard). بدون منطق درگاه/verify. |
|
||||
| `PaymentManager` (`src/Payment/Service/`) | **منطق پرداخت**: `startGatewayHandoff()` (init درگاه + CircuitBreaker + ذخیرهٔ token) و `processCallback()` (verify داخل **transaction + قفل بدبینانه**، بررسی مبلغ، ضد-replay، idempotent، post-action، لاگ). |
|
||||
| `GatewayFactory` (`src/Payment/Gateway/`) | **Factory + Strategy**: انتخاب درگاه بر اساس نام + حالت تست + فعال بودن؛ فهرست درگاههای فعال. |
|
||||
| `PaymentGatewayInterface` | قرارداد درگاه: `initiate()`, `verify()`, `isConfigured()`, `getName()`. |
|
||||
| `MellatGateway` / `SepGateway` / `MockGateway` | پیادهسازی هر درگاه (SOAP/REST/mock). ملت با POST به بانک، سپ با GET. |
|
||||
| `PaymentInitResult` / `PaymentVerifyResult` | DTO نتیجهٔ init/verify (شامل `redirectMethod`/`redirectParams`). |
|
||||
| `CircuitBreakerService` | جلوگیری از فشار روی درگاهِ خراب. |
|
||||
| `Payment` (Entity) | وضعیت پرداخت، `orderId` یکتا، `referenceId` یکتا (backstop برای replay)، `frontendAddress` (دامنهٔ مبدأ). |
|
||||
| `PaymentLog` (Entity) + `PaymentLogRepository` | **audit trail**: هر گام (`initiate`/`verify`) با نتیجه، authority، IP، payload کالبک (بدون اعتبارنامه). |
|
||||
|
||||
**امنیت verify:** `processCallback` داخل `EntityManager::wrapInTransaction` با `findByOrderIdForUpdate` (SELECT … FOR UPDATE) اجرا میشود؛ گاردِ «فقط `pending`» آن را **idempotent** میکند (verify تکراری/race بیاثر).
|
||||
|
||||
**یکدستیِ typeها:** هر سه نوع (`appointment`/`subscription`/`sms_wallet`) از همان `GET /payment/pay/{orderId}` عبور میکنند؛ `PaymentManager::callbackUrl()` پیشوند callback را بر اساس `type` انتخاب میکند. POST این endpointها فقط `Payment` pending میسازد و `pay_url` برمیگرداند (نه `redirect_url`).
|
||||
|
||||
**افزودن درگاه جدید (Open/Closed):** یک کلاس جدید implements `PaymentGatewayInterface` بساز، در `GatewayFactory::$gateways` + `LABELS` ثبت کن. `PaymentController`/`PaymentManager` تغییر نمیکنند.
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/payment/config`
|
||||
|
||||
دریافت تنظیمات عمومی پرداخت — برای نمایش وضعیت درگاه آزمایشی در frontend.
|
||||
@@ -231,12 +284,14 @@ Initiate a subscription / wallet top-up payment (not tied to a specific appointm
|
||||
"success": true,
|
||||
"data": {
|
||||
"payment_uuid": "pay-uuid-...",
|
||||
"redirect_url": "https://bpm.shaparak.ir/pgwchannel/...",
|
||||
"order_id": "CLINICPRO-SUB-1717000000-XYZ"
|
||||
"pay_url": "{APP_BASE_URL}/api/v1/payment/pay/ORD-XXXX",
|
||||
"order_id": "ORD-XXXX"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> مثل appointment: کلاینت مرورگر را به `pay_url` هدایت میکند؛ init درگاه در `GET /api/v1/payment/pay/{orderId}` انجام میشود (نه در این POST). Callback این نوع به `/api/v1/subscription-payment/callback/` میرود.
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
|
||||
Reference in New Issue
Block a user