feat: implement deposit request validation and transaction API, refactor client routes, and update transaction schema.

This commit is contained in:
pbao9
2026-02-18 21:17:13 +07:00
parent 31cbdf323f
commit 9a2cab4521
12 changed files with 371 additions and 175 deletions
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Enums;
enum TransactionStatus: int
{
case PENDING = 0;
case COMPLETED = 1;
case FAILED = 2;
public function label(): string
{
return match ($this) {
self::PENDING => '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(),
];
}
}
@@ -0,0 +1,209 @@
<?php
namespace App\Http\Controllers\Api\Transaction;
use App\Enums\TransactionStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\DepositRequest;
use App\Http\Services\VNPayService;
use App\Models\Customer;
use App\Models\Log;
use App\Models\Transaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TransactionController extends Controller
{
protected VNPayService $vnpayService;
public function __construct(VNPayService $vnpayService)
{
$this->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,
]);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DepositRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'amount' => '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.',
];
}
}
+21 -16
View File
@@ -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;
}
+9 -2
View File
@@ -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');
}
}
@@ -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');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::create('transactions', function (Blueprint $table) {
$table->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']);
});
}
};
@@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::table('transactions', function (Blueprint $table) {
$table->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');
});
}
};
@@ -184,6 +184,7 @@
</div>
</div>
</li>
@auth
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="w-30px h-30px ms-1 rounded-circle"
@@ -230,6 +231,7 @@
</ul>
</div>
</li>
@endauth
</ul>
<a href="#" class="sidebar-toggler">
+13 -1
View File
@@ -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');
});
});
});
-136
View File
@@ -1,136 +0,0 @@
<?php
use App\Http\Controllers\frontend\CartController;
use App\Http\Controllers\frontend\CustomerLogController;
use App\Http\Controllers\frontend\DomainController as CustomerDomainController;
use App\Http\Controllers\frontend\InvoiceController as CustomerInvoice;
use App\Http\Controllers\frontend\MainController;
use App\Http\Controllers\frontend\ManagerService;
use App\Http\Controllers\frontend\PostController as CustomerPostController;
use App\Http\Controllers\frontend\RechargeController;
use App\Http\Controllers\frontend\ServiceController;
use App\Http\Controllers\frontend\ShopController;
use App\Http\Controllers\frontend\TicketController;
use App\Http\Controllers\frontend\Order\OrderController as ClientOrderController;
use App\Http\Controllers\frontend\UserController as ClientController;
use Illuminate\Support\Facades\Route;
Route::middleware("IsLoginCheck")
->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');
});
});
-3
View File
@@ -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';
});