migrate to gtea from bistbucket
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user