Files

569 lines
21 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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());
}
}
}