feat(payment): update Mellat gateway implementation and enhance security checks

This commit is contained in:
hamed
2026-07-02 17:51:50 +03:30
parent e487f7437a
commit 6bb47d343d
3 changed files with 51 additions and 18 deletions
+27 -16
View File
@@ -9,6 +9,8 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class MellatGateway implements PaymentGatewayInterface
{
private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
// endpoint سرویس SOAP (بدون ?wsdl؛ ?wsdl فقط توصیفِ سرویس است و POST به آن 500 می‌دهد).
private const SERVICE_URL = 'https://bpm.shaparak.ir/pgwchannel/services/pgw';
public function __construct(
private readonly HttpClientInterface $httpClient,
@@ -41,10 +43,10 @@ class MellatGateway implements PaymentGatewayInterface
try {
$response = $this->httpClient->request(
'POST',
'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl',
self::SERVICE_URL,
[
'body' => $this->buildRequestPayload($amountRials, $orderId, $callbackUrl),
'headers' => ['Content-Type' => 'text/xml; charset=utf-8'],
'headers' => ['Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => '""'],
'timeout' => 10,
]
);
@@ -75,8 +77,9 @@ class MellatGateway implements PaymentGatewayInterface
public function verify(array $callbackData): PaymentVerifyResult
{
$refId = $callbackData['RefId'] ?? '';
$resCode = $callbackData['ResCode'] ?? '';
$resCode = $callbackData['ResCode'] ?? '';
$saleOrderId = $callbackData['SaleOrderId'] ?? '';
$saleReferenceId = $callbackData['SaleReferenceId'] ?? '';
if ($resCode === '17') {
return new PaymentVerifyResult(false, errorMessage: 'پرداخت توسط کاربر لغو شد', canceled: true);
@@ -86,25 +89,32 @@ class MellatGateway implements PaymentGatewayInterface
return new PaymentVerifyResult(false, errorMessage: "Payment failed: $resCode");
}
// برای تأیید و واریز، ملت به saleOrderId (همان orderId مرحلهٔ Pay) و
// saleReferenceId (که در callback برمی‌گردد) نیاز دارد — نه RefId.
if ($saleOrderId === '' || $saleReferenceId === '') {
return new PaymentVerifyResult(false, errorMessage: 'اطلاعات بازگشتی درگاه ناقص است');
}
try {
$response = $this->httpClient->request(
'POST',
'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl',
self::SERVICE_URL,
[
'body' => $this->buildVerifyPayload($refId),
'headers' => ['Content-Type' => 'text/xml; charset=utf-8'],
'body' => $this->buildVerifySettlePayload($saleOrderId, $saleReferenceId),
'headers' => ['Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => '""'],
'timeout' => 10,
]
);
$verifyCode = $this->parseResCode($response->getContent());
if ($verifyCode !== '0') {
// 0 = موفق، 43 = پیشتر verify شده، 45 = پیشتر settle شده (هر دو idempotent = موفق).
if (!in_array($verifyCode, ['0', '43', '45'], true)) {
return new PaymentVerifyResult(false, errorMessage: "Verify failed: $verifyCode");
}
return new PaymentVerifyResult(true, referenceId: $refId);
return new PaymentVerifyResult(true, referenceId: $saleReferenceId);
} catch (\Throwable $e) {
$this->logger->error(sprintf('Payment verify failed (mellat): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'refId' => $refId]);
$this->logger->error(sprintf('Payment verify failed (mellat): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'saleReferenceId' => $saleReferenceId]);
return new PaymentVerifyResult(false, errorMessage: $e->getMessage());
}
}
@@ -135,7 +145,8 @@ class MellatGateway implements PaymentGatewayInterface
XML;
}
private function buildVerifyPayload(string $refId): string
/** تأیید و واریز یکجا (bpVerifySettleRequest). orderId می‌تواند برابر saleOrderId باشد. */
private function buildVerifySettlePayload(string $saleOrderId, string $saleReferenceId): string
{
$terminalId = $this->cfg('mellat_terminal_id', $this->terminalId);
$username = $this->cfg('mellat_username', $this->username);
@@ -144,14 +155,14 @@ XML;
return <<<XML
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:int="http://interfaces.core.sw.bsl.com/westernmellat">
<soapenv:Body>
<int:bpVerifyRequest>
<int:bpVerifySettleRequest>
<terminalId>{$terminalId}</terminalId>
<userName>{$username}</userName>
<userPassword>{$password}</userPassword>
<orderId>{$refId}</orderId>
<saleOrderId>{$refId}</saleOrderId>
<saleReferenceId>{$refId}</saleReferenceId>
</int:bpVerifyRequest>
<orderId>{$saleOrderId}</orderId>
<saleOrderId>{$saleOrderId}</saleOrderId>
<saleReferenceId>{$saleReferenceId}</saleReferenceId>
</int:bpVerifySettleRequest>
</soapenv:Body>
</soapenv:Envelope>
XML;