Midtrans Berhasil

This commit is contained in:
Stephen Gesityan 2025-05-08 15:37:35 +07:00
parent 4de8cac352
commit 4e1dfd6f24
14 changed files with 935 additions and 53 deletions

View File

@ -56,4 +56,4 @@ VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@ -5,44 +5,96 @@
use App\Models\Booking; use App\Models\Booking;
use App\Models\Table; use App\Models\Table;
use App\Services\MidtransService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class BookingController extends Controller class BookingController extends Controller
{ {
protected $midtransService;
public function __construct(MidtransService $midtransService)
{
$this->midtransService = $midtransService;
}
public function store(Request $request) { public function store(Request $request) {
$request->validate([ try {
'table_id' => 'required|exists:tables,id', $request->validate([
'start_time' => 'required|date', 'table_id' => 'required|exists:tables,id',
'end_time' => 'required|date|after:start_time', 'start_time' => 'required|date',
]); 'end_time' => 'required|date|after:start_time',
]);
// Cek apakah meja sedang dibooking pada waktu tersebut // Cek apakah meja sedang dibooking pada waktu tersebut
$conflict = Booking::where('table_id', $request->table_id) $conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) { ->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time]) $query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) { ->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time) $query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time); ->where('end_time', '>', $request->start_time);
}); });
}) })
->where('status', '!=', 'cancelled') // skip booking yang dibatalkan ->where('status', '!=', 'cancelled')
->exists(); ->where('status', '!=', 'expired')
->exists();
if ($conflict) { if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409); 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) { public function getBookedSchedules(Request $request) {
@ -54,6 +106,7 @@ public function getBookedSchedules(Request $request) {
$bookings = Booking::where('table_id', $request->table_id) $bookings = Booking::where('table_id', $request->table_id)
->whereDate('start_time', $request->date) ->whereDate('start_time', $request->date)
->where('status', '!=', 'cancelled') ->where('status', '!=', 'cancelled')
->where('status', '!=', 'expired')
->select('start_time', 'end_time') ->select('start_time', 'end_time')
->get() ->get()
->map(function ($booking) { ->map(function ($booking) {
@ -65,4 +118,58 @@ public function getBookedSchedules(Request $request) {
return response()->json($bookings); 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);
}
}
} }

View File

@ -9,7 +9,24 @@ class Booking extends Model
{ {
use HasFactory; 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() public function table()
{ {
@ -20,4 +37,24 @@ public function user()
{ {
return $this->belongsTo(User::class); 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';
}
} }

View File

@ -9,7 +9,17 @@ class Table extends Model
{ {
use HasFactory; 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() public function venue()
{ {

View File

@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use App\Services\MidtransService;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -11,7 +12,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
// $this->app->singleton(MidtransService::class, function ($app) {
return new MidtransService();
});
} }
/** /**

View File

@ -0,0 +1,165 @@
<?php
namespace App\Services;
use App\Models\Booking;
use Midtrans\Config;
use Midtrans\Snap;
use Illuminate\Support\Facades\Log;
class MidtransService
{
public function __construct()
{
$serverKey = config('midtrans.server_key');
$clientKey = config('midtrans.client_key');
$merchantId = config('midtrans.merchant_id');
$isProduction = config('midtrans.is_production', false);
// Log semua konfigurasi untuk debugging
Log::info('Midtrans Configuration:', [
'server_key' => $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;
}
}
}

View File

@ -6,11 +6,13 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.1", "php": "^8.1",
"doctrine/dbal": "^3.9",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10", "laravel/framework": "^10.10",
"laravel/sanctum": "^3.3", "laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"laravel/ui": "^4.6" "laravel/ui": "^4.6",
"midtrans/midtrans-php": "^2.6"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",

455
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "dcbbd52cf3c38d0f1b88e12d39e47db7", "content-hash": "edf30558ed2abcf5bae02cf31ec6ee71",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -210,6 +210,349 @@
}, },
"time": "2024-07-08T12:26:09+00:00" "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", "name": "doctrine/inflector",
"version": "2.0.10", "version": "2.0.10",
@ -1947,6 +2290,67 @@
], ],
"time": "2024-09-21T08:32:55+00:00" "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", "name": "monolog/monolog",
"version": "3.7.0", "version": "3.7.0",
@ -2522,6 +2926,55 @@
], ],
"time": "2024-07-20T21:41:07+00:00" "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", "name": "psr/clock",
"version": "1.0.0", "version": "1.0.0",

12
config/midtrans.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'merchant_id' => 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',
];

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->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
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tables', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -16,7 +16,7 @@ public function run(): void
'name' => 'Capitano Billiard', 'name' => 'Capitano Billiard',
'location' => 'Genteng', 'location' => 'Genteng',
'address' => 'Jl. Hasanudin No.II, Dusun Krajan, Genteng Wetan, Kec. Genteng, Kabupaten Banyuwangi', 'address' => 'Jl. Hasanudin No.II, Dusun Krajan, Genteng Wetan, Kec. Genteng, Kabupaten Banyuwangi',
'price' => 30000, 'price' => 100000,
'image' => 'images/billiard2.jpg', 'image' => 'images/billiard2.jpg',
'tables' => [ 'tables' => [
['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'], ['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'],
@ -31,7 +31,7 @@ public function run(): void
'name' => 'Osing Billiard Center', 'name' => 'Osing Billiard Center',
'location' => 'Lidah', 'location' => 'Lidah',
'address' => 'Dusun Krajan, Kalirejo, Kec. Kabat, Kabupaten Banyuwangi', 'address' => 'Dusun Krajan, Kalirejo, Kec. Kabat, Kabupaten Banyuwangi',
'price' => 25000, 'price' => 90000,
'image' => 'images/billiard3.jpg', 'image' => 'images/billiard3.jpg',
'tables' => [ 'tables' => [
['name' => 'Table 1', 'brand' => 'Xingjue', 'status' => 'Booked'], ['name' => 'Table 1', 'brand' => 'Xingjue', 'status' => 'Booked'],
@ -47,7 +47,7 @@ public function run(): void
'name' => 'DAS Game & Billiard', 'name' => 'DAS Game & Billiard',
'location' => 'Jalen', 'location' => 'Jalen',
'address' => 'Jl. Samiran, Jalen Parungan, Setail, Kec. Genteng, Kabupaten Banyuwangi', 'address' => 'Jl. Samiran, Jalen Parungan, Setail, Kec. Genteng, Kabupaten Banyuwangi',
'price' => 20000, 'price' => 95000,
'image' => 'images/billiard4.jpg', 'image' => 'images/billiard4.jpg',
'tables' => [ 'tables' => [
['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'], ['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'],

View File

@ -80,6 +80,8 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
</div> </div>
</div> </div>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://app.sandbox.midtrans.com/snap/snap.js" data-client-key="{{ config('midtrans.client_key') }}"></script>
<script> <script>
function updateClock() { function updateClock() {
const now = new Date(); const now = new Date();
@ -128,6 +130,7 @@ function updateClock() {
submitBooking(tableId, tableName) { submitBooking(tableId, tableName) {
if (!this.isLoggedIn) { if (!this.isLoggedIn) {
alert('Silahkan login terlebih dahulu untuk melakukan booking.'); alert('Silahkan login terlebih dahulu untuk melakukan booking.');
return;
} }
const selectedTime = this.selectedTime; const selectedTime = this.selectedTime;
const selectedDuration = this.selectedDuration; const selectedDuration = this.selectedDuration;
@ -174,25 +177,45 @@ function updateClock() {
end_time: end_time, end_time: end_time,
}), }),
}) })
.then(res => { .then(res => {
if (res.status === 409) throw new Error('Meja sudah dibooking.'); if (!res.ok) {
return res.json(); return res.json().then(err => {
}) throw new Error(err.message || 'Gagal membuat booking');
.then(data => { });
alert(`Booking ${tableName} berhasil! Meja akan diblokir dari ${selectedTime} hingga ${endTimeFormatted}`); }
location.reload(); // Reload untuk update status meja return res.json();
}) })
.catch(err => { .then(data => {
alert('Gagal booking: ' + err.message); if (!data.snap_token) {
}) throw new Error('Snap token tidak ditemukan');
.finally(() => { }
this.isLoading = false;
// Buka Snap Midtrans
window.snap.pay(data.snap_token, {
onSuccess: function(result) {
alert('Pembayaran berhasil!');
location.reload();
},
onPending: function(result) {
alert('Pembayaran pending, silahkan selesaikan pembayaran');
},
onError: function(result) {
alert('Pembayaran gagal');
},
onClose: function() {
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
}
}); });
})
.catch(err => {
console.error('Booking error:', err);
alert('Gagal booking: ' + err.message);
})
.finally(() => {
this.isLoading = false;
});
} }
})) }))
}) })
</script> </script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
@endsection @endsection

View File

@ -14,6 +14,9 @@
Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue'); Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue');
Route::post('/booking', [BookingController::class, 'store'])->name('booking.store'); Route::post('/booking', [BookingController::class, 'store'])->name('booking.store');
Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules'); 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::middleware(['auth', 'is_admin'])->prefix('admin')->group(function () {
Route::get('/', [AdminController::class, 'index'])->name('admin.dashboard'); Route::get('/', [AdminController::class, 'index'])->name('admin.dashboard');
Route::get('/bookings', [BookingsController::class, 'index'])->name('admin.bookings.index'); Route::get('/bookings', [BookingsController::class, 'index'])->name('admin.bookings.index');