Files
kulakpos_web/public/restaurant/Modules/Customer/app/Http/Controllers/CustomerController.php

569 lines
21 KiB
PHP
Raw Normal View History

2026-03-15 17:08:23 +07:00
<?php
namespace Modules\Customer\Http\Controllers;
use App\Enum\ActionStatus;
use App\Enum\OrderStatus;
use App\Helper\OrderHelper;
use App\Http\Controllers\Controller;
use App\Mail\OrderInvoiceMail;
use App\Services\Firebase\FirebaseService;
use App\Traits\Authenticatable;
use App\Traits\RequestSanitizerTrait;
use App\Traits\Trackable;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Modules\Booking\Http\Requests\Booking\CustomerBookingStoreRequest;
use Modules\Booking\Models\Booking;
use Modules\Booking\Models\BookingItem;
use Modules\Restaurant\Http\Requests\Order\CustomerOrderStoreRequest;
use Modules\Restaurant\Http\Requests\Order\CustomerOrderUpdateRequest;
use Modules\Restaurant\Http\Resources\OrderResource;
use Modules\Restaurant\Models\Customer;
use Modules\Restaurant\Models\Order;
class CustomerController extends Controller
{
use Authenticatable, RequestSanitizerTrait, Trackable;
public function index(): JsonResponse
{
try {
$query = Order::query()
->select('orders.*')
->leftJoin('customers', 'orders.customer_id', '=', 'customers.id')
->leftJoin('users as waiters', 'orders.waiter_id', '=', 'waiters.id')
->with([
'items.variation',
'items.food',
'customer',
'table',
'paymentMethod',
'waiter',
]);
/**
* 🔍 GLOBAL SEARCH
*/
if ($search = request('search')) {
$search = strtolower(trim($search));
$query->where(function ($q) use ($search) {
$q->whereRaw('LOWER(orders.order_number) LIKE ?', ["%{$search}%"])
->orWhereRaw('LOWER(orders.order_type) LIKE ?', ["%{$search}%"])
->orWhereRaw('LOWER(orders.status) LIKE ?', ["%{$search}%"])
->orWhereRaw('LOWER(customers.name) LIKE ?', ["%{$search}%"])
->orWhereRaw('LOWER(customers.phone) LIKE ?', ["%{$search}%"])
->orWhereRaw('LOWER(waiters.first_name) LIKE ?', ["%{$search}%"]);
});
}
/**
* 📌 STATUS FILTER
*/
if ($status = request('status')) {
$query->where('orders.status', $status);
}
/**
* 🍽 ORDER TYPE FILTER
*/
if ($orderType = request('order_type')) {
$query->where('orders.order_type', $orderType);
}
/**
* 👨‍🍳 WAITER FILTER
*/
if ($waiterId = request('waiter_id')) {
$query->where('orders.waiter_id', $waiterId);
}
/**
* 👤 CUSTOMER FILTER
*/
if ($customerId = request('customer_id')) {
$query->where('orders.customer_id', $customerId);
}
/**
* 📆 DATE RANGE FILTER
*/
if (request('from_date') && request('to_date')) {
$query->whereBetween('orders.created_at', [
request('from_date').' 00:00:00',
request('to_date').' 23:59:59',
]);
}
/**
* 🔃 SORTING (SAFE WHITELIST)
*/
$allowedSorts = [
'created_at',
'order_number',
'status',
'order_type',
'grand_total',
];
$sortBy = request('sort_by', 'created_at');
$sortOrder = request('sort_order', 'desc');
if (! in_array($sortBy, $allowedSorts)) {
$sortBy = 'created_at';
}
if (! in_array($sortOrder, ['asc', 'desc'])) {
$sortOrder = 'desc';
}
/**
* 📄 PAGINATION
*/
$orders = $query
->orderBy("orders.$sortBy", $sortOrder)
->paginate(request('perPage', 10));
/**
* 📦 APPLY RESOURCE (KEEP PAGINATION STRUCTURE)
*/
$orders->getCollection()->transform(function ($order) {
return new OrderResource($order);
});
return $this->responseSuccess([
$orders,
], 'Order fetch successfully.');
} catch (\Throwable $e) {
return $this->responseError([], 'Order fetch failed: '.$e->getMessage());
}
}
public function store(CustomerOrderStoreRequest $request, FirebaseService $firebase): JsonResponse
{
DB::beginTransaction();
try {
$restaurantId = $this->getCurrentRestaurantId();
$orderNumber = OrderHelper::generateOrderNumber($restaurantId);
// 🔹 Calculate totals
$subtotal = collect($request->items)->sum(fn ($i) => $i['quantity'] * $i['price']);
$grandTotal = $subtotal - ($request->discount ?? 0) + ($request->tax ?? 0);
// 🔹 Create Order
$order = Order::create([
'restaurant_id' => $restaurantId,
'order_number' => $orderNumber,
'order_type' => $request->order_type,
'status' => 'pending',
'table_id' => $request->table_id,
// 'waiter_id' => $request->waiter_id,
'customer_id' => getUserId(),
'payment_method_id' => $request->payment_method_id,
'payment_status' => $request->payment_status ?? false,
'subtotal' => $subtotal,
'discount' => $request->discount ?? 0,
'tax' => $request->tax ?? 0,
'grand_total' => $grandTotal,
'order_date' => Carbon::now(),
'added_by' => getUserId(),
]);
// 🔹 Create Order Items
foreach ($request->items as $item) {
$orderItem = $order->items()->create([
'restaurant_id' => $restaurantId,
'food_item_id' => $item['food_item_id'],
'food_variant_id' => $item['food_variant_id'] ?? null,
'quantity' => $item['quantity'],
'price' => $item['price'],
'total' => $item['quantity'] * $item['price'],
'addons' => isset($item['addons']) ? json_encode($item['addons']) : null,
]);
/**
* 🔽 Ingredient deduction (Commented out for reference)
*
* foreach ($item['ingredients'] ?? [] as $ingredient) {
* $ingredientModel = Ingredient::find($ingredient['id']);
* if ($ingredientModel) {
* $deductQty = $ingredient['quantity_required'] * $item['quantity'];
* $ingredientModel->decrement('available_quantity', $deductQty);
* }
* }
*/
}
$customer = auth()->user();
// Action Log info
$this->trackAction(ActionStatus::OrderCreate, Customer::class, $customer->id, $order);
DB::commit();
// Push Notification
if ($customer?->fcm_token) {
$result = $firebase->sendNotification(
$customer->fcm_token,
'Order Confirmed',
"Your order #{$order->id} has been confirmed!",
['order_id' => $order->id]
);
if ($result === true) {
return response()->json(['message' => 'Notification sent successfully']);
} else {
return response()->json(['message' => 'Failed to send notification', 'error' => $result], 500);
}
}
// Directly send email
if ($order->customer?->email) {
try {
$result = shop_mailer_send($order->restaurant_id, $order->customer?->email, new OrderInvoiceMail($order));
if (! $result['status']) {
Log::warning("Order invoice email not sent for shop {$order->restaurant_id}: ".$result['message']);
}
Mail::to($order->customer?->email)->send(new OrderInvoiceMail($order));
} catch (Exception $ex) {
Log::error("Invoice email failed for order {$order->id}: ".$ex->getMessage());
}
}
return $this->responseSuccess([
'order' => $order->load('items'),
], 'Order created successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], 'Order creation failed: '.$e->getMessage());
}
}
public function show(int $id): JsonResponse
{
try {
$order = Order::with([
'items.food', // If you have relation: OrderItem → Food
'customer',
'table',
'paymentMethod',
])->findOrFail($id);
return $this->responseSuccess($order, 'Order has been fetched successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function update(CustomerOrderUpdateRequest $request, int $id): JsonResponse
{
DB::beginTransaction();
try {
$order = Order::with('items')->findOrFail($id);
// Recalculate total
$totalAmount = collect($request->items)->sum(function ($item) {
return $item['quantity'] * $item['price'];
});
// Update order (order_number stays untouched)
$order->update([
'order_type' => $request->order_type,
// 'status' => $request->status,
'table_id' => $request->table_id,
'payment_method_id' => $request->payment_method_id,
'payment_status' => $request->payment_status ?? false,
'waiter_id' => $request->waiter_id,
// 'customer_id' => $request->customer_id,
'grand_total' => $totalAmount,
'updated_by' => getUserId(),
]);
$existingItemIds = $order->items->pluck('id')->toArray();
$requestItemIds = collect($request->items)->pluck('id')->filter()->toArray();
// 1⃣ Remove items that are not in request anymore
$itemsToDelete = array_diff($existingItemIds, $requestItemIds);
if (! empty($itemsToDelete)) {
$order->items()->whereIn('id', $itemsToDelete)->delete();
}
// 2⃣ Add or update items
foreach ($request->items as $item) {
if (isset($item['id']) && in_array($item['id'], $existingItemIds)) {
// Update existing item
$order->items()->where('id', $item['id'])->update([
'restaurant_id' => $order->restaurant_id,
'food_item_id' => $item['food_item_id'],
'food_variant_id' => $item['food_variant_id'] ?? null,
'quantity' => $item['quantity'],
'price' => $item['price'],
'total' => $item['quantity'] * $item['price'],
]);
} else {
// Add new item
$order->items()->create([
'restaurant_id' => $order->restaurant_id,
'food_item_id' => $item['food_item_id'],
'food_variant_id' => $item['food_variant_id'] ?? null,
'quantity' => $item['quantity'],
'price' => $item['price'],
'total' => $item['quantity'] * $item['price'],
]);
}
}
DB::commit();
return $this->responseSuccess($order->load('items'), 'Order has been updated successfully.');
} catch (\Throwable $e) {
DB::rollBack();
return $this->responseError([], 'Failed to update order: '.$e->getMessage());
}
}
public function destroy(int $id): JsonResponse
{
DB::beginTransaction();
try {
$order = Order::with('items')->findOrFail($id);
// Delete related order items
foreach ($order->items as $item) {
$item->delete();
}
// Delete the order
$order->delete();
DB::commit();
return $this->responseSuccess([], 'Order and related items have been deleted successfully.');
} catch (\Throwable $e) {
DB::rollBack();
return $this->responseError([], 'Failed to delete order: '.$e->getMessage());
}
}
public function cancel(Request $request, int $orderId, FirebaseService $firebase): JsonResponse
{
$request->validate([
'cancel_reason' => 'nullable|string|max:255',
]);
DB::beginTransaction();
try {
$restaurantId = $this->getCurrentRestaurantId();
$order = Order::where('restaurant_id', $restaurantId)
->where('id', $orderId)
->first();
if (! $order) {
return $this->responseError([], 'Order not found.');
}
// 🚫 Block if already cancelled or completed
if (in_array($order->status, ['cancelled', 'completed'])) {
return $this->responseError([], "This order is already {$order->status} and cannot be cancelled.");
}
// 🕒 Check time difference (5 minutes)
$minutesSinceOrder = now()->diffInMinutes($order->created_at);
$cancelLimit = env('ORDER_CANCEL_LIMIT', 5);
if ($minutesSinceOrder > $cancelLimit) {
return $this->responseError([], "You can only cancel an order within {$cancelLimit} minutes of placement.");
}
// 🔹 Update order cancellation info
$order->update([
'status' => OrderStatus::CANCELLED,
'cancelled_at' => now(),
'cancel_reason' => $request->cancel_reason ?? 'No reason provided',
'cancelled_by' => getUserId(),
]);
$customer = auth()->user();
// Action Log info
$this->trackAction(ActionStatus::OrderCreate, Customer::class, $customer->id, $order);
DB::commit();
// TODO
// Order Cancel Mail & Notification
// // Push Notification
// if ($customer?->fcm_token) {
// $result = $firebase->sendNotification(
// $customer->fcm_token,
// 'Order Confirmed',
// "Your order #{$order->id} has been confirmed!",
// ['order_id' => $order->id]
// );
// if ($result === true) {
// return response()->json(['message' => 'Notification sent successfully']);
// } else {
// return response()->json(['message' => 'Failed to send notification', 'error' => $result], 500);
// }
// }
// // Directly send email
// if ($order->customer?->email) {
// try {
// $result = shop_mailer_send($order->restaurant_id, $order->customer?->email, new OrderInvoiceMail($order));
// if (! $result['status']) {
// Log::warning("Order invoice email not sent for shop {$order->restaurant_id}: " . $result['message']);
// }
// Mail::to($order->customer?->email)->send(new OrderInvoiceMail($order));
// } catch (Exception $ex) {
// Log::error("Invoice email failed for order {$order->id}: " . $ex->getMessage());
// }
// }
return $this->responseSuccess([
'order' => $order->fresh(),
], 'Order cancelled successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], 'Order cancellation failed: '.$e->getMessage());
}
}
public function myBooking(): JsonResponse
{
try {
$filters = request()->all();
$query = Booking::where('customer_id', getUserId())
->with([
'customer',
'hotel',
'bookingItems.room',
]);
// -----------------------------
// 🔍 SEARCHING
// -----------------------------
if (! empty($filters['search'])) {
$search = $filters['search'];
$query->whereHas('customer', function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%")
->orWhere('phone', 'LIKE', "%$search%");
})->orWhere('id', $search);
}
// -----------------------------
// 🎯 FILTERS
// -----------------------------
if (isset($filters['status'])) {
$query->where('status', $filters['status']);
}
if (! empty($filters['customer_id'])) {
$query->where('customer_id', $filters['customer_id']);
}
if (! empty($filters['hotel_id'])) {
$query->where('hotel_id', $filters['hotel_id']);
}
if (! empty($filters['check_in_from']) && ! empty($filters['check_in_to'])) {
$query->whereBetween('check_in', [$filters['check_in_from'], $filters['check_in_to']]);
}
if (! empty($filters['check_out_from']) && ! empty($filters['check_out_to'])) {
$query->whereBetween('check_out', [$filters['check_out_from'], $filters['check_out_to']]);
}
// -----------------------------
// 🔽 SORTING
// -----------------------------
$sortBy = $filters['sort_by'] ?? 'check_in';
$sortOrder = $filters['sort_order'] ?? 'desc';
$allowedSortColumns = ['id', 'check_in', 'check_out', 'status', 'created_at'];
if (! in_array($sortBy, $allowedSortColumns)) {
$sortBy = 'check_in';
}
$query->orderBy($sortBy, $sortOrder);
// -----------------------------
// 📄 PAGINATION
// -----------------------------
$perPage = $filters['per_page'] ?? 20;
$bookings = $query->paginate($perPage);
return $this->responseSuccess($bookings, 'Bookings fetched successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function booking(CustomerBookingStoreRequest $request): JsonResponse
{
$data = $request->validated();
DB::beginTransaction();
try {
// Create main booking
$booking = Booking::create([
'restaurant_id' => getUserRestaurantId(),
'hotel_id' => $data['hotel_id'],
'customer_id' => getUserId(),
'check_in' => $data['check_in'],
'check_out' => $data['check_out'],
'check_in_time' => $data['check_in_time'] ?? null,
'check_out_time' => $data['check_out_time'] ?? null,
'total_adults' => $data['total_adults'],
'total_children' => $data['total_children'],
'subtotal_amount' => $data['subtotal_amount'],
'tax_amount' => $data['tax_amount'] ?? 0,
'discount_amount' => $data['discount_amount'] ?? 0,
'total_amount' => $data['total_amount'],
'payment_status' => $data['payment_status'],
'payment_method' => $data['payment_method'] ?? null,
'channel' => $data['channel'] ?? null,
'status' => $data['status'],
'remarks' => $data['remarks'] ?? null,
]);
// Create booking items
foreach ($data['booking_items'] as $item) {
BookingItem::create([
'restaurant_id' => getUserRestaurantId(),
'booking_id' => $booking->id,
'room_id' => $item['room_id'],
'adults' => $item['adults'],
'children' => $item['children'],
'room_price' => $item['room_price'],
'nights' => $item['nights'],
'tax_amount' => $item['tax_amount'] ?? 0,
'total_amount' => $item['total_amount'],
'status' => $item['status'],
]);
}
DB::commit();
return $this->responseSuccess($booking->load('bookingItems'), 'Booking has been created successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
}