migrate to gtea from bistbucket

This commit is contained in:
2026-03-15 17:08:23 +07:00
commit 129ca2260c
3716 changed files with 566316 additions and 0 deletions

View File

@@ -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'];
}
}

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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',
];
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 {}
}

View File

@@ -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'));
}
}