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 diff --git a/routes/client.php b/routes/client.php index fda0bb1..eb80576 100644 --- a/routes/client.php +++ b/routes/client.php @@ -5,9 +5,10 @@ use App\Http\Controllers\Api\Client\ClientController; use App\Http\Controllers\Api\Service\OrderController; use App\Http\Controllers\Api\Service\ProductCategoryController; use App\Http\Controllers\Api\Service\ServiceController; +use App\Http\Controllers\Api\Transaction\TransactionController; use Illuminate\Support\Facades\Route; -Route::prefix('v1')->group(function () { +Route::prefix('v1')->as('api.')->group(function () { // Auth Route::prefix('client')->as('client.') ->group(function () { @@ -52,6 +53,17 @@ Route::prefix('v1')->group(function () { Route::get('{parentSlug}/{childSlug}', 'getChildCategoryWithParent')->name('childWithParent'); }); }); + + + Route::middleware(['api.auth'])->prefix('transaction')->as('transaction.')->group(function () { + Route::controller(TransactionController::class)->group(function () { + Route::get('get-list', 'getTransactions')->name('getList'); + Route::post('pay', 'handle')->name('pay'); + Route::get('return', 'returnCallback')->name('return'); + Route::get('{transaction:slug}', 'getDetailTransaction')->name('detail'); + }); + }); + }); diff --git a/routes/customer.php b/routes/customer.php deleted file mode 100644 index 150bbfb..0000000 --- a/routes/customer.php +++ /dev/null @@ -1,136 +0,0 @@ -controller(ClientController::class) - ->group(function () { - Route::get('dang-nhap', 'Login')->name('user.login'); - Route::post('dang-nhap', 'LoginStore')->name("user.login.store"); - Route::get('quen-mat-khau', 'Reset')->name('user.reset'); - Route::post('quen-mat-khau', 'ResetStore')->name('user.reset.store'); - Route::get('dang-ky-tai-khoan', 'Register')->name('user.register'); - Route::post('dang-ky-tai-khoan', 'RegisterStore')->name('user.register.store'); - Route::get('xac-thuc-tai-khoan', 'Verify')->name('user.verify'); - Route::post('cap-nhat-thong-bao', 'updateNotification')->name('user.updateNotification'); - }); - -Route::middleware("CustomerLoginCheck")->group(function () { - - // Route::get('/trang-chu', [MainController::class, 'index'])->name("home_old"); - Route::get('/', [MainController::class, 'index_new'])->name('home'); - - - Route::get('nhat-ky-hoat-dong', [CustomerLogController::class, 'index'])->name('frontend.customer.log'); - Route::controller(ClientController::class)->group(function () { - Route::get('dang-xuat', 'Logout')->name("user.logout"); - Route::get('thong-tin-tai-khoan', 'Information')->name('user.information'); - Route::put('thong-tin-tai-khoan', 'StoreInformation')->name('user.information.store'); - Route::get('lich-su-email', 'Email')->name('user.email'); - Route::get('chi-tiet-email', 'ViewEmail')->name('user.email.view'); - Route::get('doi-mat-khau', 'ChangePassword')->name('user.password'); - Route::put('doi-mat-khau', 'StoreChangePassword')->name('user.password.store'); - }); - Route::controller(TicketController::class)->group(function () { - Route::get('danh-sach-yeu-cau-support', 'index')->name('ticket'); - Route::get('gui-yeu-cau-support', 'create')->name('ticket.create'); - Route::post('gui-yeu-cau-support', 'store')->name('ticket.store'); - Route::get('xem-yeu-cau-support', 'show')->name('ticket.show'); - Route::post('reply-yeu-cau-support', 'ReplyStore')->name('ticket.reply.store'); - Route::put('dong-yeu-cau-support', 'ClosedTicket')->name('ticket.closed'); - }); - Route::controller(CustomerInvoice::class)->group(function () { - Route::post('thanh-toan-hoa-don/{order_id}/{order_num}', 'store')->name('client.invoice.store'); - Route::get('danh-sach-hoa-don', 'index')->name('invoice.index'); - Route::get('chi-tiet-hoa-don/{invoice_id}/{order_num}', 'show')->name('invoice.show'); - }); - Route::controller(ShopController::class)->group(function () { - Route::get('danh-sach-dich-vu', 'index')->name('shop'); - Route::get('dich-vu/{slug}', 'show')->name('shop.show'); - Route::get('product/{slug}', 'getProduct')->name('shop.product'); - Route::post('them-vao-gio-hang', 'store')->name('shop.store'); - }); - - Route::controller(ClientOrderController::class)->group(function () { - Route::post('them-order', 'store')->name('client.order.store'); - Route::get('don-hang/{order_id}/thanh-toan', 'payment')->name('client.order.payment'); - Route::delete('don-hang/{order_id}', 'destroy')->name('client.order.destroy'); - - Route::get('danh-sach-don-hang', 'index')->name('order.index'); - }); - - - - Route::controller(CartController::class)->group(function () { - Route::post('them-gio-hang', 'addCart')->name('cart.add'); - Route::get('gio-hang', 'index')->name('cart.index'); - Route::post('gio-hang', 'order')->name('cart.store'); - Route::post('add-coupon', 'applyCoupon')->name('cart.applyCoupon'); - Route::get('remove-item-gio-hang', 'destroy')->name('cart.remove'); - Route::post('order-addon', 'addon')->name('cart.addon'); - }); - Route::controller(ManagerService::class)->group(function () { - Route::get('dich-vu-cua-toi', 'index')->name('service.index'); - Route::get('chi-tiet-dich-vu', 'show')->name('service.show'); - Route::post('reinstall-vmware', 'reinstall')->name('service.reinstall'); - Route::post('task-action', 'action')->name('service.action'); - Route::post('task-snapshot', 'snapshot')->name('service.snapshot'); - Route::get('view-console', 'console')->name('service.console'); - }); - - // Service Controller routes - Route::prefix('dich-vu-dang-ky')->controller(ServiceController::class)->group(function () { - Route::get('{invoice_ordernum}', 'show')->name('service.detail'); - Route::post('{invoice_ordernum}/vm/toggle', 'toggleVM')->name('service.vm.toggle'); - Route::get('{invoice_ordernum}/vm/info', 'getVMInfo')->name('service.vm.info'); - Route::get('{invoice_ordernum}/vmware-demo', 'vmwareDemo')->name('service.vmware.demo'); - Route::get('{invoice_ordernum}/vm-console', 'vmConsole')->name('service.vm.console'); - - Route::post('secure/create-snapshots', 'createSnapshots')->name('service.create.snapshots'); - Route::get('secure/get-snapshots', 'getSnapshots')->name('service.get.snapshots'); - Route::post('secure/remove-snapshot', 'removeSnapshot')->name('service.remove.snapshot'); - Route::post('secure/revert-snapshot', 'revertSnapshot')->name('service.revert.snapshot'); - Route::post('secure/reinstall-vm', 'reinstallVM')->name('service.reinstall.vm'); - Route::post('secure/reset-vm-password', 'resetVMPassword')->name('service.reset.vm.password'); - Route::post('secure/change-remote-port', 'changeRemotePort')->name('service.change.remote.port'); - Route::post('secure/change-ip', 'changeIP')->name('service.change.ip'); - Route::post('secure/renew-price', 'renewPrice')->name('service.renew.price'); - Route::post('secure/renew', 'renew')->name('service.renew'); - Route::post('secure/start-vm', 'startVM')->name('service.start.vm'); - Route::post('secure/stop-vm', 'stopVM')->name('service.stop.vm'); - Route::post('secure/reboot-vm', 'rebootVM')->name('service.reboot.vm'); - }); - - Route::controller(CustomerDomainController::class)->group(function () { - Route::get('danh-sach-ten-mien', 'index')->name('customer.domain.index'); - }); - Route::controller(RechargeController::class)->group(function () { - Route::get('nap-tien', 'index')->name('recharge.index'); - Route::post('nap-tien', 'paymentProcess')->name('recharge.paymentProcess.vnPay'); - Route::get('nap-tien/return', 'paymentReturn')->name('recharge.return'); - Route::get('thanh-toan', 'show')->name('recharge.show'); - Route::get('lich-su-giao-dich', 'HistoryRecharge')->name('recharge.history'); - Route::get('callback-function', 'CallbackResponse')->name('recharge.CallbackResponse'); - }); - Route::controller(CustomerPostController::class)->group(function () { - Route::get('tin-tuc/{slug}', 'view')->name('post'); - }); - - Route::controller(\App\Http\Controllers\frontend\Whois\WhoisController::class)->group(function () { - Route::get('kiem-tra-ten-mien', 'index')->name('whois.index'); - Route::post('api/whois/domain-info', 'getDomainInfo')->name('whois.domain-info'); - Route::post('api/whois/check-availability', 'checkAvailability')->name('whois.check-availability'); - }); -}); diff --git a/routes/web.php b/routes/web.php index 06aa5ba..8800b89 100644 --- a/routes/web.php +++ b/routes/web.php @@ -20,7 +20,4 @@ Route::domain('admin.tgs.com.vn.test')->group(function () { include __DIR__ . '/admin.php'; }); -Route::domain('portal.tgs.com.vn.test')->group(function () { - include __DIR__ . '/customer.php'; -});