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

229 lines
11 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.
# بردن درگاه ملت روی محیط واقعی (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 خام)
```php
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()` خوانده می‌شود:
```php
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 بی‌مورد نپردازد).
```php
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 را جایگزین کن:
```php
} 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` موفق.
```php
} 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
```php
// 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` (سرور عملیاتی):
```dotenv
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`.