user(); $transaction = Transaction::where('customer_id', $user->id) ->where('type', 'manual') ->where('status', TransactionStatus::PENDING->value) ->whereNotNull('proof_image') ->latest() ->first(); if (!$transaction) { return response()->json(['error' => false, 'data' => ['has_pending' => false]]); } return response()->json([ 'error' => false, 'data' => [ 'has_pending' => true, 'transaction' => [ 'public_code' => $transaction->public_code, 'amount' => $transaction->amount, 'created_at' => $transaction->created_at?->toISOString(), ], ], ]); } /** * POST /v1/transaction/manual-deposit/prepare * Tạo transaction PENDING và trả về public_code để show trong QR / nội dung CK. * Client gọi khi chuyển sang bước chọn ngân hàng. */ public function prepare(DepositRequest $request): JsonResponse { $user = $request->user(); // Chặn nếu đã có GD đã submit đang chờ admin duyệt $submitted = Transaction::where('customer_id', $user->id) ->where('type', 'manual') ->where('status', TransactionStatus::PENDING->value) ->whereNotNull('proof_image') ->exists(); if ($submitted) { return response()->json([ 'error' => true, 'message' => 'Bạn đang có yêu cầu nạp tiền chờ duyệt. Vui lòng chờ admin xác nhận trước khi tạo yêu cầu mới.', ], 422); } ManualDepositDraft::query() ->where('customer_id', $user->id) ->get() ->each(fn (Transaction $draft) => ManualDepositDraft::discard($draft)); $uuid = Str::uuid()->toString(); $publicCode = Transaction::newPublicCode('MAN'); DB::beginTransaction(); try { $draft = Transaction::create([ 'uuid' => $uuid, 'public_code' => $publicCode, 'trans' => 'MAN' . time(), 'amount' => $request->amount, 'customer_id' => $user->id, 'type' => 'manual', 'status' => TransactionStatus::PENDING->value, ]); DB::commit(); } catch (\Exception) { DB::rollBack(); return response()->json(['error' => true, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500); } $this->activityLogger->log( event: 'deposit_prepare', logName: 'Transaction', description: "Customer {$user->email} tạo yêu cầu nạp tiền thủ công: " . number_format($request->amount) . ' VNĐ - Mã: ' . $publicCode, newValues: ['public_code' => $publicCode, 'amount' => $request->amount, 'type' => 'manual'], ); return response()->json([ 'error' => false, 'data' => $this->draftPayload($draft), ]); } /** * GET /v1/transaction/manual-deposit/{uuid} */ public function show(Request $request, string $uuid): JsonResponse { $draft = ManualDepositDraft::query() ->where('uuid', $uuid) ->where('customer_id', $request->user()->id) ->first(); if (! $draft) { return response()->json(['error' => true, 'message' => 'Mã thanh toán không tồn tại'], 404); } if (ManualDepositDraft::isExpired($draft)) { ManualDepositDraft::discard($draft); return response()->json(['error' => true, 'message' => 'Mã thanh toán đã hết hạn, vui lòng tạo mã mới'], 410); } return response()->json(['error' => false, 'data' => $this->draftPayload($draft)]); } private function draftPayload(Transaction $draft): array { return [ 'uuid' => $draft->uuid, 'public_code' => $draft->public_code, 'amount' => (int) $draft->amount, 'expires_at' => ManualDepositDraft::expiresAt($draft)->toISOString(), ]; } /** * DELETE /v1/transaction/manual-deposit/{uuid}/cancel * Huỷ transaction đang prepare (chưa có proof, chưa submit). * Client gọi khi user quay lại step 1 để đổi số tiền. */ public function cancel(Request $request, string $uuid): JsonResponse { $user = $request->user(); $transaction = ManualDepositDraft::query() ->where('uuid', $uuid) ->where('customer_id', $user->id) ->firstOrFail(); $amount = $transaction->amount; $publicCode = $transaction->public_code; ManualDepositDraft::discard($transaction); $this->activityLogger->log( event: 'deposit_cancel', logName: 'Transaction', description: "Customer {$user->email} huỷ yêu cầu nạp tiền thủ công: " . number_format($amount) . ' VNĐ - Mã: ' . $publicCode, newValues: ['public_code' => $publicCode, 'amount' => $amount], ); return response()->json(['error' => false, 'message' => 'Đã huỷ yêu cầu']); } /** * POST /v1/transaction/manual-deposit/confirm * Attach proof image vào transaction đã prepare rồi đánh dấu đã submit. * Client gọi sau khi upload proof ở step 3. */ public function confirm(Request $request): JsonResponse { $request->validate([ 'uuid' => 'required|string|uuid', 'proof_image' => 'required|string|max:500', ]); $user = $request->user(); if ( !str_starts_with($request->proof_image, "deposit-proofs/{$user->id}/") || str_contains($request->proof_image, '..') || !Storage::disk('local')->exists($request->proof_image) || Transaction::where('proof_image', $request->proof_image)->exists() ) { return response()->json([ 'error' => true, 'message' => 'Chứng từ không hợp lệ', ], 422); } $transaction = DB::transaction(function () use ($request, $user) { $draft = ManualDepositDraft::query() ->where('uuid', $request->uuid) ->where('customer_id', $user->id) ->lockForUpdate() ->first(); if (! $draft || ManualDepositDraft::isExpired($draft)) { return null; } $draft->update(['proof_image' => $request->proof_image]); $user->newQuery()->whereKey($user->getKey())->increment('total_pending', $draft->amount); return $draft; }); if (!$transaction) { return response()->json([ 'error' => true, 'message' => 'Mã thanh toán đã hết hạn hoặc đã được xử lý, vui lòng tạo mã mới', ], 422); } Notification::notifyAdmins([ 'type' => 'manual_deposit_pending', 'title' => 'Có yêu cầu nạp tiền chờ duyệt', 'body' => sprintf( '%s vừa gửi chứng từ nạp %s VNĐ (%s).', $user->fullname, number_format((float) $transaction->amount, 0, ',', '.'), $transaction->public_code, ), 'link' => '/transactions', 'data' => [ 'transaction_uuid' => $transaction->uuid, 'transaction_code' => $transaction->public_code, 'customer_id' => $user->id, 'amount' => (int) $transaction->amount, ], ]); try { (new Telegram())->SendManualDepositPendingApproval($user, $transaction); } catch (\Throwable $e) { \Log::error('Không thể gửi Telegram thông báo nạp tiền chờ duyệt: ' . $e->getMessage(), [ 'transaction_code' => $transaction->public_code, ]); } $this->sendManualDepositAdminEmail($user, $transaction, $request); Log::create([ 'agent' => 'api', 'client_ip' => $request->ip(), 'description' => 'Gửi yêu cầu nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ (chờ duyệt)', 'customer_id' => $user->id, ]); $this->activityLogger->log( event: 'deposit_confirm', logName: 'Transaction', description: "Customer {$user->email} nộp chứng từ nạp tiền thủ công: " . number_format($transaction->amount) . ' VNĐ - Mã: ' . $transaction->public_code, newValues: ['public_code' => $transaction->public_code, 'amount' => $transaction->amount, 'has_proof' => true], ); try { Mail::to($user->email)->send(new ManualDepositStatus([ 'customer_id' => $user->id, 'customer_email' => $user->email, 'customer_name' => $user->fullname, 'transaction_code' => $transaction->public_code, 'amount' => $transaction->amount, 'status_label' => 'Đang chờ duyệt', 'status_color' => '#f16723', 'status_bg' => '#fff7ed', 'subject' => 'Yêu cầu nạp tiền đang chờ duyệt - ' . $transaction->public_code, 'status_message' => 'Yêu cầu nạp tiền của bạn đã được ghi nhận. Chúng tôi sẽ kiểm tra chứng từ và phản hồi kết quả trong thời gian sớm nhất.', 'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), ])); } catch (\Throwable $e) { \Log::error('Không thể gửi email trạng thái nạp tiền thủ công: ' . $e->getMessage(), [ 'transaction_code' => $transaction->public_code, 'customer_email' => $user->email, ]); } return response()->json([ 'error' => false, 'message' => 'Yêu cầu nạp tiền đã được ghi nhận, vui lòng chờ admin xác nhận', 'data' => ['public_code' => $transaction->public_code], ]); } /** * POST /v1/transaction/manual-deposit/upload-proof * Upload ảnh chứng từ, trả về path để dùng ở bước confirm. */ public function uploadProof(Request $request): JsonResponse { $request->validate([ 'file' => 'required|image|mimes:jpeg,jpg,png,webp|max:5120', ]); $user = $request->user(); $path = $request->file('file')->store('deposit-proofs/' . $user->id, 'local'); return response()->json([ 'error' => false, 'path' => $path, ]); } private function sendManualDepositAdminEmail($customer, Transaction $transaction, Request $request): void { try { $setting = Setting::where('key', 'setting')->first(); $settingValue = $setting ? json_decode((string) $setting->value, true) : []; $adminEmail = $settingValue['admin_email'] ?? null; if (empty($adminEmail)) { \Log::warning('Manual deposit admin email not sent: admin_email not configured.', [ 'transaction_code' => $transaction->public_code, ]); return; } Mail::to($adminEmail)->send(new ManualDepositPendingNotification([ 'company_name' => $settingValue['company'] ?? config('app.name', 'Portal Admin'), 'customer_name' => (string) $customer->fullname, 'customer_email' => $customer->email, 'customer_phone' => $customer->phone ?? null, 'client_ip' => $request->ip(), 'transaction_code' => $transaction->public_code, 'amount' => $transaction->amount, 'proof_image_url' => $transaction->proof_image_urls[0] ?? null, 'created_at' => now()->format('d/m/Y H:i:s'), ])); } catch (\Throwable $e) { \Log::error('Không thể gửi email admin thông báo nạp tiền chờ duyệt: ' . $e->getMessage(), [ 'transaction_code' => $transaction->public_code, 'customer_email' => $customer->email ?? null, ]); } } }