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

13 KiB
Raw Blame History

فعال‌سازی درگاه 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 (خطوط کلیدی)

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.phpresolve() در حالت 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=1payment_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_URL sandbox بدون ?wsdl باشد (POST به ?wsdl خطای 500 می‌دهد).
  • چون موقت است، credentials به‌صورت const در کلاس قرار می‌گیرند (نه .env)؛ این عمدی و برای سادگی حذف بعدی است.