Files
boursecu_server/app/Services/Payment/Gateways/MontifyPartnerGateway.php
T

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());
}
}