Files
clinicpro/.claude/prompt/mellat-production-soap.md

11 KiB
Raw Permalink Blame History

بردن درگاه ملت روی محیط واقعی (production) با SOAP native

پروژه

clinicpro (backend / Payment)

زمینه

درگاه ملت الان دو مسیر دارد:

  • sandbox (mellat_sandbox=1): REST روی banktest.ir (کار می‌کند، برای تست).
  • prod (پیش‌فرض): SOAP روی bpm.shaparak.ir که فعلاً با POST خام XML توسط Symfony HttpClient به pgwchannel/services/pgw زده می‌شود.

طبق آموزش رسمی بانک (mellat-payment-gateway-symfony.md, Mellat PGW Tech Doc v1.38)، روش توصیه‌شده برای prod استفاده از SoapClient بومی PHP روی WSDL است (نه POST خام). اکستنشن soap روی سرور فعال است (تأیید شد). مسیر prod باید به SoapClient تبدیل شود تا با درگاه واقعی پایدار کار کند؛ مسیر sandbox (REST) دست‌نخورده می‌ماند.

هدف: مسیر prod درگاه ملت با SoapClient واقعی کار کند و آمادهٔ رفتن روی دامنهٔ عملیاتی باشد. sandbox برای تست باقی بماند.

فایل‌های مرتبط

فایل نقش
clinicpro/src/Payment/Gateway/MellatGateway.php جایگزینی مسیر prod (POST خام XML) با SoapClient؛ حفظ مسیر sandbox REST
clinicpro/config/services.yaml (در صورت نیاز) binding mellat_wsdl_url
clinicpro/.env مقادیر prod (WSDL/redirect) + credentials واقعی
clinicpro/docs/api/payment.md مستندسازی مسیر prod SOAP

وضعیت فعلی

MellatGateway.php — ثابت‌ها و مسیر prod فعلی (POST خام)

private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
private const SERVICE_URL = 'https://bpm.shaparak.ir/pgwchannel/services/pgw'; // prod SOAP (POST خام)

// sandbox (REST) — بدون تغییر باقی می‌ماند
private const SANDBOX_REST_BASE   = 'https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/ipg2/rest';
private const SANDBOX_PAYMENT_URL = 'https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/startpay.mellat';
private const SANDBOX_TERMINAL_ID = '134759344';
// ...

