- 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.
13 KiB
فعالسازی درگاه 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/...) و «جدیدترین روش» است، ولی برای کمینهکردن تغییر و برگشتپذیری، از همان مسیر SOAPpgwchannelاستفاده کن که کد فعلی با آن سازگار است. REST را پیاده نکن مگر اینکه SOAP sandbox کار نکند.
مشکل / هدف
یک فلگ تنظیماتی mellat_sandbox اضافه کن که وقتی '1' است:
MellatGatewayبه base-URLهای sandbox banktest.ir وصل شود (SOAP service + فرم پرداخت).- credentials از مقادیر sandbox بالا استفاده شوند (بهصورت fallback، تا نیازی به دستکاری تنظیمات prod نباشد).
- درگاه
mellatبهعنوان درگاه قابلانتخاب و «configured» شناخته شود، بدون رفتن بهpayment_test_mode(کهMockGatewayرا اجبار میکند و اصلاً به بانک وصل نمیشود). - 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 (خطوط کلیدی)
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 میدهد
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 (خط ۴۴)
. "form-action https://*.shaparak.ir; base-uri 'none'"
وظایف
۱. MellatGateway — سوییچ sandbox
PAYMENT_URL و SERVICE_URL را از const به متد تبدیل کن که بر اساس فلگ mellat_sandbox مقدار برمیگرداند. credentials هم وقتی sandbox روشن است از ثابتهای sandbox بهعنوان fallback استفاده کنند (اولویت همچنان با config key اگر ست شده باشد).
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 کند:
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().
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 را گسترش بده:
. "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)
# روشنکردن 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_URLsandbox بدون?wsdlباشد (POST به?wsdlخطای 500 میدهد).- چون موقت است، credentials بهصورت const در کلاس قرار میگیرند (نه
.env)؛ این عمدی و برای سادگی حذف بعدی است.