11 KiB
بردن درگاه ملت روی محیط واقعی (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.