diff --git a/.env.example b/.env.example index c67840d..88c1cfa 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index 88fcbf0..be7bd1e 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -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()), + ], + ]); } /** diff --git a/app/Http/Controllers/Api/Transaction/TransactionController.php b/app/Http/Controllers/Api/Transaction/TransactionController.php index a4cdbfd..21fbf28 100644 --- a/app/Http/Controllers/Api/Transaction/TransactionController.php +++ b/app/Http/Controllers/Api/Transaction/TransactionController.php @@ -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', + ], + ], + ]); + } } \ No newline at end of file diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 903c62f..275ac3f 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -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', diff --git a/config/vnpay.php b/config/vnpay.php index 2dbebf1..edfbb51 100644 --- a/config/vnpay.php +++ b/config/vnpay.php @@ -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'), ]; diff --git a/database/migrations/2024_05_13_165053_transaction.php b/database/migrations/2024_05_13_165053_transaction.php index 89d68ce..f45a88d 100644 --- a/database/migrations/2024_05_13_165053_transaction.php +++ b/database/migrations/2024_05_13_165053_transaction.php @@ -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(); diff --git a/database/migrations/2026_02_19_000000_add_uuid_to_transactions_table.php b/database/migrations/2026_02_19_000000_add_uuid_to_transactions_table.php new file mode 100644 index 0000000..eec5b4d --- /dev/null +++ b/database/migrations/2026_02_19_000000_add_uuid_to_transactions_table.php @@ -0,0 +1,22 @@ +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']); + }); + } +}; diff --git a/routes/client.php b/routes/client.php index e5466b4..9fe7d41 100644 --- a/routes/client.php +++ b/routes/client.php @@ -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']); +