274 lines
10 KiB
PHP
274 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payment\Gateways;
|
|
|
|
use App\Services\ConfigService;
|
|
use App\Services\Payment\Contracts\WalletPaymentGatewayInterface;
|
|
use App\Services\Payment\Support\MontifyHmacSigner;
|
|
use App\Services\Payment\WalletDepositCreditor;
|
|
use App\Services\Payment\WalletPaymentManager;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* درگاه پارتنر Montify (HMAC) — فقط credentials روی سرور.
|
|
* @see docs/EXTERNAL_PAYMENT_PARTNER.md
|
|
*/
|
|
class MontifyPartnerGateway implements WalletPaymentGatewayInterface
|
|
{
|
|
public function __construct(
|
|
private ConfigService $config,
|
|
private WalletDepositCreditor $creditor,
|
|
) {
|
|
}
|
|
|
|
public function key(): string
|
|
{
|
|
return WalletPaymentManager::PROVIDER_MONTIFY;
|
|
}
|
|
|
|
public function isAvailable(): bool
|
|
{
|
|
return $this->isConfigured();
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->baseUrl() !== ''
|
|
&& $this->clientId() !== ''
|
|
&& $this->clientSecret() !== ''
|
|
&& $this->gatewayName() !== '';
|
|
}
|
|
|
|
public function returnUrl(): string
|
|
{
|
|
$fromSettings = $this->config->getString('external_payment_return_url', '');
|
|
if ($fromSettings !== '') {
|
|
return $fromSettings;
|
|
}
|
|
|
|
// پیشفرض: صفحه فرانت برای نمایش نتیجه + verify سمت سرور
|
|
$site = $this->config->resolvePublicSiteUrl(request());
|
|
if ($site === '') {
|
|
$appUrl = rtrim((string) config('app.url'), '/');
|
|
// اگر APP_URL با /server تمام شود، دامنه فرانت همان host بدون /server است
|
|
$site = preg_replace('#/server$#', '', $appUrl) ?: $appUrl;
|
|
}
|
|
|
|
return rtrim($site, '/') . '/wallet/payment-result';
|
|
}
|
|
|
|
public function webhookUrl(): string
|
|
{
|
|
$fromSettings = $this->config->getString('external_payment_webhook_url', '');
|
|
if ($fromSettings !== '') {
|
|
return $fromSettings;
|
|
}
|
|
|
|
return url('/customer/wallet/payment/webhook/' . $this->key());
|
|
}
|
|
|
|
public function initiate(array $payload): array
|
|
{
|
|
if (!$this->isConfigured()) {
|
|
throw new RuntimeException('تنظیمات درگاه Montify ناقص است');
|
|
}
|
|
|
|
$path = '/external-payment/initiate';
|
|
$body = [
|
|
'amount' => (int) $payload['amount'],
|
|
'reference_id' => (string) $payload['reference_id'],
|
|
'gateway' => (string) ($payload['gateway'] ?? $this->gatewayName()),
|
|
'return_url' => (string) ($payload['return_url'] ?? $this->returnUrl()),
|
|
'webhook_url' => (string) ($payload['webhook_url'] ?? $this->webhookUrl()),
|
|
];
|
|
|
|
if (!empty($payload['description'])) {
|
|
$body['description'] = (string) $payload['description'];
|
|
}
|
|
if (!empty($payload['metadata']) && is_array($payload['metadata'])) {
|
|
$body['metadata'] = $payload['metadata'];
|
|
}
|
|
|
|
$signer = $this->signer();
|
|
$rawBody = $signer->stableStringify($body);
|
|
$headers = $signer->signRequest('POST', $path, $body);
|
|
|
|
try {
|
|
// مهم: post($url, []) بدنهٔ withBody را خالی میکند — حتماً send بدون data
|
|
$response = Http::withHeaders($headers)
|
|
->withBody($rawBody, 'application/json')
|
|
->acceptJson()
|
|
->timeout(30)
|
|
->send('POST', $this->baseUrl() . $path);
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
throw new RuntimeException('اتصال به سرور درگاه برقرار نشد');
|
|
}
|
|
|
|
$data = $response->json();
|
|
if (!$response->successful() || !is_array($data) || empty($data['payment_url'])) {
|
|
$errorCode = is_array($data) ? (string) ($data['error'] ?? '') : '';
|
|
$remoteMessage = is_array($data)
|
|
? (string) ($data['message'] ?? $data['error'] ?? 'initiate failed')
|
|
: 'initiate failed';
|
|
|
|
\Log::warning('montify.initiate_failed', [
|
|
'status' => $response->status(),
|
|
'error' => $errorCode,
|
|
'message' => $remoteMessage,
|
|
'return_url' => $body['return_url'] ?? null,
|
|
'webhook_url' => $body['webhook_url'] ?? null,
|
|
'gateway' => $body['gateway'] ?? null,
|
|
'reference_id' => $body['reference_id'] ?? null,
|
|
'amount' => $body['amount'] ?? null,
|
|
'body' => is_array($data) ? $data : $response->body(),
|
|
]);
|
|
|
|
throw new RuntimeException($this->mapInitiateError($errorCode, $remoteMessage));
|
|
}
|
|
|
|
return [
|
|
'payment_url' => (string) $data['payment_url'],
|
|
'session_id' => $data['session_id'] ?? null,
|
|
'reference_id' => $data['reference_id'] ?? $payload['reference_id'],
|
|
'expires_at' => $data['expires_at'] ?? null,
|
|
];
|
|
}
|
|
|
|
private function mapInitiateError(string $errorCode, string $fallback): string
|
|
{
|
|
return match ($errorCode) {
|
|
'invalid_signature' => 'امضای درخواست درگاه نامعتبر است (secret یا ساعت سرور را بررسی کنید)',
|
|
'expired_timestamp' => 'ساعت سرور با درگاه همخوان نیست',
|
|
'replay_detected' => 'درخواست تکراری شناسایی شد؛ دوباره تلاش کنید',
|
|
'invalid_return_url' => 'آدرس برگشت (return_url) در whitelist پارتنر Montify ثبت نشده است',
|
|
'invalid_gateway' => 'نام درگاه داخل Montify نامعتبر است',
|
|
'amount_too_low' => 'مبلغ کمتر از حد مجاز درگاه است',
|
|
'duplicate_reference' => 'شناسه پرداخت تکراری است؛ دوباره تلاش کنید',
|
|
'gateway_not_configured' => 'درگاه انتخابشده در Montify پیکربندی نشده است',
|
|
'missing_auth_headers' => 'هدرهای احراز هویت به درگاه ارسال نشد',
|
|
default => $fallback !== '' && $fallback !== 'initiate failed'
|
|
? $fallback
|
|
: 'مشکل در ارتباط با درگاه',
|
|
};
|
|
}
|
|
|
|
public function handleReturn(Request $request): array
|
|
{
|
|
$query = $request->query();
|
|
$signer = $this->signer();
|
|
|
|
if (!$signer->verifyReturnSignature($query)) {
|
|
return ['ok' => false, 'withdraw' => null, 'http_status' => 401, 'message' => 'امضای نامعتبر'];
|
|
}
|
|
|
|
if (!$signer->isTimestampFresh($query['timestamp'] ?? null)) {
|
|
return ['ok' => false, 'withdraw' => null, 'http_status' => 401, 'message' => 'زمان درخواست منقضی شده'];
|
|
}
|
|
|
|
$referenceId = (string) ($query['reference_id'] ?? '');
|
|
$status = (string) ($query['status'] ?? '');
|
|
$amount = (int) ($query['amount'] ?? 0);
|
|
$trackNumber = (string) ($query['track_number'] ?? '');
|
|
$sessionId = (string) ($query['session_id'] ?? '');
|
|
|
|
$withdraw = $this->creditor->findByReference($referenceId);
|
|
if (!$withdraw) {
|
|
return ['ok' => false, 'withdraw' => null, 'http_status' => 404, 'message' => 'سفارش یافت نشد'];
|
|
}
|
|
|
|
if ($amount > 0 && (int) $withdraw->cash !== $amount) {
|
|
return ['ok' => false, 'withdraw' => $withdraw, 'http_status' => 422, 'message' => 'مبلغ پرداختی دستکاری شده !'];
|
|
}
|
|
|
|
if ($status === 'success') {
|
|
$this->creditor->markPaidAndCredit(
|
|
$withdraw,
|
|
$trackNumber !== '' ? $trackNumber : $sessionId,
|
|
'واریز درگاه Montify'
|
|
);
|
|
} elseif ((int) $withdraw->sts === 1) {
|
|
$withdraw->sts = 3;
|
|
$withdraw->save();
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'withdraw' => $withdraw->fresh(['customer']),
|
|
];
|
|
}
|
|
|
|
public function handleWebhook(Request $request): array
|
|
{
|
|
$body = $request->all();
|
|
$signer = $this->signer();
|
|
|
|
if (!$signer->verifyWebhookSignature($body, $request->header('X-Signature'))) {
|
|
return ['ok' => false, 'http_status' => 401, 'message' => 'invalid_signature'];
|
|
}
|
|
|
|
$clientId = (string) $request->header('X-Client-Id', '');
|
|
if ($clientId !== '' && $clientId !== $this->clientId()) {
|
|
return ['ok' => false, 'http_status' => 401, 'message' => 'invalid_client'];
|
|
}
|
|
|
|
if (!$signer->isTimestampFresh($body['timestamp'] ?? $request->header('X-Timestamp'))) {
|
|
return ['ok' => false, 'http_status' => 401, 'message' => 'expired_timestamp'];
|
|
}
|
|
|
|
$status = (string) ($body['status'] ?? '');
|
|
if ($status !== 'success') {
|
|
return ['ok' => true, 'ignored' => true];
|
|
}
|
|
|
|
$referenceId = (string) ($body['reference_id'] ?? '');
|
|
$amount = (int) ($body['amount'] ?? 0);
|
|
$trackNumber = (string) ($body['track_number'] ?? '');
|
|
$sessionId = (string) ($body['session_id'] ?? '');
|
|
|
|
$withdraw = $this->creditor->findByReference($referenceId);
|
|
if (!$withdraw) {
|
|
return ['ok' => false, 'http_status' => 404, 'message' => 'not_found'];
|
|
}
|
|
|
|
if ($amount > 0 && (int) $withdraw->cash !== $amount) {
|
|
return ['ok' => false, 'http_status' => 422, 'message' => 'amount_mismatch'];
|
|
}
|
|
|
|
$this->creditor->markPaidAndCredit(
|
|
$withdraw,
|
|
$trackNumber !== '' ? $trackNumber : $sessionId,
|
|
'واریز درگاه Montify'
|
|
);
|
|
|
|
return ['ok' => true];
|
|
}
|
|
|
|
private function baseUrl(): string
|
|
{
|
|
return rtrim($this->config->getString('external_payment_base_url', ''), '/');
|
|
}
|
|
|
|
private function clientId(): string
|
|
{
|
|
return $this->config->getString('external_payment_client_id', '');
|
|
}
|
|
|
|
private function clientSecret(): string
|
|
{
|
|
return $this->config->getString('external_payment_client_secret', '');
|
|
}
|
|
|
|
private function gatewayName(): string
|
|
{
|
|
return strtolower($this->config->getString('external_payment_gateway', 'zarinpall'));
|
|
}
|
|
|
|
private function signer(): MontifyHmacSigner
|
|
{
|
|
return new MontifyHmacSigner($this->clientId(), $this->clientSecret());
|
|
}
|
|
}
|