public function initiate(...) {
    if ($this->sandbox()) { /* REST bpPayRequest */ }
    else {
        $response = $this->httpClient->request('POST', self::SERVICE_URL, [
            'body' => $this->buildRequestPayload(...),           // ← XML خام
            'headers' => ['Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => '""'],
        ]);
        $resCode = $this->parseResCode($response->getContent()); // ← regex روی XML
        $refId   = $this->parseRefId($response->getContent());
    }
    // ...
}

public function verify(array $callbackData) {
    if ($this->sandbox()) { /* REST verify + settle جدا */ }
    else {
        $response = $this->httpClient->request('POST', self::SERVICE_URL, [
            'body' => $this->buildVerifySettlePayload($saleOrderId, $saleReferenceId), // ← bpVerifySettleRequest خام
            // ...
        ]);
        $verifyCode = $this->parseResCode($response->getContent());
    }
}

// refund()/reverse() prod هم همین الگوی POST خام را دارند (buildRefundPayload/buildReversalPayload)
// helperهای فقط-prod: buildRequestPayload, buildVerifySettlePayload, buildRefundPayload,
//                      buildReversalPayload, parseResCode, parseRefId

اعتبارنامه از cfg() خوانده می‌شود:

private function cfg(string $key, ?string $envFallback): string
{
    if ($this->sandbox()) { /* const های sandbox */ }
    return (string) ($this->configRepo->get($key) ?: $envFallback ?? '');
}
// prod: mellat_terminal_id / mellat_username / mellat_password از site_config یا env (MELLAT_*)

وظایف

۱. افزودن WSDL prod + سازندهٔ تنبل SoapClient

یک ثابت WSDL و یک متد که SoapClient را lazy می‌سازد (فقط prod، فقط وقتی لازم شد؛ تا sandbox/mock را نشکند و هزینهٔ ساخت WSDL بی‌مورد نپردازد).

private const PROD_WSDL = 'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl';

private ?\SoapClient $soap = null;

/** WSDL قابل override با کلید تنظیم mellat_wsdl_url (برای WSDL تستِ pgw.dev اگر لازم شد). */
private function wsdlUrl(): string
{
    return (string) ($this->configRepo->get('mellat_wsdl_url') ?: self::PROD_WSDL);
}

private function soap(): \SoapClient
{
    if ($this->soap === null) {
        $this->soap = new \SoapClient($this->wsdlUrl(), [
            'trace'              => true,
            'exceptions'         => true,
            'encoding'           => 'UTF-8',
            'connection_timeout' => 10,
        ]);
    }
    return $this->soap;
}

/** پارامترهای مشترک احراز هویت prod. */
private function soapAuth(): array
{
    return [
        'terminalId'   => (int) $this->cfg('mellat_terminal_id', $this->terminalId),
        'userName'     => $this->cfg('mellat_username', $this->username),
        'userPassword' => $this->cfg('mellat_password', $this->password),
    ];
}

۲. initiate() — مسیر prod با SoapClient

مسیر sandbox بدون تغییر. مسیر else را جایگزین کن:

} else {
    $r = $this->soap()->bpPayRequest($this->soapAuth() + [
        'orderId'        => (int) $orderId,
        'amount'         => $amountRials,
        'localDate'      => $this->date(),
        'localTime'      => $this->time(),
        'additionalData' => '',
        'callBackUrl'    => $callbackUrl,
        'payerId'        => 0,
    ]);
    // پاسخ "resCode,refId"
    $parts   = array_map('trim', explode(',', (string) ($r->return ?? ''), 2));
    $resCode = $parts[0] ?? '-1';
    $refId   = $parts[1] ?? '';
}

SoapFault باید مثل بقیه در catch (\Throwable $e) موجود گرفته شود (هست).

۳. verify() — prod با verify + settle جدا (طبق آموزش)

طبق آموزش، prod هم مثل sandbox verify سپس settle جدا انجام شود (به‌جای bpVerifySettleRequest ترکیبی). کدها: verify 0/43 موفق، settle 0/45 موفق.

} else {
    $auth = $this->soapAuth() + [
        'orderId'         => (int) $saleOrderId,
        'saleOrderId'     => (int) $saleOrderId,
        'saleReferenceId' => (int) $saleReferenceId,
    ];
    $vc = (string) ($this->soap()->bpVerifyRequest($auth)->return ?? '-1');
    if (!in_array($vc, ['0', '43'], true)) {
        return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($vc));
    }
    $sc = (string) ($this->soap()->bpSettleRequest($auth)->return ?? '-1');
    if (!in_array($sc, ['0', '45'], true)) {
        return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($sc));
    }
    return new PaymentVerifyResult(true, referenceId: $saleReferenceId);
}

نکته: بعد از این تغییر، پیام خطای prod هم از mellatMessage() استفاده کند (نه رشتهٔ خام «Verify failed»). چک ResCode === '17' (انصراف) و ناقص‌بودن saleOrderId/saleReferenceId که قبل از try هست، حفظ شود.

۴. refund() / reverse() — prod با SoapClient

// refund prod:
$r = $this->soap()->bpRefundRequest($this->soapAuth() + [
    'orderId'         => $this->uniqueOrderId(),
    'saleOrderId'     => (int) $saleOrderId,
    'saleReferenceId' => (int) $saleReferenceId,
    'refundAmount'    => $refundAmountRials,
]);
$parts = array_map('trim', explode(',', (string) ($r->return ?? ''), 2));
$code  = $parts[0] ?? '-1';
// code !== '0' → PaymentRefundResult(false, mellatMessage($code)); else refundRefId = $parts[1]

