diff --git a/app/Http/Controllers/pages/BookingController.php b/app/Http/Controllers/pages/BookingController.php index 371d0ec..91808a2 100644 --- a/app/Http/Controllers/pages/BookingController.php +++ b/app/Http/Controllers/pages/BookingController.php @@ -5,11 +5,13 @@ use App\Models\Booking; use App\Models\Table; +use App\Models\PendingBooking; use App\Services\MidtransService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Carbon\Carbon; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Session; class BookingController extends Controller { @@ -20,7 +22,7 @@ public function __construct(MidtransService $midtransService) $this->midtransService = $midtransService; } - public function store(Request $request) { + public function createPaymentIntent(Request $request) { try { $request->validate([ 'table_id' => 'required|exists:tables,id', @@ -28,7 +30,7 @@ public function store(Request $request) { 'end_time' => 'required|date|after:start_time', ]); - // Cek apakah meja sedang dibooking pada waktu tersebut + // Cek apakah meja sedang dibooking pada waktu tersebut (hanya yang sudah paid) $conflict = Booking::where('table_id', $request->table_id) ->where(function($query) use ($request) { $query->whereBetween('start_time', [$request->start_time, $request->end_time]) @@ -37,8 +39,7 @@ public function store(Request $request) { ->where('end_time', '>', $request->start_time); }); }) - ->where('status', '!=', 'cancelled') - ->where('status', '!=', 'expired') + ->where('status', 'paid') // Hanya cek yang sudah paid ->exists(); if ($conflict) { @@ -52,47 +53,143 @@ public function store(Request $request) { $duration = $endTime->diffInHours($startTime); $totalAmount = $duration * $table->price_per_hour; - // Buat booking dengan status pending - $booking = Booking::create([ + // Simpan data booking sementara di session untuk digunakan setelah pembayaran + Session::put('temp_booking', [ 'table_id' => $request->table_id, 'user_id' => Auth::id(), 'start_time' => $request->start_time, 'end_time' => $request->end_time, - 'status' => 'pending', 'total_amount' => $totalAmount, - 'payment_expired_at' => now()->addHours(24), // Expired dalam 24 jam + 'created_at' => now(), ]); - // Dapatkan snap token dari Midtrans - $snapToken = $this->midtransService->createTransaction($booking); + // Generate unique order ID + $tempOrderId = 'TEMP-' . Auth::id() . '-' . time(); + Session::put('temp_order_id', $tempOrderId); + + // Simpan booking sementara ke database untuk bisa dilanjutkan nanti + PendingBooking::updateOrCreate( + [ + 'user_id' => Auth::id(), + 'table_id' => $request->table_id, + 'start_time' => $request->start_time + ], + [ + 'end_time' => $request->end_time, + 'total_amount' => $totalAmount, + 'order_id' => $tempOrderId, + 'expired_at' => now()->addHours(24), // Kadaluarsa dalam 24 jam + ] + ); + + // Dapatkan snap token dari Midtrans tanpa menyimpan booking + $snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user()); if (!$snapToken) { throw new \Exception('Failed to get snap token from Midtrans'); } - \Log::info('Booking created successfully:', [ - 'booking_id' => $booking->id, + \Log::info('Payment intent created successfully:', [ + 'order_id' => $tempOrderId, 'snap_token' => $snapToken ]); return response()->json([ - 'message' => 'Booking berhasil dibuat, silahkan lakukan pembayaran', - 'booking_id' => $booking->id, + 'message' => 'Payment intent created, proceed to payment', 'total_amount' => $totalAmount, - 'snap_token' => $snapToken + 'snap_token' => $snapToken, + 'order_id' => $tempOrderId ]); } catch (\Exception $e) { - \Log::error('Booking error:', [ + \Log::error('Payment intent error:', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return response()->json([ + 'message' => 'Gagal membuat transaksi: ' . $e->getMessage() + ], 500); + } + } + + public function store(Request $request) { + try { + $request->validate([ + 'order_id' => 'required|string', + 'transaction_id' => 'required|string', + 'payment_method' => 'required|string', + 'transaction_status' => 'required|string', + ]); + + // Retrieve booking data from session + $tempBooking = Session::get('temp_booking'); + $tempOrderId = Session::get('temp_order_id'); + + // If not in session, try from pending bookings + if (!$tempBooking || $tempOrderId != $request->order_id) { + $pendingBooking = PendingBooking::where('order_id', $request->order_id) + ->where('user_id', Auth::id()) + ->first(); + + if (!$pendingBooking) { + throw new \Exception('Invalid or expired booking session'); + } + + $tempBooking = [ + 'table_id' => $pendingBooking->table_id, + 'user_id' => $pendingBooking->user_id, + 'start_time' => $pendingBooking->start_time, + 'end_time' => $pendingBooking->end_time, + 'total_amount' => $pendingBooking->total_amount, + ]; + $tempOrderId = $pendingBooking->order_id; + } + + // Process based on transaction status + if ($request->transaction_status == 'settlement' || $request->transaction_status == 'capture') { + // Create the actual booking record + $booking = Booking::create([ + 'table_id' => $tempBooking['table_id'], + 'user_id' => $tempBooking['user_id'], + 'start_time' => $tempBooking['start_time'], + 'end_time' => $tempBooking['end_time'], + 'status' => 'paid', + 'total_amount' => $tempBooking['total_amount'], + 'payment_id' => $request->transaction_id, + 'payment_method' => $request->payment_method, + 'order_id' => $request->order_id, + ]); + + // Update table status to booked + $table = Table::findOrFail($tempBooking['table_id']); + $table->update(['status' => 'Booked']); + + // Delete pending booking if exists + PendingBooking::where('order_id', $request->order_id)->delete(); + + // Clear session data + Session::forget('temp_booking'); + Session::forget('temp_order_id'); + + return response()->json([ + 'message' => 'Booking created successfully', + 'booking_id' => $booking->id + ]); + } else { + // For pending, deny, cancel, etc. - don't create booking + return response()->json([ + 'message' => 'Payment ' . $request->transaction_status . ', booking not created', + 'status' => $request->transaction_status + ]); + } + } catch (\Exception $e) { + \Log::error('Booking store error:', [ 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); - if (isset($booking)) { - $booking->delete(); // Hapus booking jika gagal membuat transaksi - } - return response()->json([ - 'message' => 'Gagal membuat transaksi: ' . $e->getMessage() + 'message' => 'Failed to create booking: ' . $e->getMessage() ], 500); } } @@ -103,10 +200,10 @@ public function getBookedSchedules(Request $request) { 'date' => 'required|date', ]); + // Only get bookings with paid status $bookings = Booking::where('table_id', $request->table_id) ->whereDate('start_time', $request->date) - ->where('status', '!=', 'cancelled') - ->where('status', '!=', 'expired') + ->where('status', 'paid') // Only include paid bookings ->select('start_time', 'end_time') ->get() ->map(function ($booking) { @@ -127,9 +224,22 @@ public function handleNotification(Request $request) $transactionStatus = $notification['transaction_status']; $orderId = $notification['order_id']; - $fraudStatus = $notification['fraud_status']; + $fraudStatus = $notification['fraud_status'] ?? null; + $transactionId = $notification['transaction_id']; + $paymentType = $notification['payment_type']; - // Get booking from order_id + // Check if this is a temporary order (from our new flow) + if (strpos($orderId, 'TEMP-') === 0) { + // This is a notification for a transaction that started with our new flow + // We don't need to do anything here as the frontend will handle creating the booking + // after successful payment via the store method + Log::info('Received notification for temp order, will be handled by frontend', [ + 'order_id' => $orderId + ]); + return response()->json(['message' => 'Notification received for temp order']); + } + + // Handle notifications for existing bookings (from old flow or admin-created bookings) $booking = Booking::where('order_id', $orderId)->first(); if (!$booking) { Log::error('Booking not found for order_id: ' . $orderId); @@ -163,7 +273,10 @@ public function handleNotification(Request $request) $booking->status = 'pending'; } + $booking->payment_id = $transactionId; + $booking->payment_method = $paymentType; $booking->save(); + Log::info('Booking status updated:', ['booking_id' => $booking->id, 'status' => $booking->status]); return response()->json(['message' => 'Notification processed successfully']); @@ -172,4 +285,114 @@ public function handleNotification(Request $request) return response()->json(['message' => 'Error processing notification'], 500); } } -} + + public function getPendingBookings() + { + $pendingBookings = PendingBooking::where('user_id', Auth::id()) + ->where('expired_at', '>', now()) + ->with(['table.venue']) + ->get(); + + return response()->json($pendingBookings); + } + + public function resumeBooking($id) + { + try { + $pendingBooking = PendingBooking::where('id', $id) + ->where('user_id', Auth::id()) + ->where('expired_at', '>', now()) + ->firstOrFail(); + + // Cek apakah meja masih available di waktu tersebut + $conflict = Booking::where('table_id', $pendingBooking->table_id) + ->where(function($query) use ($pendingBooking) { + $query->whereBetween('start_time', [$pendingBooking->start_time, $pendingBooking->end_time]) + ->orWhere(function($query) use ($pendingBooking) { + $query->where('start_time', '<', $pendingBooking->start_time) + ->where('end_time', '>', $pendingBooking->start_time); + }); + }) + ->where('status', 'paid') + ->exists(); + + if ($conflict) { + return response()->json([ + 'success' => false, + 'message' => 'Meja ini sudah tidak tersedia pada waktu yang Anda pilih' + ], 409); + } + + // Simpan ke session + Session::put('temp_booking', [ + 'table_id' => $pendingBooking->table_id, + 'user_id' => Auth::id(), + 'start_time' => $pendingBooking->start_time, + 'end_time' => $pendingBooking->end_time, + 'total_amount' => $pendingBooking->total_amount, + 'created_at' => now(), + ]); + Session::put('temp_order_id', $pendingBooking->order_id); + + // Dapatkan table data + $table = Table::findOrFail($pendingBooking->table_id); + + // Dapatkan snap token baru dari Midtrans + $snapToken = $this->midtransService->createTemporaryTransaction( + $table, + $pendingBooking->total_amount, + $pendingBooking->order_id, + Auth::user() + ); + + if (!$snapToken) { + throw new \Exception('Failed to get snap token from Midtrans'); + } + + return response()->json([ + 'success' => true, + 'message' => 'Booking dapat dilanjutkan', + 'snap_token' => $snapToken, + 'order_id' => $pendingBooking->order_id, + 'venue_id' => $pendingBooking->table->venue_id, + 'table_id' => $pendingBooking->table_id, + 'table_name' => $pendingBooking->table->name, + 'start_time' => Carbon::parse($pendingBooking->start_time)->format('H:i'), + 'duration' => Carbon::parse($pendingBooking->start_time)->diffInHours($pendingBooking->end_time), + 'total_amount' => $pendingBooking->total_amount + ]); + + } catch (\Exception $e) { + Log::error('Resume booking error:', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Gagal memproses pembayaran: ' . $e->getMessage() + ], 500); + } + } + + public function deletePendingBooking($id) + { + try { + $pendingBooking = PendingBooking::where('id', $id) + ->where('user_id', Auth::id()) + ->firstOrFail(); + + $pendingBooking->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Booking berhasil dihapus' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghapus booking: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/pages/BookingHistoryController.php b/app/Http/Controllers/pages/BookingHistoryController.php new file mode 100644 index 0000000..dcec0b8 --- /dev/null +++ b/app/Http/Controllers/pages/BookingHistoryController.php @@ -0,0 +1,20 @@ +with(['table.venue']) + ->orderBy('start_time', 'desc') + ->paginate(10); + + return view('pages.booking-history', compact('bookings')); + } +} \ No newline at end of file diff --git a/app/Models/PendingBooking.php b/app/Models/PendingBooking.php new file mode 100644 index 0000000..546388f --- /dev/null +++ b/app/Models/PendingBooking.php @@ -0,0 +1,37 @@ + 'datetime', + 'end_time' => 'datetime', + 'expired_at' => 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + public function table() + { + return $this->belongsTo(Table::class); + } +} \ No newline at end of file diff --git a/app/Services/MidtransService.php b/app/Services/MidtransService.php index bc9ac3f..5ed9f6f 100644 --- a/app/Services/MidtransService.php +++ b/app/Services/MidtransService.php @@ -3,6 +3,8 @@ namespace App\Services; use App\Models\Booking; +use App\Models\Table; +use App\Models\User; use Midtrans\Config; use Midtrans\Snap; use Illuminate\Support\Facades\Log; @@ -42,6 +44,67 @@ public function __construct() ]); } + // Method baru untuk membuat transaksi sementara tanpa booking record + public function createTemporaryTransaction(Table $table, int $amount, string $orderId, User $user) + { + try { + if ($amount <= 0) { + throw new \Exception('Invalid booking amount'); + } + + $params = [ + 'transaction_details' => [ + 'order_id' => $orderId, + 'gross_amount' => (int) $amount, + ], + 'customer_details' => [ + 'first_name' => $user->name, + 'email' => $user->email, + ], + 'item_details' => [ + [ + 'id' => $table->id, + 'price' => (int) $amount, + 'quantity' => 1, + 'name' => 'Booking Meja ' . $table->name, + ], + ], + 'expiry' => [ + 'start_time' => now()->format('Y-m-d H:i:s O'), + 'unit' => 'hour', + 'duration' => 24, + ], + ]; + + Log::info('Creating Midtrans temporary transaction:', [ + 'order_id' => $orderId, + 'amount' => $amount, + 'params' => $params + ]); + + $snapToken = Snap::getSnapToken($params); + + if (empty($snapToken)) { + throw new \Exception('Empty snap token received from Midtrans'); + } + + Log::info('Midtrans temporary transaction created successfully:', [ + 'order_id' => $orderId, + 'snap_token' => $snapToken + ]); + + return $snapToken; + } catch (\Exception $e) { + Log::error('Midtrans temporary transaction failed:', [ + 'order_id' => $orderId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw new \Exception('Failed to create Midtrans transaction: ' . $e->getMessage()); + } + } + + // Metode original untuk backward compatibility public function createTransaction(Booking $booking) { try { @@ -120,6 +183,15 @@ public function handleNotification($notification) 'fraud_status' => $fraud ]); + // Check if this is a temporary order + if (strpos($orderId, 'TEMP-') === 0) { + Log::info('Notification for temporary order received, will be handled separately', [ + 'order_id' => $orderId + ]); + return null; + } + + // Handle existing bookings // Extract booking ID from order ID (format: BOOK-{id}) $bookingId = explode('-', $orderId)[1]; $booking = Booking::findOrFail($bookingId); @@ -162,4 +234,4 @@ public function handleNotification($notification) throw $e; } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/database/migrations/2025_05_08_163931_create_pending_bookings_table.php b/database/migrations/2025_05_08_163931_create_pending_bookings_table.php new file mode 100644 index 0000000..b5c6a51 --- /dev/null +++ b/database/migrations/2025_05_08_163931_create_pending_bookings_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('table_id')->constrained()->onDelete('cascade'); + $table->dateTime('start_time'); + $table->dateTime('end_time'); + $table->decimal('total_amount', 10, 2); + $table->string('order_id')->unique(); + $table->dateTime('expired_at'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pending_bookings'); + } +}; \ No newline at end of file diff --git a/resources/views/layouts/main.blade.php b/resources/views/layouts/main.blade.php index ff2e721..e1cccd8 100644 --- a/resources/views/layouts/main.blade.php +++ b/resources/views/layouts/main.blade.php @@ -39,6 +39,9 @@ class="block lg:hidden border-l pl-4 border-gray-300 focus:outline-none">