ifnex/04_Laravel/app/Services/ZarinpalService.php
Kazem Alghasi 2e7f975094 feat(wallet): complete online payment with Zarinpal + mock gateway
- Add ZarinpalService for production payment gateway
- Add MockZarinpalService for testing without real merchant ID
- Add MockGatewayController for simulating payment page
- Add beautiful mock gateway UI with RTL support
- Update PaymentController to switch between mock/real based on config
- Fix double-click bug: prevent re-processing completed transactions
- Add payment-result page with success/failure UI
- All API endpoints tested successfully:
  * balance, transactions, activity-log
  * admin-adjust (deposit/withdrawal)
  * freeze/unfreeze
  * online payment redirect + callback + verify
- Wallet balance verified: 67,700,000 IRR after all transactions
- 5 transactions recorded with correct gateways (manual/zarinpal)

Closes: Payment gateway integration for Phase 2
2026-08-08 00:46:54 +03:30

126 lines
4.0 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ZarinpalService
{
private string $merchantId;
private string $baseUrl;
private ?string $callbackUrl;
public function __construct()
{
$this->merchantId = config('ifnex.zarinpal.merchant_id', 'fake-merchant-id-for-testing');
$isSandbox = config('ifnex.zarinpal.sandbox', true);
$this->baseUrl = $isSandbox
? 'https://sandbox.zarinpal.com/pg/v4'
: 'https://api.zarinpal.com/pg/v4';
// ✅ اصلاح: مدیریت null و تنظیم callback
$this->callbackUrl = config('ifnex.zarinpal.callback_url')
?? url('/api/v1/payment/callback');
}
public function requestPayment(
float $amount,
string $description,
?string $mobile = null,
?string $email = null
): array {
$amountToman = (int) round($amount / 10);
$response = Http::timeout(30)->post("{$this->baseUrl}/PaymentRequest.json", [
'merchant_id' => $this->merchantId,
'amount' => $amountToman,
'currency' => 'IRR',
'description' => $description,
'callback_url' => $this->callbackUrl,
'metadata' => [
'mobile' => $mobile,
'email' => $email,
],
]);
$result = $response->json();
Log::info('Zarinpal Payment Request', [
'amount' => $amountToman,
'response' => $result,
]);
if (!isset($result['data']['code']) || $result['data']['code'] !== 100) {
throw new \Exception('خطا در ارتباط با زرین‌پال: ' . ($result['errors']['message'] ?? $result['data']['message'] ?? 'نامشخص'));
}
$authority = $result['data']['authority'];
$paymentUrl = $this->getPaymentUrl($authority);
return [
'authority' => $authority,
'payment_url' => $paymentUrl,
'amount' => $amount,
'amount_toman' => $amountToman,
];
}
public function verifyPayment(string $authority, float $expectedAmount): array
{
$amountToman = (int) round($expectedAmount / 10);
$response = Http::timeout(30)->post("{$this->baseUrl}/PaymentVerification.json", [
'merchant_id' => $this->merchantId,
'authority' => $authority,
'amount' => $amountToman,
]);
$result = $response->json();
Log::info('Zarinpal Payment Verification', [
'authority' => $authority,
'response' => $result,
]);
if (!isset($result['data']['code'])) {
return [
'success' => false,
'error_code' => -1,
'error_message' => 'پاسخ نامعتبر از زرین‌پال',
];
}
if ($result['data']['code'] === 100) {
return [
'success' => true,
'ref_id' => $result['data']['ref_id'],
'card_hash' => $result['data']['card_hash'] ?? null,
'card_pan' => $result['data']['card_pan'] ?? null,
];
} elseif ($result['data']['code'] === 101) {
return [
'success' => true,
'ref_id' => $result['data']['ref_id'] ?? null,
'already_verified' => true,
];
}
return [
'success' => false,
'error_code' => $result['data']['code'] ?? null,
'error_message' => $result['errors']['message'] ?? 'نامشخص',
];
}
private function getPaymentUrl(string $authority): string
{
$isSandbox = config('ifnex.zarinpal.sandbox', true);
$gatewayUrl = $isSandbox
? 'https://sandbox.zarinpal.com/pg/StartPay/'
: 'https://www.zarinpal.com/pg/StartPay/';
return $gatewayUrl . $authority;
}
}