migrate to gtea from bistbucket
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Http\Controllers;
|
||||
|
||||
use App\Helper\RestaurantHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\Authenticatable;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Modules\Customer\Http\Requests\Auth\CustomerLoginRequest;
|
||||
use Modules\Customer\Http\Requests\Auth\ProfileUpdateRequest;
|
||||
use Modules\Customer\Models\OtpVerification;
|
||||
use Modules\Restaurant\Models\Customer;
|
||||
|
||||
class CustomerAuthController extends Controller
|
||||
{
|
||||
use Authenticatable;
|
||||
|
||||
/**
|
||||
* Step 1: Send OTP to phone (Create customer if not exists)
|
||||
*/
|
||||
public function customerLogin(CustomerLoginRequest $request)
|
||||
{
|
||||
$phone = $request->phone;
|
||||
$restaurantId = $this->getRestaurantIdFromHeader($request);
|
||||
if ($restaurantId instanceof JsonResponse) {
|
||||
return $restaurantId;
|
||||
}
|
||||
|
||||
// Check if customer already exists
|
||||
$customer = Customer::where('restaurant_id', $restaurantId)
|
||||
->where('phone', $phone)
|
||||
->first();
|
||||
|
||||
// If customer not exists, create one
|
||||
if (! $customer) {
|
||||
$customer = Customer::create([
|
||||
'name' => 'Customer '.substr($phone, -4),
|
||||
'phone' => $phone,
|
||||
'password' => Hash::make($phone), // temporary password
|
||||
'restaurant_id' => $restaurantId,
|
||||
]);
|
||||
}
|
||||
|
||||
// Generate OTP
|
||||
$otp = '1234'; // rand(100000, 999999);
|
||||
$expiryTime = now()->addMinutes(5);
|
||||
|
||||
// Save or update OTP record
|
||||
OtpVerification::updateOrCreate(
|
||||
[
|
||||
'restaurant_id' => $restaurantId,
|
||||
'phone' => $phone,
|
||||
],
|
||||
[
|
||||
'otp' => $otp,
|
||||
'expires_at' => $expiryTime,
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// TODO: Send OTP via SMS Gateway (mock for now)
|
||||
// Muthofun::send($phone, "Your OTP is: $otp");
|
||||
|
||||
return $this->ResponseSuccess([
|
||||
'phone' => $phone,
|
||||
'otp' => $otp, // ⚠️ remove in production
|
||||
], 'OTP sent successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Verify OTP and Login
|
||||
*/
|
||||
public function customerOtpVerify(Request $request)
|
||||
{
|
||||
$restaurantId = $this->getRestaurantIdFromHeader($request);
|
||||
if ($restaurantId instanceof JsonResponse) {
|
||||
return $restaurantId;
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'phone' => 'required|numeric|exists:customers,phone',
|
||||
'otp' => 'required',
|
||||
]);
|
||||
|
||||
$phone = $request->phone;
|
||||
$otp = $request->otp;
|
||||
|
||||
// Fetch OTP record
|
||||
$otpRecord = OtpVerification::where('restaurant_id', $restaurantId)
|
||||
->where('phone', $phone)
|
||||
->first();
|
||||
if (! $otpRecord) {
|
||||
return $this->ResponseError([], 'Invalid phone or OTP expired.', 400);
|
||||
}
|
||||
|
||||
// Check expiry
|
||||
if (now()->greaterThan($otpRecord->expires_at)) {
|
||||
$otpRecord->update(['otp' => null]);
|
||||
|
||||
return $this->ResponseError([], 'OTP expired. Please request a new one.', 400);
|
||||
}
|
||||
|
||||
// Check OTP validity
|
||||
if ($otpRecord->otp != $otp) {
|
||||
return $this->ResponseError([], 'Invalid OTP.', 400);
|
||||
}
|
||||
|
||||
// OTP valid → Find customer
|
||||
$customer = Customer::where('restaurant_id', $restaurantId)
|
||||
->where('phone', $phone)
|
||||
->first();
|
||||
|
||||
if (! $customer) {
|
||||
return $this->ResponseError([], 'Customer not found.', 404);
|
||||
}
|
||||
|
||||
// Clear OTP after success
|
||||
$otpRecord->update(['otp' => null]);
|
||||
|
||||
// Create API token
|
||||
$tokenResult = $customer->createToken('Customer Access Token');
|
||||
|
||||
return $this->ResponseSuccess([
|
||||
'customer' => $customer,
|
||||
'access_token' => $tokenResult->accessToken,
|
||||
'expires_at' => $tokenResult->token->expires_at,
|
||||
], 'Login successful.');
|
||||
}
|
||||
|
||||
public function customerOtpSent(Request $request)
|
||||
{
|
||||
$restaurantId = $this->getRestaurantIdFromHeader($request);
|
||||
if ($restaurantId instanceof JsonResponse) {
|
||||
return $restaurantId;
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'phone' => 'required|numeric|exists:customers,phone',
|
||||
]);
|
||||
|
||||
$phone = $request->phone;
|
||||
// TODO
|
||||
$expiryTime = now()->addMinutes(15); // OTP expiry = 2 minutes
|
||||
$otp = rand(100000, 999999); // Generate random 6-digit OTP
|
||||
|
||||
// Check if an OTP record already exists for this phone
|
||||
$otpRecord = OtpVerification::where('restaurant_id', $restaurantId)
|
||||
->where('phone', $phone)
|
||||
->first();
|
||||
|
||||
if ($otpRecord) {
|
||||
// Check if existing OTP is still valid (not expired)
|
||||
if (now()->lessThan($otpRecord->expires_at)) {
|
||||
$remainingSeconds = now()->diffInSeconds($otpRecord->expires_at);
|
||||
|
||||
return $this->ResponseError([
|
||||
'remaining_seconds' => $remainingSeconds,
|
||||
], "An OTP has already been sent. Please wait {$remainingSeconds} seconds before requesting a new one.", 429);
|
||||
}
|
||||
|
||||
// OTP expired → generate and resend a new one
|
||||
$otpRecord->update([
|
||||
'otp' => $otp,
|
||||
'expires_at' => $expiryTime,
|
||||
]);
|
||||
} else {
|
||||
// No record → create a new one
|
||||
OtpVerification::create([
|
||||
'phone' => $phone,
|
||||
'otp' => $otp,
|
||||
'expires_at' => $expiryTime,
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO: Replace with your SMS sending logic
|
||||
// Example: Muthofun::send($phone, "Your OTP is: $otp");
|
||||
|
||||
return $this->ResponseSuccess([
|
||||
'phone' => $phone,
|
||||
'otp' => $otp, // ⚠️ Remove in production
|
||||
'expires_at' => $expiryTime->toDateTimeString(),
|
||||
], 'OTP sent successfully.');
|
||||
}
|
||||
|
||||
public function customerProfile(Request $request)
|
||||
{
|
||||
try {
|
||||
$customer = Customer::where('id', Auth::id())->first();
|
||||
|
||||
if ($customer) {
|
||||
$permissions = $customer->role?->permissions->pluck('name')->toArray() ?? []; // Convert permissions to an array
|
||||
|
||||
$customer = [
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->first_name.' '.$customer->last_name,
|
||||
'email' => $customer->email,
|
||||
'phone' => $customer->phone,
|
||||
'image' => $customer->avatar,
|
||||
'role' => $customer->role?->name ?? 'no-role-assign',
|
||||
'status' => $customer->status,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
|
||||
return $this->responseSuccess($customer, 'Profile fetched successfully.');
|
||||
} else {
|
||||
return $this->responseError([], 'Profile not found.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return $this->responseError([], 'An error occurred while fetching the profile.');
|
||||
}
|
||||
}
|
||||
|
||||
public function customerUpdate(ProfileUpdateRequest $request)
|
||||
{
|
||||
$customer = Auth::guard('customer')->user();
|
||||
|
||||
// Get validated data
|
||||
$data = $request->validated();
|
||||
|
||||
// Avatar upload (first time + update)
|
||||
if ($request->hasFile('avatar')) {
|
||||
$data['avatar'] = fileUploader(
|
||||
'customers/',
|
||||
'png',
|
||||
$request->file('avatar'),
|
||||
$customer->avatar // old image (null for first upload)
|
||||
);
|
||||
}
|
||||
|
||||
// Fill customer data
|
||||
$customer->fill($data);
|
||||
|
||||
// Reset email verification if email changed
|
||||
if ($customer->isDirty('email')) {
|
||||
$customer->email_verified_at = null;
|
||||
}
|
||||
|
||||
$customer->save();
|
||||
|
||||
return $this->responseSuccess(
|
||||
$customer->fresh(),
|
||||
'Profile Update Successfully Done.'
|
||||
);
|
||||
}
|
||||
|
||||
public function changePassword(Request $request)
|
||||
{
|
||||
$validated = $request->validateWithBag('updatePassword', [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->customer()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return $this->ResponseSuccess([], 'Password Changes Successfully.');
|
||||
}
|
||||
|
||||
public function accountDeleted(Request $request)
|
||||
{
|
||||
$request->validateWithBag('updatePassword', [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$request->customer()->update([
|
||||
'deleted_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
return $this->ResponseSuccess([], 'Account Delete successfully.');
|
||||
}
|
||||
|
||||
public function customerLogout(Request $request)
|
||||
{
|
||||
try {
|
||||
$result = $request->customer()->token()->revoke();
|
||||
if ($result) {
|
||||
return $this->ResponseSuccess([], 'Logout Success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return $this->ResponseSuccess([], 'Logout Failed');
|
||||
}
|
||||
}
|
||||
|
||||
private function getRestaurantIdFromHeader(Request $request)
|
||||
{
|
||||
$domain = $request->header('X-Domain'); // Custom header key (Change if needed)
|
||||
if (! $domain) {
|
||||
return $this->responseError([], 'Domain header is missing.', 400);
|
||||
}
|
||||
|
||||
$restaurantResponse = RestaurantHelper::getRestaurantIdByDomain($domain);
|
||||
if (! $restaurantResponse['status']) {
|
||||
return $this->responseError([], $restaurantResponse['error'], 404);
|
||||
}
|
||||
|
||||
return $restaurantResponse['restaurant_id'];
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CustomerLoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'phone' => 'required|exists:customers,phone',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
// Proceed with normal authentication
|
||||
if (! Auth::attempt($this->only('phone'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'phone' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'phone' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('phone')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'sometimes|string|max:255',
|
||||
// 'email' => 'sometimes|email|max:255',
|
||||
// 'phone' => 'sometimes|string|max:20',
|
||||
'address' => 'sometimes|string|max:500',
|
||||
'avatar' => 'sometimes|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
// Proceed with normal authentication
|
||||
if (! Auth::attempt($this->only('phone'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'phone' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'phone' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('phone')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OtpVerification extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = [
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
52
public/restaurant/Modules/Customer/app/Models/Review.php
Normal file
52
public/restaurant/Modules/Customer/app/Models/Review.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Modules\Restaurant\Models\Customer;
|
||||
use Modules\Restaurant\Models\FoodItem;
|
||||
|
||||
class Review extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'restaurant_id',
|
||||
'customer_id',
|
||||
'food_item_id',
|
||||
'rating',
|
||||
'comment',
|
||||
'is_approved',
|
||||
'is_active',
|
||||
'likes_count',
|
||||
'reported_count',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_approved' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'rating' => 'integer',
|
||||
];
|
||||
|
||||
public const TABLE_NAME = 'reviews';
|
||||
|
||||
protected $table = self::TABLE_NAME;
|
||||
|
||||
/* ================= Relationships ================= */
|
||||
|
||||
public function images()
|
||||
{
|
||||
return $this->hasMany(ReviewImage::class)->select('id', 'review_id', 'image_path', 'position');
|
||||
}
|
||||
|
||||
public function foodItem()
|
||||
{
|
||||
return $this->belongsTo(FoodItem::class)->select('id', 'name', 'image');
|
||||
}
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReviewImage extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'restaurant_id',
|
||||
'review_id',
|
||||
'image_path',
|
||||
'position',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'position' => 'integer',
|
||||
];
|
||||
|
||||
public const TABLE_NAME = 'review_images';
|
||||
|
||||
protected $table = self::TABLE_NAME;
|
||||
|
||||
public function review()
|
||||
{
|
||||
return $this->belongsTo(Review::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class CustomerServiceProvider extends ServiceProvider
|
||||
{
|
||||
use PathNamespace;
|
||||
|
||||
protected string $name = 'Customer';
|
||||
|
||||
protected string $nameLower = 'customer';
|
||||
|
||||
/**
|
||||
* Boot the application events.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerCommands();
|
||||
$this->registerCommandSchedules();
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->loadMigrationsFrom(module_path($this->name, 'database/migrations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(EventServiceProvider::class);
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register commands in the format of Command::class
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
// $this->commands([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register command Schedules.
|
||||
*/
|
||||
protected function registerCommandSchedules(): void
|
||||
{
|
||||
// $this->app->booted(function () {
|
||||
// $schedule = $this->app->make(Schedule::class);
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.$this->nameLower);
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom($langPath);
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom(module_path($this->name, 'lang'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
|
||||
|
||||
if (is_dir($configPath)) {
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
|
||||
$segments = explode('.', $this->nameLower.'.'.$config_key);
|
||||
|
||||
// Remove duplicated adjacent segments
|
||||
$normalized = [];
|
||||
foreach ($segments as $segment) {
|
||||
if (end($normalized) !== $segment) {
|
||||
$normalized[] = $segment;
|
||||
}
|
||||
}
|
||||
|
||||
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
|
||||
|
||||
$this->publishes([$file->getPathname() => config_path($config)], 'config');
|
||||
$this->merge_config_from($file->getPathname(), $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge config from the given path recursively.
|
||||
*/
|
||||
protected function merge_config_from(string $path, string $key): void
|
||||
{
|
||||
$existing = config($key, []);
|
||||
$module_config = require $path;
|
||||
|
||||
config([$key => array_replace_recursive($existing, $module_config)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register views.
|
||||
*/
|
||||
public function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/'.$this->nameLower);
|
||||
$sourcePath = module_path($this->name, 'resources/views');
|
||||
|
||||
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
|
||||
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
|
||||
|
||||
Blade::componentNamespace(config('modules.namespace').'\\'.$this->name.'\\View\\Components', $this->nameLower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (config('view.paths') as $path) {
|
||||
if (is_dir($path.'/modules/'.$this->nameLower)) {
|
||||
$paths[] = $path.'/modules/'.$this->nameLower;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event handler mappings for the application.
|
||||
*
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
protected $listen = [];
|
||||
|
||||
/**
|
||||
* Indicates if events should be discovered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $shouldDiscoverEvents = true;
|
||||
|
||||
/**
|
||||
* Configure the proper event listeners for email verification.
|
||||
*/
|
||||
protected function configureEmailVerification(): void {}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $name = 'Customer';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php'));
|
||||
}
|
||||
}
|
||||
30
public/restaurant/Modules/Customer/composer.json
Normal file
30
public/restaurant/Modules/Customer/composer.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "nwidart/customer",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Widart",
|
||||
"email": "n.widart@gmail.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Customer\\": "app/",
|
||||
"Modules\\Customer\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\Customer\\Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\Customer\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
0
public/restaurant/Modules/Customer/config/.gitkeep
Normal file
0
public/restaurant/Modules/Customer/config/.gitkeep
Normal file
5
public/restaurant/Modules/Customer/config/config.php
Normal file
5
public/restaurant/Modules/Customer/config/config.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Customer',
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Authentication\Models\Restaurant;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('otp_verifications', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(Restaurant::class)->constrained('restaurants')->cascadeOnDelete();
|
||||
$table->string('phone');
|
||||
$table->string('otp')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('otp_verifications');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('reviews', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedBigInteger('restaurant_id')->nullable();
|
||||
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('food_item_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->tinyInteger('rating')->unsigned()->default(5);
|
||||
$table->text('comment')->nullable();
|
||||
|
||||
// moderation & visibility
|
||||
$table->boolean('is_approved')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
// future analytics
|
||||
$table->unsignedInteger('likes_count')->default(0);
|
||||
$table->unsignedInteger('reported_count')->default(0);
|
||||
|
||||
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('reviews');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('review_images', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedBigInteger('restaurant_id')->nullable();
|
||||
$table->foreignId('review_id')->constrained('reviews')->cascadeOnDelete();
|
||||
|
||||
$table->string('image_path');
|
||||
$table->tinyInteger('position')->default(1);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('review_images');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Customer\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Booking\Database\Seeders\BookingTableSeeder;
|
||||
use Modules\Restaurant\Models\Customer;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class CustomerDatabaseSeeder extends Seeder
|
||||
{
|
||||
private $customerPermissions = [
|
||||
'dashboard',
|
||||
'customer',
|
||||
'profile_index',
|
||||
'profile_edit',
|
||||
'menu_types_index',
|
||||
'menu_sections_index',
|
||||
'menu_categories_index',
|
||||
'food_categories_index',
|
||||
'food_items_index',
|
||||
'food_variants_index',
|
||||
'food_variant_ingredients_index',
|
||||
'tables_index',
|
||||
'reservations_index',
|
||||
'reservations_create',
|
||||
'reservation_unavailability_index',
|
||||
'my_orders_index',
|
||||
'my_orders_create',
|
||||
'my_orders_edit',
|
||||
'my_orders_delete',
|
||||
'my_orders_show',
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
/** ----------------------------------------------------------------
|
||||
* 1) Create Permissions for Customer Guard
|
||||
* ----------------------------------------------------------------*/
|
||||
foreach ($this->customerPermissions as $permissionName) {
|
||||
Permission::updateOrCreate([
|
||||
'name' => $permissionName,
|
||||
'guard_name' => 'customer',
|
||||
]);
|
||||
}
|
||||
|
||||
/** ----------------------------------------------------------------
|
||||
* 2) Create Customer Role
|
||||
* ----------------------------------------------------------------*/
|
||||
$customerRole = Role::updateOrCreate([
|
||||
'name' => 'Customer',
|
||||
'guard_name' => 'customer',
|
||||
'restaurant_id' => null,
|
||||
]);
|
||||
|
||||
$customerRole->syncPermissions($this->customerPermissions);
|
||||
|
||||
/** ----------------------------------------------------------------
|
||||
* 3) Walk-in Customer
|
||||
* ----------------------------------------------------------------*/
|
||||
Customer::updateOrCreate([
|
||||
'customer_code' => 'CUST-WALKIN',
|
||||
], [
|
||||
'restaurant_id' => 1,
|
||||
'name' => 'Walk-in',
|
||||
'phone' => '00123456789',
|
||||
'address' => 'Counter',
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
/** ----------------------------------------------------------------
|
||||
* 4) Demo Customers
|
||||
* ----------------------------------------------------------------*/
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$customer = Customer::updateOrCreate(
|
||||
['email' => "customer{$i}@example.com"],
|
||||
[
|
||||
'restaurant_id' => 1,
|
||||
'customer_code' => 'CUST-'.strtoupper(Str::random(6)),
|
||||
'name' => "Customer {$i}",
|
||||
'email_verified_at' => now(),
|
||||
'phone' => "013000000{$i}",
|
||||
'password' => Hash::make('password123'),
|
||||
'platform' => 'WEB',
|
||||
'address' => 'Dhaka, Bangladesh',
|
||||
'status' => 1,
|
||||
]
|
||||
);
|
||||
|
||||
// Assign role safely
|
||||
$customer->assignRole($customerRole); // ✅ pass name only
|
||||
}
|
||||
|
||||
$this->command->info('✅ Seeded 1 Walk-in + 10 demo customers successfully.');
|
||||
|
||||
/** ----------------------------------------------------------------
|
||||
* 5) OAuth Clients for Users and Customers
|
||||
* ----------------------------------------------------------------*/
|
||||
$this->createOAuthClient('users', 'User Personal Access Client');
|
||||
$this->createOAuthClient('customers', 'Customer Personal Access Client');
|
||||
|
||||
// 6) Booking Module Seeders
|
||||
$this->call([
|
||||
BookingTableSeeder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createOAuthClient(string $provider, string $name): void
|
||||
{
|
||||
$clientId = (string) Str::uuid();
|
||||
$clientSecret = Str::random(40);
|
||||
|
||||
DB::table('oauth_clients')->updateOrInsert(
|
||||
['provider' => $provider],
|
||||
[
|
||||
'id' => $clientId,
|
||||
'owner_type' => null,
|
||||
'owner_id' => null,
|
||||
'name' => $name,
|
||||
'secret' => Hash::make($clientSecret),
|
||||
'provider' => $provider,
|
||||
'redirect_uris' => '[]',
|
||||
'grant_types' => '["personal_access"]',
|
||||
'revoked' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->command->info("🔐 OAuth $provider Client ID: $clientId");
|
||||
$this->command->info("🔐 OAuth $provider Secret: $clientSecret");
|
||||
}
|
||||
}
|
||||
11
public/restaurant/Modules/Customer/module.json
Normal file
11
public/restaurant/Modules/Customer/module.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Customer",
|
||||
"alias": "customer",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Customer\\Providers\\CustomerServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
15
public/restaurant/Modules/Customer/package.json
Normal file
15
public/restaurant/Modules/Customer/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.1.2",
|
||||
"laravel-vite-plugin": "^0.7.5",
|
||||
"sass": "^1.69.5",
|
||||
"postcss": "^8.3.7",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
0
public/restaurant/Modules/Customer/routes/.gitkeep
Normal file
0
public/restaurant/Modules/Customer/routes/.gitkeep
Normal file
35
public/restaurant/Modules/Customer/routes/api.php
Normal file
35
public/restaurant/Modules/Customer/routes/api.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Customer\Http\Controllers\CustomerAuthController;
|
||||
use Modules\Customer\Http\Controllers\CustomerController;
|
||||
use Modules\Restaurant\Http\Controllers\API\FoodReviewController;
|
||||
use Modules\Restaurant\Http\Controllers\API\FoodReviewReplyController;
|
||||
|
||||
Route::post('v1/customer-login', [CustomerAuthController::class, 'customerLogin']);
|
||||
Route::post('v1/customer-otp-verify', [CustomerAuthController::class, 'customerOtpVerify']);
|
||||
Route::post('v1/customer-otp-sent', [CustomerAuthController::class, 'customerOtpSent']);
|
||||
|
||||
Route::middleware(['auth:customer'])->group(function () {
|
||||
Route::prefix('v1/customer')->group(function () {
|
||||
// Customer Profile
|
||||
Route::get('profile', [CustomerAuthController::class, 'customerProfile']);
|
||||
Route::post('update', [CustomerAuthController::class, 'customerUpdate']);
|
||||
Route::post('change-password', [CustomerAuthController::class, 'changePassword']);
|
||||
Route::post('account-delete', [CustomerAuthController::class, 'accountDeleted']);
|
||||
Route::post('logout', [CustomerAuthController::class, 'customerLogout']);
|
||||
|
||||
// Booking
|
||||
Route::get('booking', [CustomerController::class, 'myBooking']);
|
||||
Route::post('booking', [CustomerController::class, 'booking']);
|
||||
|
||||
// Customer Orders
|
||||
// Order related All things.
|
||||
Route::post('my-orders/{order}/cancel', [CustomerController::class, 'cancel']);
|
||||
Route::apiResource('my-orders', CustomerController::class);
|
||||
|
||||
// Customer Protected Routes
|
||||
Route::apiResource('customer-food-reviews', FoodReviewController::class);
|
||||
Route::apiResource('customer-reviews-replies', FoodReviewReplyController::class);
|
||||
});
|
||||
});
|
||||
57
public/restaurant/Modules/Customer/vite.config.js
Normal file
57
public/restaurant/Modules/Customer/vite.config.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join,relative,dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: '../../public/build-customer',
|
||||
emptyOutDir: true,
|
||||
manifest: true,
|
||||
},
|
||||
plugins: [
|
||||
laravel({
|
||||
publicDirectory: '../../public',
|
||||
buildDirectory: 'build-customer',
|
||||
input: [
|
||||
__dirname + '/resources/assets/sass/app.scss',
|
||||
__dirname + '/resources/assets/js/app.js'
|
||||
],
|
||||
refresh: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
// Scen all resources for assets file. Return array
|
||||
//function getFilePaths(dir) {
|
||||
// const filePaths = [];
|
||||
//
|
||||
// function walkDirectory(currentPath) {
|
||||
// const files = readdirSync(currentPath);
|
||||
// for (const file of files) {
|
||||
// const filePath = join(currentPath, file);
|
||||
// const stats = statSync(filePath);
|
||||
// if (stats.isFile() && !file.startsWith('.')) {
|
||||
// const relativePath = 'Modules/Customer/'+relative(__dirname, filePath);
|
||||
// filePaths.push(relativePath);
|
||||
// } else if (stats.isDirectory()) {
|
||||
// walkDirectory(filePath);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// walkDirectory(dir);
|
||||
// return filePaths;
|
||||
//}
|
||||
|
||||
//const __filename = fileURLToPath(import.meta.url);
|
||||
//const __dirname = dirname(__filename);
|
||||
|
||||
//const assetsDir = join(__dirname, 'resources/assets');
|
||||
//export const paths = getFilePaths(assetsDir);
|
||||
|
||||
|
||||
//export const paths = [
|
||||
// 'Modules/Customer/resources/assets/sass/app.scss',
|
||||
// 'Modules/Customer/resources/assets/js/app.js',
|
||||
//];
|
||||
Reference in New Issue
Block a user