diff --git a/app/Enums/TransactionStatus.php b/app/Enums/TransactionStatus.php new file mode 100644 index 0000000..f8fdfd8 --- /dev/null +++ b/app/Enums/TransactionStatus.php @@ -0,0 +1,28 @@ + 'Chờ xử lý', + self::COMPLETED => 'Thành công', + self::FAILED => 'Thất bại', + }; + } + + public static function getOptions(): array + { + return [ + self::PENDING->value => self::PENDING->label(), + self::COMPLETED->value => self::COMPLETED->label(), + self::FAILED->value => self::FAILED->label(), + ]; + } +} diff --git a/app/Http/Controllers/Api/Transaction/TransactionController.php b/app/Http/Controllers/Api/Transaction/TransactionController.php new file mode 100644 index 0000000..ac97688 --- /dev/null +++ b/app/Http/Controllers/Api/Transaction/TransactionController.php @@ -0,0 +1,209 @@ +vnpayService = $vnpayService; + } + + public function handle(DepositRequest $request) + { + $user = $request->user(); + $transRef = time(); + + $transaction = Transaction::create([ + 'trans' => 'DEP' . $transRef, + 'amount' => $request->amount, + 'customer_id' => $user->id, + 'status' => TransactionStatus::PENDING->value, + ]); + + $paymentUrl = $this->vnpayService->createPaymentUrl([ + 'amount' => $request->amount, + 'transaction_ref' => $transRef, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Tạo yêu cầu nạp tiền thành công', + 'data' => [ + 'transaction_id' => $transaction->trans, + 'payment_url' => $paymentUrl, + ], + ]); + } + + public function returnCallback(Request $request) + { + $data = $request->all(); + + $vnp_TxnRef = $data['vnp_TxnRef'] ?? null; + $vnp_Amount = $data['vnp_Amount'] ?? 0; + + $transaction = Transaction::where('trans', 'DEP' . $vnp_TxnRef)->first(); + + if (!$transaction) { + return response()->json([ + 'error' => true, + 'message' => 'Giao dịch không tồn tại', + 'debug' => [ + 'vnp_TxnRef' => $vnp_TxnRef, + 'search_trans' => 'DEP' . $vnp_TxnRef, + ] + ], 404); + } + + if ($transaction->status == TransactionStatus::COMPLETED->value) { + return response()->json([ + 'error' => false, + 'message' => 'Giao dịch đã được xử lý trước đó', + 'data' => $transaction, + ]); + } + + $user = $request->user(); + + if (!$user) { + return response()->json([ + 'error' => true, + 'message' => 'Vui lòng đăng nhập', + 'debug' => [ + 'transaction_found' => true, + 'trans' => $transaction->trans, + 'amount_db' => $transaction->amount, + 'amount_vnp' => $vnp_Amount, + ] + ], 401); + } + + if ($transaction->customer_id !== $user->id) { + return response()->json([ + 'error' => true, + 'message' => 'Giao dịch không thuộc về bạn', + ], 403); + } + + $verify = $this->vnpayService->verifyPayment($data); + + if (!$verify['isValid']) { + return response()->json([ + 'error' => true, + 'message' => 'Dữ liệu không hợp lệ', + ], 400); + } + + if (!$verify['isSuccess']) { + $transaction->update(['status' => TransactionStatus::FAILED->value]); + + return response()->json([ + 'error' => true, + 'message' => 'Thanh toán thất bại', + 'data' => $verify['data'], + ], 400); + } + + $originalAmount = $transaction->amount * 100; + if ($vnp_Amount != $originalAmount) { + $transaction->update(['status' => TransactionStatus::FAILED->value]); + return response()->json([ + 'error' => true, + 'message' => 'Số tiền không khớp', + 'debug' => [ + 'amount_db' => $transaction->amount, + 'amount_db_times_100' => $originalAmount, + 'amount_vnp' => $vnp_Amount, + ] + ], 400); + } + + try { + DB::beginTransaction(); + + $transaction->update(['status' => TransactionStatus::COMPLETED->value]); + + $customer = Customer::find($transaction->customer_id); + $customer->total += $transaction->amount; + $customer->credit += $transaction->amount / 100; + $customer->save(); + + Log::create([ + 'agent' => 'api', + 'client_ip' => $request->ip(), + 'description' => 'Nạp tiền vào tài khoản: ' . number_format($transaction->amount) . ' VNĐ (+' . number_format($transaction->amount / 100) . ' credit)', + 'customer_id' => $transaction->customer_id, + ]); + + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + return response()->json([ + 'error' => true, + 'message' => 'Lỗi xử lý giao dịch: ' . $e->getMessage(), + ], 500); + } + + return response()->json([ + 'error' => false, + 'message' => 'Thanh toán thành công', + 'data' => [ + 'transaction' => $transaction->fresh(), + 'customer' => [ + 'total' => $customer->total, + 'credit' => $customer->credit, + ], + ], + ]); + } + + public function getTransactions(Request $request) + { + $user = $request->user(); + + $transactions = Transaction::where('customer_id', $user->id) + ->orderBy('created_at', 'desc') + ->get(); + + return response()->json([ + 'error' => false, + 'message' => 'Lấy danh sách giao dịch thành công', + 'data' => $transactions, + ]); + } + + public function getDetailTransaction(Request $request, $slug) + { + $user = $request->user(); + + $transaction = Transaction::where('trans', $slug) + ->where('customer_id', $user->id) + ->first(); + + if (!$transaction) { + return response()->json([ + 'error' => true, + 'message' => 'Giao dịch không tồn tại', + ], 404); + } + + return response()->json([ + 'error' => false, + 'message' => 'Lấy chi tiết giao dịch thành công', + 'data' => $transaction, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Requests/DepositRequest.php b/app/Http/Requests/DepositRequest.php new file mode 100644 index 0000000..3ba2718 --- /dev/null +++ b/app/Http/Requests/DepositRequest.php @@ -0,0 +1,29 @@ + 'required|numeric|min:10000', + ]; + } + + public function messages(): array + { + return [ + 'amount.required' => 'Số tiền nạp không được bỏ trống.', + 'amount.numeric' => 'Số tiền nạp phải là một số.', + 'amount.min' => 'Số tiền nạp tối thiểu là :min đồng.', + ]; + } +} diff --git a/app/Http/Services/VNPayService.php b/app/Http/Services/VNPayService.php index 20c5544..6188cd9 100644 --- a/app/Http/Services/VNPayService.php +++ b/app/Http/Services/VNPayService.php @@ -15,21 +15,27 @@ class VNPayService */ public function createPaymentUrl(array $data) { - $vnp_TxnRef = rand(1, 10000); // Transaction reference + $vnp_TxnRef = $data['transaction_ref'] ?? rand(1, 10000); + + if (!is_numeric($vnp_TxnRef)) { + $vnp_TxnRef = preg_replace('/[^0-9]/', '', $vnp_TxnRef); + } + $vnp_Amount = $data['amount'] ?? 0; $vnp_Locale = $data['language'] ?? 'vn'; $vnp_BankCode = $data['bankCode'] ?? ''; $vnp_IpAddr = request()->ip(); - // Lấy thông tin người dùng đang đăng nhập - $user = customer(); - $userName = $user ? $user->firstname . ' ' . $user->lastname : 'Khách'; + $user = Auth::guard('sanctum')->user() ?? Auth::guard('users')->user(); + $userName = $user ? $user->fullname : 'Khách'; - // Lấy thông tin đơn hàng nếu có - $orderId = $data['order_id'] ?? $vnp_TxnRef; - - // Tạo nội dung thanh toán với thông tin người dùng và mã đơn hàng - $orderInfo = "Thông tin đơn hàng: " . $userName . " - Mã đơn hàng: " . $orderId; + $isDeposit = isset($data['transaction_ref']); + if ($isDeposit) { + $orderInfo = "Nạp tiền vào tài khoản - Khách hàng: " . $userName; + } else { + $orderId = $data['order_id'] ?? $vnp_TxnRef; + $orderInfo = "Thông tin đơn hàng: " . $userName . " - Mã đơn hàng: " . $orderId; + } // Set timezone Carbon::setLocale('vi'); @@ -38,9 +44,9 @@ class VNPayService $inputData = [ "vnp_Version" => "2.1.0", - "vnp_TmnCode" => setting('setting.vnpay_tmn_code'), + "vnp_TmnCode" => config('vnpay.tmn_code'), "vnp_Amount" => $vnp_Amount * 100, - "vnp_Command" => "billpayment", + "vnp_Command" => "pay", "vnp_CreateDate" => $startTime, "vnp_CurrCode" => "VND", "vnp_IpAddr" => $vnp_IpAddr, @@ -48,8 +54,7 @@ class VNPayService "vnp_OrderInfo" => $orderInfo, "vnp_OrderType" => "other", "vnp_ReturnUrl" => config('vnpay.return_url'), - "vnp_TxnRef" => $orderId, -// "vnp_ExpireDate" => $expire + "vnp_TxnRef" => $vnp_TxnRef, ]; // Bỏ cmt khi nếu muốn chọn thanh toán chọn thẻ thôi @@ -71,9 +76,9 @@ class VNPayService $query .= urlencode($key) . "=" . urlencode($value) . '&'; } - $vnp_Url = config('vnpay.url') . "?" . $query; - $vnpSecureHash = hash_hmac('sha512', $hashdata, setting('setting.vnpay_hash_secret')); - $vnp_Url .= 'vnp_SecureHash=' . $vnpSecureHash; + $vnp_Url = config('vnpay.url') . "?" . rtrim($query, '&'); + $vnpSecureHash = hash_hmac('sha512', $hashdata, config('vnpay.hash_secret')); + $vnp_Url .= '&vnp_SecureHash=' . $vnpSecureHash; return $vnp_Url; } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index a4c7aed..903c62f 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -9,11 +9,18 @@ use Illuminate\Database\Eloquent\SoftDeletes; class Transaction extends Model { use HasFactory, SoftDeletes; - protected $table = 'transaction'; + protected $table = 'transactions'; protected $fillable = [ 'trans', 'amount', - 'customer_id' + 'customer_id', + 'status' ]; + + + public function customer() + { + return $this->belongsTo(Customer::class, 'customer_id'); + } } diff --git a/database/migrations/2024_05_13_165053_transaction.php b/database/migrations/2024_05_13_165053_transaction.php index c5ffcf5..daa982a 100644 --- a/database/migrations/2024_05_13_165053_transaction.php +++ b/database/migrations/2024_05_13_165053_transaction.php @@ -4,33 +4,25 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ - /** - * Run the migrations. - * - * @return void - */ +return new class extends Migration { public function up() { - // - Schema::create('transaction',function (Blueprint $table) { - $table->id()->autoIncrement(); + Schema::create('transactions', function (Blueprint $table) { + $table->id(); $table->string('trans')->nullable(); $table->integer('amount')->nullable(); - $table->bigInteger('customer_id')->default(0)->index(); + + $table->foreignId('customer_id') + ->constrained('customers') + ->cascadeOnDelete(); + $table->timestamps(); + $table->softDeletes(); }); } - /** - * Reverse the migrations. - * - * @return void - */ public function down() { - // Schema::dropIfExists('transactions'); } }; diff --git a/database/migrations/2026_02_18_000000_add_foreign_key_to_transactions.php b/database/migrations/2026_02_18_000000_add_foreign_key_to_transactions.php new file mode 100644 index 0000000..9207c28 --- /dev/null +++ b/database/migrations/2026_02_18_000000_add_foreign_key_to_transactions.php @@ -0,0 +1,30 @@ +id(); + $table->string('trans')->nullable(); + $table->integer('amount')->nullable(); + + $table->foreignId('customer_id') + ->constrained('customers') + ->cascadeOnDelete(); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down() + { + Schema::table('transactions', function (Blueprint $table) { + $table->dropForeign(['customer_id']); + }); + } +}; diff --git a/database/migrations/2026_02_18_000001_add_status_to_transactions.php b/database/migrations/2026_02_18_000001_add_status_to_transactions.php new file mode 100644 index 0000000..0360a70 --- /dev/null +++ b/database/migrations/2026_02_18_000001_add_status_to_transactions.php @@ -0,0 +1,21 @@ +tinyInteger('status')->default(0)->comment('0: pending, 1: completed, 2: fail')->after('amount'); + }); + } + + public function down() + { + Schema::table('transactions', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +}; diff --git a/resources/views/administrator/layout/partials/header.blade.php b/resources/views/administrator/layout/partials/header.blade.php index 3aff996..515fbfa 100644 --- a/resources/views/administrator/layout/partials/header.blade.php +++ b/resources/views/administrator/layout/partials/header.blade.php @@ -184,6 +184,7 @@ + @auth