From 4e1dfd6f245747fa55b14ceda9df53ed372c1fb7 Mon Sep 17 00:00:00 2001 From: Stephen Gesityan Date: Thu, 8 May 2025 15:37:35 +0700 Subject: [PATCH] Midtrans Berhasil --- .env.example | 2 +- .../Controllers/pages/BookingController.php | 163 +++++-- app/Models/Booking.php | 39 +- app/Models/Table.php | 12 +- app/Providers/AppServiceProvider.php | 5 +- app/Services/MidtransService.php | 165 +++++++ composer.json | 4 +- composer.lock | 455 +++++++++++++++++- config/midtrans.php | 12 + ..._add_payment_columns_to_bookings_table.php | 39 ++ ...029_add_price_per_hour_to_tables_table.php | 28 ++ database/seeders/VenueSeeder.php | 6 +- resources/views/pages/venue.blade.php | 55 ++- routes/web.php | 3 + 14 files changed, 935 insertions(+), 53 deletions(-) create mode 100644 app/Services/MidtransService.php create mode 100644 config/midtrans.php create mode 100644 database/migrations/2025_05_07_191310_add_payment_columns_to_bookings_table.php create mode 100644 database/migrations/2025_05_07_193029_add_price_per_hour_to_tables_table.php diff --git a/.env.example b/.env.example index ea0665b..ed84566 100644 --- a/.env.example +++ b/.env.example @@ -56,4 +56,4 @@ VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" -VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" \ No newline at end of file diff --git a/app/Http/Controllers/pages/BookingController.php b/app/Http/Controllers/pages/BookingController.php index f973fec..371d0ec 100644 --- a/app/Http/Controllers/pages/BookingController.php +++ b/app/Http/Controllers/pages/BookingController.php @@ -5,44 +5,96 @@ use App\Models\Booking; use App\Models\Table; +use App\Services\MidtransService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Carbon\Carbon; +use Illuminate\Support\Facades\Log; class BookingController extends Controller { + protected $midtransService; + + public function __construct(MidtransService $midtransService) + { + $this->midtransService = $midtransService; + } + public function store(Request $request) { - $request->validate([ - 'table_id' => 'required|exists:tables,id', - 'start_time' => 'required|date', - 'end_time' => 'required|date|after:start_time', - ]); + try { + $request->validate([ + 'table_id' => 'required|exists:tables,id', + 'start_time' => 'required|date', + 'end_time' => 'required|date|after:start_time', + ]); - // Cek apakah meja sedang dibooking pada waktu tersebut - $conflict = Booking::where('table_id', $request->table_id) - ->where(function($query) use ($request) { - $query->whereBetween('start_time', [$request->start_time, $request->end_time]) - ->orWhere(function($query) use ($request) { - $query->where('start_time', '<', $request->start_time) - ->where('end_time', '>', $request->start_time); - }); - }) - ->where('status', '!=', 'cancelled') // skip booking yang dibatalkan - ->exists(); + // Cek apakah meja sedang dibooking pada waktu tersebut + $conflict = Booking::where('table_id', $request->table_id) + ->where(function($query) use ($request) { + $query->whereBetween('start_time', [$request->start_time, $request->end_time]) + ->orWhere(function($query) use ($request) { + $query->where('start_time', '<', $request->start_time) + ->where('end_time', '>', $request->start_time); + }); + }) + ->where('status', '!=', 'cancelled') + ->where('status', '!=', 'expired') + ->exists(); - if ($conflict) { - return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409); + if ($conflict) { + return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409); + } + + // Hitung total biaya + $table = Table::findOrFail($request->table_id); + $startTime = Carbon::parse($request->start_time); + $endTime = Carbon::parse($request->end_time); + $duration = $endTime->diffInHours($startTime); + $totalAmount = $duration * $table->price_per_hour; + + // Buat booking dengan status pending + $booking = Booking::create([ + '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 + ]); + + // Dapatkan snap token dari Midtrans + $snapToken = $this->midtransService->createTransaction($booking); + + if (!$snapToken) { + throw new \Exception('Failed to get snap token from Midtrans'); + } + + \Log::info('Booking created successfully:', [ + 'booking_id' => $booking->id, + 'snap_token' => $snapToken + ]); + + return response()->json([ + 'message' => 'Booking berhasil dibuat, silahkan lakukan pembayaran', + 'booking_id' => $booking->id, + 'total_amount' => $totalAmount, + 'snap_token' => $snapToken + ]); + } catch (\Exception $e) { + \Log::error('Booking 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() + ], 500); } - - Booking::create([ - 'table_id' => $request->table_id, - 'user_id' => Auth::id(), - 'start_time' => $request->start_time, - 'end_time' => $request->end_time, - 'status' => 'booked', - ]); - - return response()->json(['message' => 'Booking berhasil']); } public function getBookedSchedules(Request $request) { @@ -54,6 +106,7 @@ public function getBookedSchedules(Request $request) { $bookings = Booking::where('table_id', $request->table_id) ->whereDate('start_time', $request->date) ->where('status', '!=', 'cancelled') + ->where('status', '!=', 'expired') ->select('start_time', 'end_time') ->get() ->map(function ($booking) { @@ -65,4 +118,58 @@ public function getBookedSchedules(Request $request) { return response()->json($bookings); } + + public function handleNotification(Request $request) + { + try { + $notification = $request->all(); + Log::info('Midtrans notification received:', $notification); + + $transactionStatus = $notification['transaction_status']; + $orderId = $notification['order_id']; + $fraudStatus = $notification['fraud_status']; + + // Get booking from order_id + $booking = Booking::where('order_id', $orderId)->first(); + if (!$booking) { + Log::error('Booking not found for order_id: ' . $orderId); + return response()->json(['message' => 'Booking not found'], 404); + } + + // Update booking status based on transaction status + if ($transactionStatus == 'capture') { + if ($fraudStatus == 'challenge') { + $booking->status = 'challenge'; + } else if ($fraudStatus == 'accept') { + $booking->status = 'paid'; + // Update table status to booked + $booking->table->update(['status' => 'Booked']); + } + } else if ($transactionStatus == 'settlement') { + $booking->status = 'paid'; + // Update table status to booked + $booking->table->update(['status' => 'Booked']); + } else if ($transactionStatus == 'cancel' || $transactionStatus == 'deny' || $transactionStatus == 'expire') { + $booking->status = 'cancelled'; + // Reset table status to available if no other active bookings + $hasActiveBookings = $booking->table->bookings() + ->where('status', 'paid') + ->where('id', '!=', $booking->id) + ->exists(); + if (!$hasActiveBookings) { + $booking->table->update(['status' => 'Available']); + } + } else if ($transactionStatus == 'pending') { + $booking->status = 'pending'; + } + + $booking->save(); + Log::info('Booking status updated:', ['booking_id' => $booking->id, 'status' => $booking->status]); + + return response()->json(['message' => 'Notification processed successfully']); + } catch (\Exception $e) { + Log::error('Error processing Midtrans notification: ' . $e->getMessage()); + return response()->json(['message' => 'Error processing notification'], 500); + } + } } diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 40fea22..e4a6eb9 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -9,7 +9,24 @@ class Booking extends Model { use HasFactory; - protected $fillable = ['table_id', 'user_id', 'start_time', 'end_time', 'status']; + protected $fillable = [ + 'table_id', + 'user_id', + 'start_time', + 'end_time', + 'status', + 'payment_id', + 'payment_method', + 'total_amount', + 'payment_expired_at' + ]; + + protected $casts = [ + 'start_time' => 'datetime', + 'end_time' => 'datetime', + 'payment_expired_at' => 'datetime', + 'total_amount' => 'decimal:2' + ]; public function table() { @@ -20,4 +37,24 @@ public function user() { return $this->belongsTo(User::class); } + + public function isPending() + { + return $this->status === 'pending'; + } + + public function isPaid() + { + return $this->status === 'paid'; + } + + public function isExpired() + { + return $this->status === 'expired'; + } + + public function isCancelled() + { + return $this->status === 'cancelled'; + } } diff --git a/app/Models/Table.php b/app/Models/Table.php index c0a51a2..7b26f76 100644 --- a/app/Models/Table.php +++ b/app/Models/Table.php @@ -9,7 +9,17 @@ class Table extends Model { use HasFactory; - protected $fillable = ['venue_id', 'name', 'brand', 'status']; + protected $fillable = [ + 'name', + 'brand', + 'status', + 'venue_id', + 'price_per_hour' + ]; + + protected $casts = [ + 'price_per_hour' => 'decimal:2' + ]; public function venue() { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..8b0a605 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use App\Services\MidtransService; class AppServiceProvider extends ServiceProvider { @@ -11,7 +12,9 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(MidtransService::class, function ($app) { + return new MidtransService(); + }); } /** diff --git a/app/Services/MidtransService.php b/app/Services/MidtransService.php new file mode 100644 index 0000000..bc9ac3f --- /dev/null +++ b/app/Services/MidtransService.php @@ -0,0 +1,165 @@ + $serverKey, + 'client_key' => $clientKey, + 'merchant_id' => $merchantId, + 'is_production' => $isProduction + ]); + + if (empty($serverKey)) { + throw new \Exception('Midtrans server key is not configured'); + } + + Config::$serverKey = $serverKey; + Config::$isProduction = $isProduction; + Config::$isSanitized = true; + Config::$is3ds = true; + + // Log konfigurasi yang digunakan oleh Midtrans + Log::info('Midtrans Config:', [ + 'server_key' => Config::$serverKey, + 'is_production' => Config::$isProduction, + 'is_sanitized' => Config::$isSanitized, + 'is_3ds' => Config::$is3ds + ]); + } + + public function createTransaction(Booking $booking) + { + try { + if (!$booking->user) { + throw new \Exception('User not found for booking'); + } + + if (empty($booking->total_amount) || $booking->total_amount <= 0) { + throw new \Exception('Invalid booking amount'); + } + + $params = [ + 'transaction_details' => [ + 'order_id' => 'BOOK-' . $booking->id, + 'gross_amount' => (int) $booking->total_amount, + ], + 'customer_details' => [ + 'first_name' => $booking->user->name, + 'email' => $booking->user->email, + ], + 'item_details' => [ + [ + 'id' => $booking->table_id, + 'price' => (int) $booking->total_amount, + 'quantity' => 1, + 'name' => 'Booking Meja ' . $booking->table->name, + ], + ], + 'expiry' => [ + 'start_time' => now()->format('Y-m-d H:i:s O'), + 'unit' => 'hour', + 'duration' => 24, + ], + ]; + + Log::info('Creating Midtrans transaction:', [ + 'booking_id' => $booking->id, + 'amount' => $booking->total_amount, + 'params' => $params + ]); + + $snapToken = Snap::getSnapToken($params); + + if (empty($snapToken)) { + throw new \Exception('Empty snap token received from Midtrans'); + } + + Log::info('Midtrans transaction created successfully:', [ + 'booking_id' => $booking->id, + 'snap_token' => $snapToken + ]); + + return $snapToken; + } catch (\Exception $e) { + Log::error('Midtrans transaction failed:', [ + 'booking_id' => $booking->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw new \Exception('Failed to create Midtrans transaction: ' . $e->getMessage()); + } + } + + public function handleNotification($notification) + { + try { + $transaction = $notification->transaction_status; + $type = $notification->payment_type; + $orderId = $notification->order_id; + $fraud = $notification->fraud_status; + + Log::info('Received Midtrans notification:', [ + 'transaction_status' => $transaction, + 'payment_type' => $type, + 'order_id' => $orderId, + 'fraud_status' => $fraud + ]); + + // Extract booking ID from order ID (format: BOOK-{id}) + $bookingId = explode('-', $orderId)[1]; + $booking = Booking::findOrFail($bookingId); + + if ($transaction == 'capture') { + if ($type == 'credit_card') { + if ($fraud == 'challenge') { + $booking->status = 'pending'; + } else { + $booking->status = 'paid'; + } + } + } else if ($transaction == 'settlement') { + $booking->status = 'paid'; + } else if ($transaction == 'pending') { + $booking->status = 'pending'; + } else if ($transaction == 'deny') { + $booking->status = 'cancelled'; + } else if ($transaction == 'expire') { + $booking->status = 'expired'; + } else if ($transaction == 'cancel') { + $booking->status = 'cancelled'; + } + + $booking->payment_id = $notification->transaction_id; + $booking->payment_method = $type; + $booking->save(); + + Log::info('Booking status updated:', [ + 'booking_id' => $booking->id, + 'new_status' => $booking->status + ]); + + return $booking; + } catch (\Exception $e) { + Log::error('Failed to handle Midtrans notification:', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 9e6d206..d582c7e 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,13 @@ "license": "MIT", "require": { "php": "^8.1", + "doctrine/dbal": "^3.9", "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.10", "laravel/sanctum": "^3.3", "laravel/tinker": "^2.8", - "laravel/ui": "^4.6" + "laravel/ui": "^4.6", + "midtrans/midtrans-php": "^2.6" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index d811d50..9925425 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dcbbd52cf3c38d0f1b88e12d39e47db7", + "content-hash": "edf30558ed2abcf5bae02cf31ec6ee71", "packages": [ { "name": "brick/math", @@ -210,6 +210,349 @@ }, "time": "2024-07-08T12:26:09+00:00" }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.9.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", + "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.22", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.9.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-01-16T08:28:55+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.10", @@ -1947,6 +2290,67 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "midtrans/midtrans-php", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/Midtrans/midtrans-php.git", + "reference": "8ed7fc58ff1ababe675da17acf8233f4028eb3be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Midtrans/midtrans-php/zipball/8ed7fc58ff1ababe675da17acf8233f4028eb3be", + "reference": "8ed7fc58ff1ababe675da17acf8233f4028eb3be", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "5.7.*", + "psy/psysh": "0.4.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "SnapBi\\": "SnapBi/", + "Midtrans\\": "Midtrans/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andri Setiawan", + "email": "andri.setiawan@veritrans.co.id" + }, + { + "name": "Alvin Litani", + "email": "alvin.litani@veritrans.co.id" + }, + { + "name": "Ismail Faruqi", + "email": "ismail.faruqi@veritrans.co.id" + }, + { + "name": "Muhammad Fauzi Masykur", + "email": "muhammad.masykur@gojek.com" + } + ], + "description": "PHP Wrapper for Midtrans Payment API.", + "homepage": "https://midtrans.com", + "support": { + "issues": "https://github.com/Midtrans/midtrans-php/issues", + "source": "https://github.com/Midtrans/midtrans-php/tree/v2.6.2" + }, + "time": "2025-03-18T06:30:17+00:00" + }, { "name": "monolog/monolog", "version": "3.7.0", @@ -2522,6 +2926,55 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/config/midtrans.php b/config/midtrans.php new file mode 100644 index 0000000..023809b --- /dev/null +++ b/config/midtrans.php @@ -0,0 +1,12 @@ + env('MIDTRANS_MERCHANT_ID', ''), + 'client_key' => env('MIDTRANS_CLIENT_KEY', ''), + 'server_key' => env('MIDTRANS_SERVER_KEY', ''), + 'is_production' => env('MIDTRANS_IS_PRODUCTION', false), + 'snap_url' => env('MIDTRANS_IS_PRODUCTION', false) ? 'https://app.midtrans.com/snap/snap.js' : 'https://app.sandbox.midtrans.com/snap/snap.js', + 'notification_url' => env('APP_URL') . '/payment/notification', + 'expiry_duration' => 24, // Durasi expired dalam jam + 'expiry_unit' => 'hour', +]; \ No newline at end of file diff --git a/database/migrations/2025_05_07_191310_add_payment_columns_to_bookings_table.php b/database/migrations/2025_05_07_191310_add_payment_columns_to_bookings_table.php new file mode 100644 index 0000000..d1a1546 --- /dev/null +++ b/database/migrations/2025_05_07_191310_add_payment_columns_to_bookings_table.php @@ -0,0 +1,39 @@ +string('payment_id')->nullable(); + $table->string('payment_method')->nullable(); + $table->decimal('total_amount', 10, 2)->nullable(); + $table->timestamp('payment_expired_at')->nullable(); + + // Update status column if it exists + if (Schema::hasColumn('bookings', 'status')) { + $table->string('status')->default('pending')->change(); + } else { + $table->string('status')->default('pending'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('bookings', function (Blueprint $table) { + $table->dropColumn(['payment_id', 'payment_method', 'total_amount', 'payment_expired_at']); + // Don't drop status column as it might be used by other parts of the application + }); + } +}; diff --git a/database/migrations/2025_05_07_193029_add_price_per_hour_to_tables_table.php b/database/migrations/2025_05_07_193029_add_price_per_hour_to_tables_table.php new file mode 100644 index 0000000..ce737a0 --- /dev/null +++ b/database/migrations/2025_05_07_193029_add_price_per_hour_to_tables_table.php @@ -0,0 +1,28 @@ +decimal('price_per_hour', 10, 2)->default(50000); // Default harga Rp 50.000 per jam + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tables', function (Blueprint $table) { + $table->dropColumn('price_per_hour'); + }); + } +}; diff --git a/database/seeders/VenueSeeder.php b/database/seeders/VenueSeeder.php index 0b818ea..d062bff 100644 --- a/database/seeders/VenueSeeder.php +++ b/database/seeders/VenueSeeder.php @@ -16,7 +16,7 @@ public function run(): void 'name' => 'Capitano Billiard', 'location' => 'Genteng', 'address' => 'Jl. Hasanudin No.II, Dusun Krajan, Genteng Wetan, Kec. Genteng, Kabupaten Banyuwangi', - 'price' => 30000, + 'price' => 100000, 'image' => 'images/billiard2.jpg', 'tables' => [ ['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'], @@ -31,7 +31,7 @@ public function run(): void 'name' => 'Osing Billiard Center', 'location' => 'Lidah', 'address' => 'Dusun Krajan, Kalirejo, Kec. Kabat, Kabupaten Banyuwangi', - 'price' => 25000, + 'price' => 90000, 'image' => 'images/billiard3.jpg', 'tables' => [ ['name' => 'Table 1', 'brand' => 'Xingjue', 'status' => 'Booked'], @@ -47,7 +47,7 @@ public function run(): void 'name' => 'DAS Game & Billiard', 'location' => 'Jalen', 'address' => 'Jl. Samiran, Jalen Parungan, Setail, Kec. Genteng, Kabupaten Banyuwangi', - 'price' => 20000, + 'price' => 95000, 'image' => 'images/billiard4.jpg', 'tables' => [ ['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'], diff --git a/resources/views/pages/venue.blade.php b/resources/views/pages/venue.blade.php index 720f6ff..5d246bf 100644 --- a/resources/views/pages/venue.blade.php +++ b/resources/views/pages/venue.blade.php @@ -80,6 +80,8 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p + + - - - @endsection \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index e19d8be..85007b2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,9 @@ Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue'); Route::post('/booking', [BookingController::class, 'store'])->name('booking.store'); Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules'); +Route::post('/booking/payment', [BookingController::class, 'processPayment'])->name('booking.payment'); +Route::get('/booking/payment/{bookingId}', [BookingController::class, 'checkPaymentStatus'])->name('booking.payment.status'); +Route::post('/payment/notification', [BookingController::class, 'handleNotification'])->name('payment.notification'); Route::middleware(['auth', 'is_admin'])->prefix('admin')->group(function () { Route::get('/', [AdminController::class, 'index'])->name('admin.dashboard'); Route::get('/bookings', [BookingsController::class, 'index'])->name('admin.bookings.index');