Implement a comprehensive financial management suite within the Filament admin panel, including new resources for payments and discount codes, along with a financial dashboard. - feat(ui): add Filament Dashboard with finance overview and transaction widgets - feat(ui): implement PaymentResource for monitoring gateway transactions - feat(ui): complete DiscountCodeResource with full CRUD and filtering - feat(wallet): add automatic wallet creation on balance retrieval - fix(wallet): resolve null handling in isFrozen() and balance viewing - fix(db): correct enum type from 'percent' to 'percentage' in discount_codes migration - fix(api): update pricing calculation endpoint to /api/v1/calculate - test: add 21 new feature and service tests covering wallet, payment, and discount logic - chore: remove obsolete PaymentGatewayService and update project status
299 lines
9.8 KiB
PHP
299 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Enums\PaymentGateway;
|
|
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 Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class WalletController extends Controller
|
|
{
|
|
public function __construct(protected WalletService $walletService) {}
|
|
|
|
/**
|
|
* مشاهده موجودی کیف پول کاربر
|
|
*/
|
|
public function balance(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (!$user) {
|
|
return response()->json(['message' => 'Unauthorized'], 401);
|
|
}
|
|
|
|
$wallet = $user->wallet;
|
|
|
|
if (!$wallet) {
|
|
$wallet = Wallet::create([
|
|
'user_id' => $user->id,
|
|
'balance' => 0,
|
|
'total_deposited' => 0,
|
|
'total_withdrawn' => 0,
|
|
]);
|
|
}
|
|
|
|
WalletActivityLog::log(
|
|
$wallet,
|
|
'balance_viewed',
|
|
'مشاهده موجودی کیف پول',
|
|
null,
|
|
['balance' => $wallet->balance],
|
|
$user
|
|
);
|
|
|
|
return response()->json([
|
|
'balance' => $wallet->balance,
|
|
'total_deposited' => $wallet->total_deposited,
|
|
'total_withdrawn' => $wallet->total_withdrawn,
|
|
'is_frozen' => $wallet->isFrozen(),
|
|
'currency' => 'IRR',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* لیست تراکنشهای کاربر (paginated)
|
|
*/
|
|
public function transactions(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (!$user) {
|
|
return response()->json(['message' => 'Unauthorized'], 401);
|
|
}
|
|
|
|
$wallet = $user->wallet;
|
|
|
|
if (!$wallet) {
|
|
return response()->json([
|
|
'transactions' => [],
|
|
'total' => 0,
|
|
'current_page' => 1,
|
|
]);
|
|
}
|
|
|
|
$perPage = $request->query('per_page', 15);
|
|
$status = $request->query('status'); // pending, completed, failed
|
|
$type = $request->query('type'); // deposit, withdrawal
|
|
|
|
$query = $wallet->transactions()->orderByDesc('created_at');
|
|
|
|
if ($status) {
|
|
$query->where('status', $status);
|
|
}
|
|
|
|
if ($type) {
|
|
$query->where('type', $type);
|
|
}
|
|
|
|
$transactions = $query->paginate($perPage);
|
|
|
|
return response()->json([
|
|
'transactions' => $transactions->map(fn ($t) => [
|
|
'id' => $t->id,
|
|
'amount' => $t->amount,
|
|
'formatted_amount' => $t->formatted_amount,
|
|
'type' => $t->type->label(),
|
|
'status' => $t->status->label(),
|
|
'status_code' => $t->status->value,
|
|
'gateway' => $t->gateway?->label(),
|
|
'reference_id' => $t->gateway_reference_id,
|
|
'description' => $t->description,
|
|
'created_at' => $t->created_at->toIso8601String(),
|
|
'created_at_jalali' => \Morilog\Jalali\Jalalian::fromCarbon($t->created_at)->format('Y/m/d H:i'),
|
|
]),
|
|
'total' => $transactions->total(),
|
|
'current_page' => $transactions->currentPage(),
|
|
'last_page' => $transactions->lastPage(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* شارژ/کسر دستی توسط ادمین
|
|
*/
|
|
public function adminAdjust(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return response()->json(['message' => 'Forbidden — Only Super Admin can perform this action'], 403);
|
|
}
|
|
|
|
try {
|
|
$validated = $request->validate([
|
|
'user_id' => ['required', 'exists:users,id'],
|
|
'amount' => ['required', 'numeric', 'not_in:0'], // مثبت = شارژ، منفی = کسر
|
|
'description' => ['required', 'string', 'max:1000'],
|
|
'admin_notes' => ['nullable', 'string', 'max:2000'],
|
|
]);
|
|
} catch (ValidationException $e) {
|
|
return response()->json([
|
|
'message' => 'Validation failed',
|
|
'errors' => $e->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$targetUser = User::findOrFail($validated['user_id']);
|
|
$wallet = $targetUser->wallet ?? Wallet::create([
|
|
'user_id' => $targetUser->id,
|
|
'balance' => 0,
|
|
]);
|
|
|
|
try {
|
|
$amount = abs($validated['amount']);
|
|
|
|
if ($validated['amount'] > 0) {
|
|
// شارژ
|
|
$transaction = $this->walletService->manualDeposit(
|
|
wallet: $wallet,
|
|
amount: $amount,
|
|
description: $validated['description'],
|
|
admin: $user,
|
|
adminNotes: $validated['admin_notes'] ?? null
|
|
);
|
|
$message = 'شارژ دستی با موفقیت انجام شد';
|
|
} else {
|
|
// کسر
|
|
$transaction = $this->walletService->manualWithdrawal(
|
|
wallet: $wallet,
|
|
amount: $amount,
|
|
description: $validated['description'],
|
|
admin: $user,
|
|
adminNotes: $validated['admin_notes'] ?? null
|
|
);
|
|
$message = 'کسر دستی با موفقیت انجام شد';
|
|
}
|
|
|
|
return response()->json([
|
|
'message' => $message,
|
|
'transaction_id' => $transaction->id,
|
|
'target_user_id' => $targetUser->id,
|
|
'target_user_name' => $targetUser->name,
|
|
'balance_before' => $transaction->balance_before,
|
|
'balance_after' => $transaction->balance_after,
|
|
'amount' => $transaction->amount,
|
|
'description' => $transaction->description,
|
|
'admin_notes' => $transaction->admin_notes,
|
|
'performed_by' => $user->name,
|
|
'created_at' => $transaction->created_at->toIso8601String(),
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'message' => 'خطا در انجام عملیات: ' . $e->getMessage(),
|
|
], 422);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* مسدود کردن کیف پول (توسط ادمین)
|
|
*/
|
|
public function freeze(Request $request, $walletId): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return response()->json(['message' => 'Forbidden'], 403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'reason' => ['required', 'string', 'max:1000'],
|
|
]);
|
|
|
|
$wallet = Wallet::findOrFail($walletId);
|
|
$oldFrozen = $wallet->isFrozen();
|
|
|
|
$wallet->freeze($validated['reason'], $user);
|
|
|
|
WalletActivityLog::log(
|
|
$wallet,
|
|
'wallet_frozen',
|
|
'کیف پول مسدود شد توسط ' . $user->name,
|
|
['is_frozen' => $oldFrozen],
|
|
['is_frozen' => true, 'reason' => $validated['reason']],
|
|
$user
|
|
);
|
|
|
|
return response()->json([
|
|
'message' => 'کیف پول با موفقیت مسدود شد',
|
|
'wallet_id' => $wallet->id,
|
|
'reason' => $validated['reason'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* رفع مسدودی کیف پول (توسط ادمین)
|
|
*/
|
|
public function unfreeze(Request $request, $walletId): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return response()->json(['message' => 'Forbidden'], 403);
|
|
}
|
|
|
|
$wallet = Wallet::findOrFail($walletId);
|
|
$oldReason = $wallet->freeze_reason;
|
|
|
|
$wallet->unfreeze($user);
|
|
|
|
WalletActivityLog::log(
|
|
$wallet,
|
|
'wallet_unfrozen',
|
|
'کیف پول از مسدودی خارج شد توسط ' . $user->name,
|
|
['reason' => $oldReason],
|
|
['is_frozen' => false],
|
|
$user
|
|
);
|
|
|
|
return response()->json([
|
|
'message' => 'کیف پول با موفقیت از مسدودی خارج شد',
|
|
'wallet_id' => $wallet->id,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* مشاهده لاگ فعالیتهای کیف پول (توسط ادمین)
|
|
*/
|
|
public function activityLog(Request $request, $walletId): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return response()->json(['message' => 'Forbidden'], 403);
|
|
}
|
|
|
|
$wallet = Wallet::findOrFail($walletId);
|
|
$perPage = $request->query('per_page', 20);
|
|
|
|
$logs = $wallet->activityLogs()->with('user')->paginate($perPage);
|
|
|
|
return response()->json([
|
|
'wallet_id' => $wallet->id,
|
|
'user_name' => $wallet->user->name,
|
|
'balance' => $wallet->balance,
|
|
'is_frozen' => $wallet->isFrozen(),
|
|
'freeze_reason' => $wallet->freeze_reason,
|
|
'logs' => $logs->map(fn ($log) => [
|
|
'id' => $log->id,
|
|
'action' => $log->action,
|
|
'description' => $log->description,
|
|
'old_values' => $log->old_values,
|
|
'new_values' => $log->new_values,
|
|
'performed_by' => $log->user?->name ?? 'سیستم',
|
|
'ip_address' => $log->ip_address,
|
|
'created_at' => $log->created_at->toIso8601String(),
|
|
'created_at_jalali' => \Morilog\Jalali\Jalalian::fromCarbon($log->created_at)->format('Y/m/d H:i'),
|
|
]),
|
|
'total' => $logs->total(),
|
|
'current_page' => $logs->currentPage(),
|
|
'last_page' => $logs->lastPage(),
|
|
]);
|
|
}
|
|
} |