feat: Implement VNPAY callback redirection to frontend and add UUID/public code to transactions.

This commit is contained in:
pbao9
2026-02-19 17:01:49 +07:00
parent c7215ababc
commit c720e62bda
8 changed files with 118 additions and 74 deletions
+2 -1
View File
@@ -75,9 +75,10 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
VNPAY_TMN_CODE=BU4VVGV5
VNPAY_HASH_SECRET=ISDSSDEGJ8EKDHXJVTVMD919DAFBF96I
VNPAY_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
VNPAY_RETURN_URL=https://dev.tgs.com.vn/nap-tien/return
VNPAY_RETURN_URL=https://api-agency.test/v1/transaction/return
VNPAY_API_URL=http://sandbox.vnpayment.vn/merchant_webapi/merchant.html
VNPAY_TRANSACTION_API=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
FRONTEND_URL=https://portal.test
@@ -81,7 +81,12 @@ class ClientController extends Controller
*/
public function me()
{
return new ClientResource(auth('client')->user());
return response()->json([
'error' => false,
'data' => [
'client' => new ClientResource(auth('client')->user()),
],
]);
}
/**
@@ -12,6 +12,7 @@ use App\Models\Transaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log as FacadesLog;
use Illuminate\Support\Str;
class TransactionController extends Controller
{
@@ -26,8 +27,12 @@ class TransactionController extends Controller
{
$user = $request->user();
$transRef = time();
$uuid = Str::uuid();
$publicCode = 'DEP-' . now()->format('Ymd') . '-' . strtoupper(Str::random(6));
$transaction = Transaction::create([
'uuid' => $uuid,
'public_code' => $publicCode,
'trans' => 'DEP' . $transRef,
'amount' => $request->amount,
'customer_id' => $user->id,
@@ -37,6 +42,7 @@ class TransactionController extends Controller
$paymentUrl = $this->vnpayService->createPaymentUrl([
'amount' => $request->amount,
'transaction_ref' => $transRef,
'transaction_uuid' => $uuid,
]);
return response()->json([
@@ -44,11 +50,27 @@ class TransactionController extends Controller
'message' => 'Tạo yêu cầu nạp tiền thành công',
'data' => [
'transaction_id' => $transaction->trans,
'transaction_uuid' => $uuid,
'transaction_code' => $publicCode,
'payment_url' => $paymentUrl,
],
]);
}
protected function redirectToFrontend(string $status = 'failed', ?string $transactionCode = null): \Illuminate\Http\RedirectResponse
{
$frontendUrl = config('vnpay.frontend_url', 'http://localhost:3000');
$url = $frontendUrl . '/nap-tien/return';
$params = ['status' => $status];
if ($transactionCode) {
$params['code'] = $transactionCode;
}
return redirect()->away($url . '?' . http_build_query($params));
}
public function returnCallback(Request $request)
{
$data = $request->all();
@@ -65,44 +87,11 @@ class TransactionController extends Controller
$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);
return $this->redirectToFrontend('failed');
}
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);
return $this->redirectToFrontend('success', $transaction->public_code);
}
$verify = $this->vnpayService->verifyPayment($data);
@@ -114,34 +103,18 @@ class TransactionController extends Controller
]);
if (!$verify['isValid']) {
return response()->json([
'error' => true,
'message' => 'Dữ liệu không hợp lệ',
], 400);
return $this->redirectToFrontend('failed', $transaction->public_code);
}
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);
return $this->redirectToFrontend('failed', $transaction->public_code);
}
$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);
return $this->redirectToFrontend('failed', $transaction->public_code);
}
try {
@@ -178,23 +151,11 @@ class TransactionController extends Controller
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => true,
'message' => 'Lỗi xử lý giao dịch: ' . $e->getMessage(),
], 500);
FacadesLog::error('Payment processing error: ' . $e->getMessage());
return $this->redirectToFrontend('failed', $transaction->public_code);
}
return response()->json([
'error' => false,
'message' => 'Thanh toán thành công',
'data' => [
'transaction' => $transaction->fresh(),
'customer' => [
'total' => $customer->total,
'credit' => $customer->credit,
],
],
]);
return $this->redirectToFrontend('success', $transaction->public_code);
}
public function getTransactions(Request $request)
@@ -268,4 +229,53 @@ class TransactionController extends Controller
],
]);
}
public function getByPublicCode(Request $request)
{
$code = $request->query('code');
if (!$code) {
return response()->json([
'error' => true,
'message' => 'Thiếu mã giao dịch',
], 400);
}
$transaction = Transaction::where('public_code', $code)->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 thông tin giao dịch thành công',
'data' => [
'transaction' => [
'public_code' => $transaction->public_code,
'amount' => $transaction->amount,
'amount_formatted' => number_format($transaction->amount) . ' VNĐ',
'status' => $transaction->status,
'status_label' => TransactionStatus::from($transaction->status)->label(),
'created_at' => $transaction->created_at->format('Y-m-d H:i:s'),
'paid_at' => $transaction->updated_at->format('Y-m-d H:i:s'),
],
'customer' => [
'fullname' => $transaction->customer->fullname,
'email' => $transaction->customer->email,
'phone' => $transaction->customer->phone,
'address' => $transaction->customer->address,
],
'balance' => [
'total' => $transaction->customer->total,
'credit' => $transaction->customer->credit,
'total_formatted' => number_format($transaction->customer->total) . ' VNĐ',
'credit_formatted' => number_format($transaction->customer->credit) . ' điểm',
],
],
]);
}
}
+4 -1
View File
@@ -5,13 +5,16 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\HasUuid;
class Transaction extends Model
{
use HasFactory, SoftDeletes;
use HasFactory, SoftDeletes, HasUuid;
protected $table = 'transactions';
protected $fillable = [
'uuid',
'public_code',
'trans',
'amount',
'customer_id',
+1
View File
@@ -7,4 +7,5 @@ return [
'return_url' => env('VNPAY_RETURN_URL', 'http://localhost/thanh-toan/vnpay-return'),
'api_url' => env('VNPAY_API_URL', 'http://sandbox.vnpayment.vn/merchant_webapi/merchant.html'),
'transaction_api' => env('VNPAY_TRANSACTION_API', 'https://sandbox.vnpayment.vn/merchant_webapi/api/transaction'),
'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'),
];
@@ -9,6 +9,7 @@ return new class extends Migration {
{
Schema::create('transactions', function (Blueprint $table) {
$table->id();
$table->uuid();
$table->string('trans')->nullable();
$table->integer('amount')->nullable();
@@ -0,0 +1,22 @@
<?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->uuid('uuid')->unique()->nullable()->after('id');
$table->string('public_code')->unique()->nullable()->after('uuid');
});
}
public function down()
{
Schema::table('transactions', function (Blueprint $table) {
$table->dropColumn(['uuid', 'public_code']);
});
}
};
+3 -2
View File
@@ -60,13 +60,14 @@ Route::prefix('v1')->as('api.')->group(function () {
Route::get('get-list', 'getTransactions')->name('getList');
Route::post('pay', 'handle')->name('pay');
Route::get('verify', 'verifyPayment')->name('verify');
Route::get('return', 'returnCallback')->name('return');
Route::get('{transaction:slug}', 'getDetailTransaction')->name('detail');
Route::get('detail-by-code', 'getByPublicCode')->name('detailByCode');
});
});
});
Route::get('v1/payment/vnpay/return', [TransactionController::class, 'returnCallback']);