Files
clinicpro/.claude/prompt/mellat-sandbox-gateway.md
T
hamed 1b171a82f4 feat(payment): add refund and reversal functionality to payment gateways
- Implemented `refund` and `reverse` methods in `PaymentGatewayInterface`.
- Added `PaymentRefundResult` class to handle refund operation results.
- Enhanced `MockGateway` and `SepGateway` to support refund and reversal operations.
- Updated `PaymentManager` to include `refundPayment` and `reversePayment` methods for handling refunds and reversals in transactions.
- Modified `ClinicSubscriptionRepository` and `SubscriptionService` to manage subscriptions during refunds.
- Added admin API endpoints for processing refunds and reversals.
- Updated security headers to allow form actions to the sandbox environment.
- Documented the new refund and reversal features in the API documentation.
2026-07-02 18:45:34 +03:30

265 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# فعال‌سازی درگاه sandbox بانک ملت (banktest.ir) — موقت
## پروژه
`clinicpro` (backend / Payment)
> **مهم:** این تغییر **موقتی** است و باید روی یک **branch جدا** انجام شود (مثلاً `feature/mellat-sandbox`). هدف: تست کامل چرخهٔ پرداخت ملت بدون درگاه واقعی، با استفاده از sandbox شرکت banktest.ir که SOAP/REST ملت را شبیه‌سازی می‌کند. بعد از اتمام تست باید به‌راحتی برگردانده شود؛ پس تغییرات را کمینه و برگشت‌پذیر نگه دار (یک فلگ روشن/خاموش).
## زمینه
`MellatGateway` فعلاً روی endpoint **عملیاتی** ملت هاردکد است:
- SOAP service: `https://bpm.shaparak.ir/pgwchannel/services/pgw`
- فرم پرداخت: `https://bpm.shaparak.ir/pgwchannel/startpay.mellat`
تست واقعی روی سرور بدون ترمینال/دامنهٔ ثبت‌شده ممکن نیست (خطای ۶۲ / ۲۱ / ۲۴). banktest.ir یک sandbox می‌دهد که **همان کانال SOAP `pgwchannel`** ملت را روی دامنهٔ خودش mirror می‌کند و credentials آزمایشی می‌دهد. چون کد فعلی ما SOAP روی `pgwchannel` است، فقط با **سوییچ base-URL + credentials** به sandbox وصل می‌شود — بدون بازنویسی منطق XML/parse.
### اطلاعات sandbox (banktest.ir)
| مورد | مقدار |
|------|-------|
| terminalId | `134759344` |
| userName | `user134759344` |
| userPassword | `17384843` |
| WSDL | `https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/services/pgw?wsdl` |
| SOAP service (بدون `?wsdl`) | `https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/services/pgw` |
| فرم پرداخت | `https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/startpay.mellat` |
> نکته: sandbox متد REST هم دارد (`.../ipg2/rest/...`) و «جدیدترین روش» است، ولی برای کمینه‌کردن تغییر و برگشت‌پذیری، **از همان مسیر SOAP `pgwchannel` استفاده کن** که کد فعلی با آن سازگار است. REST را پیاده نکن مگر اینکه SOAP sandbox کار نکند.
## مشکل / هدف
یک فلگ تنظیماتی `mellat_sandbox` اضافه کن که وقتی `'1'` است:
1. `MellatGateway` به base-URLهای sandbox banktest.ir وصل شود (SOAP service + فرم پرداخت).
2. credentials از مقادیر sandbox بالا استفاده شوند (به‌صورت fallback، تا نیازی به دست‌کاری تنظیمات prod نباشد).
3. درگاه `mellat` به‌عنوان درگاه قابل‌انتخاب و «configured» شناخته شود، **بدون** رفتن به `payment_test_mode` (که `MockGateway` را اجبار می‌کند و اصلاً به بانک وصل نمی‌شود).
4. CSP اجازهٔ `form-action` به `sandbox.banktest.ir` بدهد تا فرم auto-submit به sandbox باز شود.
خاموش‌بودن فلگ = رفتار دقیقاً مثل قبل (prod).
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `clinicpro/src/Payment/Gateway/MellatGateway.php` | سوییچ base-URL + credentials sandbox |
| `clinicpro/src/Payment/Gateway/GatewayFactory.php` | اجازهٔ استفاده از mellat در حالت sandbox (بدون Mock) |
| `clinicpro/src/Shared/EventSubscriber/SecurityHeadersSubscriber.php` | افزودن `sandbox.banktest.ir` به `form-action` |
| `clinicpro/src/Config/Controller/SiteConfigController.php` | افزودن `mellat_sandbox` به `ALLOWED_KEYS` (تا از admin settings toggle شود) |
| `clinicpro/docs/api/payment.md` | مستندسازی حالت sandbox |
## وضعیت فعلی
### `MellatGateway.php` (خطوط کلیدی)
```php
class MellatGateway implements PaymentGatewayInterface
{
private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
private const SERVICE_URL = 'https://bpm.shaparak.ir/pgwchannel/services/pgw';
public function isConfigured(): bool
{
return $this->cfg('mellat_terminal_id', $this->terminalId) !== ''
&& $this->cfg('mellat_username', $this->username) !== ''
&& $this->cfg('mellat_password', $this->password) !== '';
}
private function cfg(string $key, ?string $envFallback): string
{
return (string) ($this->configRepo->get($key) ?: $envFallback ?? '');
}
public function initiate(...) {
$response = $this->httpClient->request('POST', self::SERVICE_URL, [...]);
...
$redirectUrl = self::PAYMENT_URL . '?RefId=' . $refId;
}
public function verify(...) {
$response = $this->httpClient->request('POST', self::SERVICE_URL, [...]);
}
// buildRequestPayload / buildVerifySettlePayload از cfg('mellat_terminal_id'...) می‌خوانند
}
```
### `GatewayFactory.php` — `resolve()` در حالت test_mode همیشه Mock می‌دهد
```php
public function resolve(string $name): ?PaymentGatewayInterface
{
if ($this->isTestMode()) {
return $this->mock; // ← مانع اتصال واقعی به sandbox
}
if (!$this->isEnabled($name)) {
return null;
}
return $this->gateways[$name] ?? null;
}
public function activeGateways(): array
{
if ($this->isTestMode()) {
return [['name' => 'mellat', 'label' => 'بانک ملت (آزمایشی)']];
}
$out = [];
foreach ($this->gateways as $name => $gateway) {
if ($gateway->isConfigured() && $this->isEnabled($name)) {
$out[] = ['name' => $name, 'label' => self::LABELS[$name] ?? $name];
}
}
return $out;
}
```
### `SecurityHeadersSubscriber.php` (خط ۴۴)
```php
. "form-action https://*.shaparak.ir; base-uri 'none'"
```
## وظایف
### ۱. `MellatGateway` — سوییچ sandbox
`PAYMENT_URL` و `SERVICE_URL` را از `const` به **متد** تبدیل کن که بر اساس فلگ `mellat_sandbox` مقدار برمی‌گرداند. credentials هم وقتی sandbox روشن است از ثابت‌های sandbox به‌عنوان fallback استفاده کنند (اولویت همچنان با config key اگر ست شده باشد).
```php
class MellatGateway implements PaymentGatewayInterface
{
// prod (پیش‌فرض)
private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
private const SERVICE_URL = 'https://bpm.shaparak.ir/pgwchannel/services/pgw';
// sandbox banktest.ir (موقت — همان کانال SOAP pgwchannel)
private const SANDBOX_PAYMENT_URL = 'https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/startpay.mellat';
private const SANDBOX_SERVICE_URL = 'https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/services/pgw';
private const SANDBOX_TERMINAL_ID = '134759344';
private const SANDBOX_USERNAME = 'user134759344';
private const SANDBOX_PASSWORD = '17384843';
private function sandbox(): bool
{
return $this->configRepo->get('mellat_sandbox') === '1';
}
private function serviceUrl(): string
{
return $this->sandbox() ? self::SANDBOX_SERVICE_URL : self::SERVICE_URL;
}
private function paymentUrl(): string
{
return $this->sandbox() ? self::SANDBOX_PAYMENT_URL : self::PAYMENT_URL;
}
```
سپس:
- در `initiate()` و `verify()` به‌جای `self::SERVICE_URL` از `$this->serviceUrl()` استفاده کن.
- در `initiate()` به‌جای `self::PAYMENT_URL` از `$this->paymentUrl()` استفاده کن.
- در `cfg()` وقتی sandbox روشن است، fallback را به مقدار sandbox بده. تمیزترین راه: یک helper که envFallback را در حالت sandbox override کند:
```php
private function cfg(string $key, ?string $envFallback): string
{
if ($this->sandbox()) {
$envFallback = match ($key) {
'mellat_terminal_id' => self::SANDBOX_TERMINAL_ID,
'mellat_username' => self::SANDBOX_USERNAME,
'mellat_password' => self::SANDBOX_PASSWORD,
default => $envFallback,
};
// در sandbox، config مقدار prod را override نکند:
return $envFallback ?? '';
}
return (string) ($this->configRepo->get($key) ?: $envFallback ?? '');
}
```
> توجه: در حالت sandbox عمداً `configRepo` را نادیده می‌گیریم تا credentials واقعیِ prod (اگر در تنظیمات ست شده باشند) روی sandbox نشتی نکنند. `isConfigured()` بدون تغییر می‌ماند و چون `cfg()` مقادیر sandbox را می‌دهد، خودبه‌خود `true` می‌شود.
### ۲. `GatewayFactory` — استفاده از mellat در sandbox بدون Mock
sandbox یعنی اتصال واقعی به banktest، پس **نباید** Mock برگردد. یک متد `isMellatSandbox()` اضافه کن و در `resolve()` قبل از چک `isTestMode()` لحاظ کن. همچنین برچسب sandbox در `activeGateways()`.
```php
public function isMellatSandbox(): bool
{
return $this->configRepo->get('mellat_sandbox') === '1';
}
public function resolve(string $name): ?PaymentGatewayInterface
{
// sandbox ملت: اتصال واقعی به banktest، نه Mock.
if ($name === 'mellat' && $this->isMellatSandbox()) {
return $this->gateways['mellat'] ?? null;
}
if ($this->isTestMode()) {
return $this->mock;
}
if (!$this->isEnabled($name)) {
return null;
}
return $this->gateways[$name] ?? null;
}
public function activeGateways(): array
{
if ($this->isMellatSandbox()) {
return [['name' => 'mellat', 'label' => 'بانک ملت (Sandbox)']];
}
if ($this->isTestMode()) {
return [['name' => 'mellat', 'label' => 'بانک ملت (آزمایشی)']];
}
// ... بدون تغییر
}
```
> نکته: برای تست sandbox، `payment_test_mode` باید `0` باشد و `mellat_sandbox` برابر `1`. اگر هر دو `1` باشند، sandbox اولویت دارد (طبق ترتیب بالا).
### ۳. CSP — اجازهٔ فرم به sandbox.banktest.ir
در `SecurityHeadersSubscriber.php` خط `form-action` را گسترش بده:
```php
. "form-action https://*.shaparak.ir https://sandbox.banktest.ir; base-uri 'none'"
```
### ۴. `SiteConfigController` — کلید قابل‌تنظیم
`mellat_sandbox` را به `ALLOWED_KEYS` اضافه کن تا از `/admin/settings` قابل toggle باشد (یا مستقیم در DB ست شود). مقدار: `'1'` روشن / `'0'` یا نبود = خاموش.
### ۵. مستندسازی — `docs/api/payment.md`
یک بخش «حالت Sandbox ملت (موقت)» اضافه کن:
- فلگ `mellat_sandbox=1``payment_test_mode=0`).
- base-URLها و credentials sandbox.
- توضیح اینکه از کانال SOAP `pgwchannel` استفاده می‌شود و همان verify+settle + چک ضد-دستکاری برقرار است.
- هشدار: **موقت**، فقط روی branch جدا، بعد از تست حذف شود.
## تست (e2e روی ddev)
```bash
# روشن‌کردن sandbox، خاموش‌کردن test_mode
ddev exec mysql -e "INSERT INTO site_config (config_key,config_value,updated_at) VALUES ('mellat_sandbox','1',UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE config_value='1',updated_at=UNIX_TIMESTAMP(); UPDATE site_config SET config_value='0' WHERE config_key='payment_test_mode';" db
ddev exec php -l src/Payment/Gateway/MellatGateway.php
ddev exec php -l src/Payment/Gateway/GatewayFactory.php
ddev exec php bin/console cache:clear
# ساخت یک Payment pending با gateway=mellat و امتحان initiate (باید RefId واقعی از sandbox بگیرد یا کد خطای معنی‌دار ملت)
# سپس pay → redirect به فرم sandbox → callback → verify+settle
```
معیار موفقیت: `initiate` از banktest یک `0,<RefId>` برمی‌گرداند (نه HTTP 500، نه exception اتصال)، فرم auto-submit به `sandbox.banktest.ir/.../startpay.mellat` باز می‌شود، بعد از پرداخت آزمایشی callback به بک‌اند می‌آید، `verify` با ResCode `0/43/45` موفق می‌شود و `payment.status=success` + `reference_id` ثبت می‌شود.
## نکات مهم
- **برگشت‌پذیری:** کل رفتار sandbox پشت فلگ `mellat_sandbox` است؛ خاموش‌کردنش = رفتار prod. هیچ URL prod حذف نشود.
- **جدا از test_mode:** sandbox اتصال واقعی است؛ نباید `MockGateway` برگردد.
- **credentials در حالت sandbox از config خوانده نشوند** تا مقادیر prod نشتی نکنند (عمدی).
- چک ضد-دستکاری در `PaymentManager` (RefId==gateway_token، SaleOrderId==payment.id) و numeric orderId (`payment.id`) بدون تغییر باقی می‌مانند و با sandbox هم کار می‌کنند.
- `SERVICE_URL` sandbox **بدون** `?wsdl` باشد (POST به `?wsdl` خطای 500 می‌دهد).
- چون موقت است، credentials به‌صورت const در کلاس قرار می‌گیرند (نه `.env`)؛ این عمدی و برای سادگی حذف بعدی است.