// reverse prod:
$code = (string) ($this->soap()->bpReversalRequest($this->soapAuth() + [
    'orderId'         => $this->uniqueOrderId(),
    'saleOrderId'     => (int) $saleOrderId,
    'saleReferenceId' => (int) $saleReferenceId,
])->return ?? '-1');
// in_array($code, ['0','48']) → success

۵. حذف helperهای مردهٔ prod

بعد از سوییچ به SoapClient، این‌ها دیگر استفاده نمی‌شوند و باید حذف شوند (اگر جای دیگری استفاده نشده‌اند — بررسی کن): buildRequestPayload, buildVerifySettlePayload, buildRefundPayload, buildReversalPayload, parseResCode, parseRefId, و ثابت SERVICE_URL (POST خام). restParts/restCall/restHeaders و ثابت‌های sandbox باقی می‌مانند (sandbox REST همچنان از آن‌ها استفاده می‌کند).

۶. تنظیمات محیط prod

  • .env (سرور عملیاتی):
    APP_BASE_URL=https://<دامنهٔ-ثبت‌شده-نزد-ملت>      # مثلا https://clinic-pro.ir
    # اعتبارنامهٔ واقعی (یا از /admin/settings ست شود):
    MELLAT_TERMINAL_ID=<terminal واقعی>
    MELLAT_USERNAME=<username واقعی>
    MELLAT_PASSWORD=<password واقعی>
    # WSDL prod پیش‌فرض است؛ فقط اگر WSDL تستِ pgw.dev خواستی:
    # کلید site_config: mellat_wsdl_url = https://pgw.dev.bpmellat.ir/pgwchannel/services/pgw?wsdl
    
  • در /admin/settings: mellat_sandbox=0 (خاموش) و payment_test_mode=0؛ mellat_enabled=1 و در صورت نبود env، mellat_terminal_id/username/password را همان‌جا ست کن.

نکات مهم (چک‌لیست عملیاتی prod)

  • IP سرور باید توسط «شرکت به‌پرداخت ملت» whitelist شود، وگرنه کد 421 (IP نامعتبر).
  • دامنهٔ callBackUrl = APP_BASE_URL باید دقیقاً دامنهٔ ثبت‌شده نزد ملت باشد (نه IP)، وگرنه کد 62. callbackUrl() در PaymentManager از APP_BASE_URL می‌سازد.
  • ext-soap روی سرور prod فعال باشد (php -m | grep soap) — روی ddev هست.
  • پورت‌های 443/80 خروجی سرور به شبکهٔ شاپرک باز باشد.
  • orderId مرحلهٔ Pay = payment.id عددی (هست)؛ refund/reverse از uniqueOrderId() یکتا استفاده می‌کنند (هست).
  • چک ضد-دستکاری callback (RefId==gateway_token, SaleOrderId==payment.id) در PaymentManager::processCallback برای prod هم فعال است — دست نخورد.
  • Auto-reversal: اگر verify ظرف ۲۰ دقیقه بعد از پرداخت موفق ارسال نشود، بانک خودکار برگشت می‌زند؛ چون verify در callback بلافاصله انجام می‌شود مشکلی نیست.
  • SoapClient روی خطای شبکه/WSDL SoapFault می‌اندازد که در catch (\Throwable) موجود گرفته و به پیام کاربر تبدیل می‌شود.
  • بعد از تغییر: ddev exec php -l، cache:clear، و یک تراکنش واقعی با مبلغ کم تست شود (prod را نمی‌توان با mock تست کرد؛ فقط syntax + بارگذاری WSDL قابل بررسی خودکار است).
  • docs/api/payment.md را به‌روزرسانی کن (prod = SoapClient؛ verify+settle جدا).

آنچه نباید تغییر کند

  • مسیر sandbox REST (initiate/verify/refund/reverse وقتی sandbox() true است).
  • منطق PaymentManager (transaction/lock/tamper/reverse-post-action).
  • SepGateway/MockGateway.