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); } // Huỷ transaction prepare cũ (chưa có proof) nếu còn tồn tại $old = Transaction::where('customer_id', $user->id) ->where('type', 'manual') ->where('status', TransactionStatus::PENDING->value) ->whereNull('proof_image') ->latest() ->first(); if ($old) { DB::transaction(function () use ($old, $user) { $user->total_pending = max(0, $user->total_pending - $old->amount); $user->save(); $old->forceDelete(); }); } $uuid = Str::uuid()->toString(); $publicCode = 'MAN-' . now()->format('Ymd') . '-' . strtoupper(Str::random(6)); DB::beginTransaction(); try { Transaction::create([ 'uuid' => $uuid, 'public_code' => $publicCode, 'trans' => 'MAN' . time(), 'amount' => $request->amount, 'customer_id' => $user->id, 'type' => 'manual', 'status' => TransactionStatus::PENDING->value, ]); $user->total_pending += $request->amount; $user->save(); 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' => [ 'uuid' => $uuid, 'public_code' => $publicCode, ], ]); } /** * 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 = Transaction::where('uuid', $uuid) ->where('customer_id', $user->id) ->where('type', 'manual') ->where('status', TransactionStatus::PENDING->value) ->whereNull('proof_image') ->firstOrFail(); $amount = $transaction->amount; $publicCode = $transaction->public_code; DB::transaction(function () use ($transaction, $user) { $user->total_pending = max(0, $user->total_pending - $transaction->amount); $user->save(); $transaction->forceDelete(); }); $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 = Transaction::where('uuid', $request->uuid) ->where('customer_id', $user->id) ->where('type', 'manual') ->where('status', TransactionStatus::PENDING->value) ->whereNull('proof_image') ->first(); if (!$transaction) { return response()->json([ 'error' => true, 'message' => 'Giao dịch không hợp lệ hoặc đã được xử lý', ], 422); } $transaction->update(['proof_image' => $request->proof_image]); try { (new Telegram())->SendManualDepositPendingApproval($user, $transaction); $this->sendManualDepositAdminEmail($user, $transaction, $request); } catch (\Exception) { // Không để lỗi Telegram ảnh hưởng response } 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' => $customer->fullname ?: trim(($customer->firstname ?? '') . ' ' . ($customer->lastname ?? '')), '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, ]); } } }