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');