where('status', TransactionStatus::PENDING->value) ->whereNotNull('proof_image') ->count(); return response()->json(['error' => false, 'data' => ['count' => $count]]); } public function index(Request $request): JsonResponse { $query = Transaction::with('customer') ->where('type', 'manual') ->orderByDesc('created_at'); if ($request->filled('status')) { $query->where('status', $request->integer('status')); } if ($request->filled('filter')) { $search = $request->string('filter')->trim(); $query->where(function ($q) use ($search) { $q->where('public_code', 'like', "%{$search}%") ->orWhereHas('customer', fn($c) => $c ->where('fullname', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%")); }); } $perPage = $request->integer('per_page', 15); $paginated = $query->paginate($perPage); $items = $paginated->map(fn(Transaction $t) => [ 'id' => $t->id, 'uuid' => $t->uuid, 'public_code' => $t->public_code, 'trans' => $t->trans, 'amount' => $t->amount, 'type' => $t->type, 'status' => $t->status, 'status_label' => TransactionStatus::from((int) $t->status)->label(), 'proof_image' => $t->proof_image ? Storage::disk('public')->url($t->proof_image) : null, 'created_at' => $t->created_at?->toISOString(), 'customer' => $t->customer ? [ 'uuid' => $t->customer->uuid, 'fullname' => $t->customer->fullname, 'email' => $t->customer->email, 'phone' => $t->customer->phone, 'total' => $t->customer->total, 'total_pending' => $t->customer->total_pending, ] : null, ]); return response()->json([ 'error' => false, 'data' => [ 'items' => $items, 'total' => $paginated->total(), 'per_page' => $paginated->perPage(), 'current_page' => $paginated->currentPage(), 'last_page' => $paginated->lastPage(), ], ]); } public function approve(string $uuid): JsonResponse { try { // Lock the transaction (and customer) row for the whole read-check-write so two // admins approving the same transaction concurrently cannot both credit the customer. $transaction = DB::transaction(function () use ($uuid) { $transaction = Transaction::where('uuid', $uuid) ->where('type', 'manual') ->lockForUpdate() ->first(); if (!$transaction || (int) $transaction->status !== TransactionStatus::PENDING->value) { return null; } $transaction->update(['status' => TransactionStatus::COMPLETED->value]); $customer = Customer::where('id', $transaction->customer_id)->lockForUpdate()->first(); $customer->total += $transaction->amount; $customer->credit += (int) floor($transaction->amount / 100); $customer->total_pending = max(0, $customer->total_pending - $transaction->amount); $customer->save(); Log::create([ 'agent' => 'admin', 'client_ip' => request()->ip(), 'description' => 'Admin duyệt nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ (+' . floor($transaction->amount / 100) . ' credit)', 'customer_id' => $customer->id, ]); Notification::notifyCustomer($customer, [ 'type' => 'deposit_success', 'title' => 'Nạp tiền thành công', 'body' => 'Giao dịch ' . $transaction->public_code . ' đã được duyệt.', 'link' => '/lich-su-giao-dich/' . $transaction->id, 'data' => [ 'transaction_uuid' => $transaction->uuid, 'amount' => $transaction->amount, 'public_code' => $transaction->public_code, ], ]); return $transaction->setRelation('customer', $customer); }); } catch (\Exception $e) { FacadesLog::error('AdminTransactionController::approve error: ' . $e->getMessage()); return response()->json([ 'error' => true, 'message' => 'Có lỗi xảy ra, vui lòng thử lại', ], 500); } if (!$transaction) { return response()->json([ 'error' => true, 'message' => 'Giao dịch không tồn tại hoặc không thể duyệt', ], 404); } $customer = $transaction->customer; try { Mail::to($customer->email)->send(new ManualDepositStatus([ 'customer_id' => $customer->id, 'customer_email' => $customer->email, 'customer_name' => $customer->fullname, 'transaction_code' => $transaction->public_code, 'amount' => $transaction->amount, 'status_label' => 'Đã duyệt', 'status_color' => '#15803d', 'status_bg' => '#f0fdf4', 'subject' => 'Yêu cầu nạp tiền đã được duyệt - ' . $transaction->public_code, 'status_message' => 'Yêu cầu nạp tiền của bạn đã được duyệt. Số tiền đã được cộng vào tài khoản và có thể sử dụng ngay.', 'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), 'processed_date' => now()->format('d/m/Y H:i:s'), ])); } catch (\Throwable $e) { FacadesLog::error('Không thể gửi email duyệt nạp tiền thủ công: ' . $e->getMessage(), [ 'transaction_code' => $transaction->public_code, 'customer_email' => $customer->email, ]); } return response()->json([ 'error' => false, 'message' => 'Đã duyệt giao dịch thành công', ]); } public function reject(string $uuid): JsonResponse { try { // Same lock-then-verify-status pattern as approve() — prevents a duplicate // reject (or reject-after-approve) race from double-adjusting total_pending. $transaction = DB::transaction(function () use ($uuid) { $transaction = Transaction::where('uuid', $uuid) ->where('type', 'manual') ->lockForUpdate() ->first(); if (!$transaction || (int) $transaction->status !== TransactionStatus::PENDING->value) { return null; } $transaction->update(['status' => TransactionStatus::FAILED->value]); $customer = Customer::where('id', $transaction->customer_id)->lockForUpdate()->first(); $customer->total_pending = max(0, $customer->total_pending - $transaction->amount); $customer->save(); Log::create([ 'agent' => 'admin', 'client_ip' => request()->ip(), 'description' => 'Admin từ chối nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ', 'customer_id' => $customer->id, ]); return $transaction->setRelation('customer', $customer); }); } catch (\Exception $e) { FacadesLog::error('AdminTransactionController::reject error: ' . $e->getMessage()); return response()->json([ 'error' => true, 'message' => 'Có lỗi xảy ra, vui lòng thử lại', ], 500); } if (!$transaction) { return response()->json([ 'error' => true, 'message' => 'Giao dịch không tồn tại hoặc không thể từ chối', ], 404); } $customer = $transaction->customer; try { Mail::to($customer->email)->send(new ManualDepositStatus([ 'customer_id' => $customer->id, 'customer_email' => $customer->email, 'customer_name' => $customer->fullname, 'transaction_code' => $transaction->public_code, 'amount' => $transaction->amount, 'status_label' => 'Không phù hợp', 'status_color' => '#b91c1c', 'status_bg' => '#fef2f2', 'subject' => 'Yêu cầu nạp tiền không phù hợp - ' . $transaction->public_code, 'status_message' => 'Yêu cầu nạp tiền của bạn chưa phù hợp để được duyệt. Vui lòng kiểm tra lại chứng từ hoặc liên hệ bộ phận hỗ trợ để được hướng dẫn thêm.', 'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), 'processed_date' => now()->format('d/m/Y H:i:s'), ])); } catch (\Throwable $e) { FacadesLog::error('Không thể gửi email từ chối nạp tiền thủ công: ' . $e->getMessage(), [ 'transaction_code' => $transaction->public_code, 'customer_email' => $customer->email, ]); } return response()->json([ 'error' => false, 'message' => 'Đã từ chối giao dịch', ]); } }