- Add Enums: TransactionType, TransactionStatus, PaymentGateway - Migrations: wallets, wallet_transactions, wallet_activity_logs - Models: Wallet, WalletTransaction, WalletActivityLog - Services: WalletService with complete business logic - Controllers: WalletController, PaymentController - Filament Resources: WalletResource, WalletTransactionResource - Artisan Command: ifnex:token for API token generation - Sanctum integration with statefulApi and exception handling - Full API endpoints: balance, transactions, admin-adjust, freeze/unfreeze
213 lines
8.0 KiB
PHP
213 lines
8.0 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Api;
|
||
|
||
use App\Enums\PaymentGateway;
|
||
use App\Enums\TransactionStatus;
|
||
use App\Enums\TransactionType;
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\User;
|
||
use App\Models\Wallet;
|
||
use App\Models\WalletActivityLog;
|
||
use App\Models\WalletTransaction;
|
||
use App\Services\WalletService;
|
||
use App\Services\ZarinpalService;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class PaymentController extends Controller
|
||
{
|
||
public function __construct(
|
||
protected WalletService $walletService,
|
||
protected ZarinpalService $zarinpalService
|
||
) {}
|
||
|
||
/**
|
||
* ارسال کاربر به درگاه پرداخت
|
||
*/
|
||
public function redirectToGateway(Request $request): JsonResponse
|
||
{
|
||
$user = $request->user();
|
||
|
||
if (!$user) {
|
||
return response()->json(['message' => 'Unauthorized'], 401);
|
||
}
|
||
|
||
$validated = $request->validate([
|
||
'amount' => ['required', 'numeric', 'min:10000'], // حداقل ۱۰ هزار ریال
|
||
'description' => ['nullable', 'string', 'max:500'],
|
||
'frontend_callback' => ['nullable', 'url', 'max:500'], // URL فرانتاند برای redirect بعد از پرداخت
|
||
]);
|
||
|
||
$wallet = $user->wallet ?? Wallet::create([
|
||
'user_id' => $user->id,
|
||
'balance' => 0,
|
||
]);
|
||
|
||
// بررسی مسدود نبودن کیف پول
|
||
if ($wallet->isFrozen()) {
|
||
return response()->json([
|
||
'message' => 'کیف پول شما مسدود است. لطفاً با پشتیبانی تماس بگیرید.',
|
||
], 403);
|
||
}
|
||
|
||
try {
|
||
// ایجاد تراکنش pending
|
||
$transaction = $this->walletService->requestDeposit(
|
||
wallet: $wallet,
|
||
amount: $validated['amount'],
|
||
description: $validated['description'] ?? 'شارژ آنلاین کیف پول',
|
||
user: $user,
|
||
gateway: PaymentGateway::ZARINPAL
|
||
);
|
||
|
||
// دریافت لینک پرداخت از زرینپال
|
||
$paymentData = $this->zarinpalService->requestPayment(
|
||
amount: $validated['amount'],
|
||
description: $validated['description'] ?? 'شارژ کیف پول IFNEX',
|
||
mobile: $user->phone,
|
||
email: $user->email
|
||
);
|
||
|
||
// ذخیره authority و frontend_callback در metadata
|
||
$metadata = $transaction->metadata ?? [];
|
||
$metadata['zarinpal_authority'] = $paymentData['authority'];
|
||
$metadata['frontend_callback'] = $validated['frontend_callback'] ?? null;
|
||
$transaction->update(['metadata' => $metadata]);
|
||
|
||
return response()->json([
|
||
'success' => true,
|
||
'message' => 'در حال انتقال به درگاه پرداخت...',
|
||
'payment_url' => $paymentData['payment_url'],
|
||
'transaction_id' => $transaction->id,
|
||
'authority' => $paymentData['authority'],
|
||
'amount' => $paymentData['amount'],
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Payment gateway error', [
|
||
'user_id' => $user->id,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
|
||
return response()->json([
|
||
'success' => false,
|
||
'message' => 'خطا در ارتباط با درگاه پرداخت: ' . $e->getMessage(),
|
||
], 500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Callback از درگاه پرداخت (بعد از بازگشت کاربر)
|
||
*/
|
||
public function callback(Request $request): RedirectResponse
|
||
{
|
||
$authority = $request->input('Authority');
|
||
$status = $request->input('Status'); // OK یا NOK
|
||
|
||
// پیدا کردن تراکنش بر اساس authority
|
||
$transaction = WalletTransaction::whereJsonContains('metadata->zarinpal_authority', $authority)->first();
|
||
|
||
if (!$transaction) {
|
||
Log::warning('Payment callback: transaction not found', ['authority' => $authority]);
|
||
return redirect(config('ifnex.zarinpal.frontend_failure_url', '/payment/failed'));
|
||
}
|
||
|
||
$frontendCallback = $transaction->metadata['frontend_callback'] ?? config('ifnex.zarinpal.frontend_failure_url', '/payment/failed');
|
||
|
||
// کاربر پرداخت را لغو کرده
|
||
if ($status !== 'OK') {
|
||
$this->walletService->failTransaction($transaction, 'کاربر پرداخت را لغو کرد یا پرداخت ناموفق بود.');
|
||
|
||
$failureUrl = $frontendCallback . '?' . http_build_query([
|
||
'status' => 'failed',
|
||
'transaction_id' => $transaction->id,
|
||
'message' => 'پرداخت ناموفق بود',
|
||
]);
|
||
|
||
return redirect($failureUrl);
|
||
}
|
||
|
||
try {
|
||
// بررسی صحت پرداخت
|
||
$verification = $this->zarinpalService->verifyPayment(
|
||
authority: $authority,
|
||
expectedAmount: $transaction->amount
|
||
);
|
||
|
||
if (!$verification['success']) {
|
||
$this->walletService->failTransaction(
|
||
$transaction,
|
||
'تأیید درگاه ناموفق بود: ' . ($verification['error_message'] ?? 'نامشخص')
|
||
);
|
||
|
||
$failureUrl = $frontendCallback . '?' . http_build_query([
|
||
'status' => 'failed',
|
||
'transaction_id' => $transaction->id,
|
||
'message' => 'تأیید پرداخت ناموفق بود',
|
||
]);
|
||
|
||
return redirect($failureUrl);
|
||
}
|
||
|
||
// تأیید تراکنش و افزایش موجودی
|
||
$this->walletService->completeDeposit(
|
||
transaction: $transaction,
|
||
gatewayReferenceId: $verification['ref_id'] ?? $authority
|
||
);
|
||
|
||
$successUrl = config('ifnex.zarinpal.frontend_success_url', '/payment/success') . '?' . http_build_query([
|
||
'status' => 'success',
|
||
'transaction_id' => $transaction->id,
|
||
'ref_id' => $verification['ref_id'] ?? '',
|
||
'amount' => $transaction->amount,
|
||
]);
|
||
|
||
return redirect($successUrl);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Payment callback error', [
|
||
'transaction_id' => $transaction->id,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
|
||
$this->walletService->failTransaction($transaction, 'خطای سیستمی: ' . $e->getMessage());
|
||
|
||
$failureUrl = $frontendCallback . '?' . http_build_query([
|
||
'status' => 'failed',
|
||
'transaction_id' => $transaction->id,
|
||
'message' => 'خطای سیستمی در پردازش پرداخت',
|
||
]);
|
||
|
||
return redirect($failureUrl);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* استعلام وضعیت یک پرداخت
|
||
*/
|
||
public function checkStatus(Request $request, $transactionId): JsonResponse
|
||
{
|
||
$user = $request->user();
|
||
|
||
$transaction = WalletTransaction::where('id', $transactionId)
|
||
->whereHas('wallet', fn ($q) => $q->where('user_id', $user->id))
|
||
->first();
|
||
|
||
if (!$transaction) {
|
||
return response()->json(['message' => 'تراکنش یافت نشد'], 404);
|
||
}
|
||
|
||
return response()->json([
|
||
'transaction_id' => $transaction->id,
|
||
'status' => $transaction->status->label(),
|
||
'status_code' => $transaction->status->value,
|
||
'amount' => $transaction->amount,
|
||
'description' => $transaction->description,
|
||
'reference_id' => $transaction->gateway_reference_id,
|
||
'created_at' => $transaction->created_at->toIso8601String(),
|
||
]);
|
||
}
|
||
} |