feat(appointment): enhance appointment detail page with time formatting and additional info fix(payment): update payment query to fetch from the correct endpoint and adjust response structure docs(api): add search parameter to payments API documentation and detail response structure test(payment): add unit test for MellatGateway to verify null credentials handling
183 lines
6.6 KiB
PHP
183 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Payment\Gateway;
|
|
|
|
use App\Config\Repository\SiteConfigRepository;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
class MellatGateway implements PaymentGatewayInterface
|
|
{
|
|
private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
|
|
|
|
public function __construct(
|
|
private readonly HttpClientInterface $httpClient,
|
|
private readonly SiteConfigRepository $configRepo,
|
|
private readonly LoggerInterface $logger,
|
|
private readonly ?string $terminalId = '',
|
|
private readonly ?string $username = '',
|
|
private readonly ?string $password = '',
|
|
) {}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'mellat';
|
|
}
|
|
|
|
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(int $amountRials, string $orderId, string $callbackUrl): PaymentInitResult
|
|
{
|
|
try {
|
|
$response = $this->httpClient->request(
|
|
'POST',
|
|
'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl',
|
|
[
|
|
'body' => $this->buildRequestPayload($amountRials, $orderId, $callbackUrl),
|
|
'headers' => ['Content-Type' => 'text/xml; charset=utf-8'],
|
|
'timeout' => 10,
|
|
]
|
|
);
|
|
|
|
$resCode = $this->parseResCode($response->getContent());
|
|
|
|
if ($resCode !== '0') {
|
|
return new PaymentInitResult(false, errorMessage: "Mellat error: $resCode");
|
|
}
|
|
|
|
$refId = $this->parseRefId($response->getContent());
|
|
$redirectUrl = self::PAYMENT_URL . '?RefId=' . $refId;
|
|
|
|
// درگاه ملت باید با POST فرم (فیلد RefId) باز شود؛ redirectUrl (شامل RefId)
|
|
// برای سازگاری با مصرفکنندههای قدیمی نگه داشته میشود.
|
|
return new PaymentInitResult(
|
|
true,
|
|
redirectUrl: $redirectUrl,
|
|
token: $refId,
|
|
redirectMethod: 'POST',
|
|
redirectParams: ['RefId' => $refId],
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error(sprintf('Payment initiate failed (mellat): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'orderId' => $orderId, 'amount' => $amountRials]);
|
|
return new PaymentInitResult(false, errorMessage: $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function verify(array $callbackData): PaymentVerifyResult
|
|
{
|
|
$refId = $callbackData['RefId'] ?? '';
|
|
$resCode = $callbackData['ResCode'] ?? '';
|
|
|
|
if ($resCode === '17') {
|
|
return new PaymentVerifyResult(false, errorMessage: 'پرداخت توسط کاربر لغو شد', canceled: true);
|
|
}
|
|
|
|
if ($resCode !== '0') {
|
|
return new PaymentVerifyResult(false, errorMessage: "Payment failed: $resCode");
|
|
}
|
|
|
|
try {
|
|
$response = $this->httpClient->request(
|
|
'POST',
|
|
'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl',
|
|
[
|
|
'body' => $this->buildVerifyPayload($refId),
|
|
'headers' => ['Content-Type' => 'text/xml; charset=utf-8'],
|
|
'timeout' => 10,
|
|
]
|
|
);
|
|
|
|
$verifyCode = $this->parseResCode($response->getContent());
|
|
if ($verifyCode !== '0') {
|
|
return new PaymentVerifyResult(false, errorMessage: "Verify failed: $verifyCode");
|
|
}
|
|
|
|
return new PaymentVerifyResult(true, referenceId: $refId);
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error(sprintf('Payment verify failed (mellat): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'refId' => $refId]);
|
|
return new PaymentVerifyResult(false, errorMessage: $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function buildRequestPayload(int $amount, string $orderId, string $callbackUrl): string
|
|
{
|
|
$terminalId = $this->cfg('mellat_terminal_id', $this->terminalId);
|
|
$username = $this->cfg('mellat_username', $this->username);
|
|
$password = $this->cfg('mellat_password', $this->password);
|
|
|
|
return <<<XML
|
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:int="http://interfaces.core.sw.bsl.com/westernmellat">
|
|
<soapenv:Body>
|
|
<int:bpPayRequest>
|
|
<terminalId>{$terminalId}</terminalId>
|
|
<userName>{$username}</userName>
|
|
<userPassword>{$password}</userPassword>
|
|
<orderId>{$orderId}</orderId>
|
|
<amount>{$amount}</amount>
|
|
<localDate>{$this->date()}</localDate>
|
|
<localTime>{$this->time()}</localTime>
|
|
<additionalData></additionalData>
|
|
<callBackUrl>{$callbackUrl}</callBackUrl>
|
|
<payerId>0</payerId>
|
|
</int:bpPayRequest>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>
|
|
XML;
|
|
}
|
|
|
|
private function buildVerifyPayload(string $refId): string
|
|
{
|
|
$terminalId = $this->cfg('mellat_terminal_id', $this->terminalId);
|
|
$username = $this->cfg('mellat_username', $this->username);
|
|
$password = $this->cfg('mellat_password', $this->password);
|
|
|
|
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>
|
|
<terminalId>{$terminalId}</terminalId>
|
|
<userName>{$username}</userName>
|
|
<userPassword>{$password}</userPassword>
|
|
<orderId>{$refId}</orderId>
|
|
<saleOrderId>{$refId}</saleOrderId>
|
|
<saleReferenceId>{$refId}</saleReferenceId>
|
|
</int:bpVerifyRequest>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>
|
|
XML;
|
|
}
|
|
|
|
private function parseResCode(string $xml): string
|
|
{
|
|
preg_match('/<return>(.*?)<\/return>/', $xml, $m);
|
|
$parts = explode(',', $m[1] ?? '');
|
|
return trim($parts[0] ?? '-1');
|
|
}
|
|
|
|
private function parseRefId(string $xml): string
|
|
{
|
|
preg_match('/<return>(.*?)<\/return>/', $xml, $m);
|
|
$parts = explode(',', $m[1] ?? '');
|
|
return trim($parts[1] ?? '');
|
|
}
|
|
|
|
private function date(): string
|
|
{
|
|
return date('Ymd');
|
|
}
|
|
private function time(): string
|
|
{
|
|
return date('His');
|
|
}
|
|
}
|