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,16 @@
<?php
namespace Modules\Authentication\Interfaces;
interface PermissionInterface
{
public function index($request, int $perPage = 50);
public function getById(int $id);
public function create(array $data);
public function update(int $id, array $data);
public function delete(int $id);
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Modules\Authentication\Interfaces;
interface RoleInterface
{
public function index($request, int $perPage = 50);
public function getById(int $id);
public function create(array $data);
public function update(int $id, array $data);
public function delete(int $id);
}

View File

@@ -0,0 +1,173 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Modules\Authentication\Models\Feedback;
use Symfony\Component\HttpFoundation\Response;
class FeedbackRepository extends EntityRepository
{
public string $table = Feedback::TABLE_NAME;
protected array $fillableColumns = [
'user_id',
'description',
'status',
'created_at',
'updated_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getFeedbackQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (isset($filter['search']) && strlen($filter['search']) > 0) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
];
return array_merge($defaultArgs, $filterData);
}
private function getFeedbackQuery(): Builder
{
return $this->getQuery()
->select(
'feedback.id',
'feedback.user_id',
'feedback.description',
'feedback.status',
'feedback.created_at'
);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where('feedback.user_id', 'LIKE', $searchable)
->orWhere('feedback.description', 'LIKE', $searchable)
->orWhere('feedback.status', 'LIKE', $searchable);
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$user = $this->getFeedbackQuery()
->where($columnName, $columnValue)
->first();
if (empty($user)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $user;
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws Exception
*/
public function create(array $data): object
{
try {
$data = $this->prepareForDB($data);
$userId = $this->getQuery()->insertGetId($data);
$user = Feedback::find($userId);
return $user;
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
$data['status'] = 1;
} else {
$data['updated_at'] = now();
}
return $data;
}
/**
* @throws Exception
*/
public function update(int $id, array $data): ?object
{
try {
$user = Feedback::find($id);
$data = $this->prepareForDB($data, $user);
parent::update($id, $data);
return $this->getById($id);
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
protected function getExceptionMessages(): array
{
$exceptionMessages = parent::getExceptionMessages();
$userExceptionMessages = [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'Feedback does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'Feedback could not be deleted.',
];
return array_merge($exceptionMessages, $userExceptionMessages);
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Modules\Authentication\Models\RestaurantImageSAASSetting;
use Symfony\Component\HttpFoundation\Response;
class RestaurantImageSAASSettingRepository extends EntityRepository
{
public string $table = RestaurantImageSAASSetting::TABLE_NAME;
protected array $fillableColumns = [
'header_logo_light_theme',
'header_logo_dark_theme',
'footer_logo_light_theme',
'footer_logo_dark_theme',
'banner_image',
'status',
'created_at',
'updated_at',
'deleted_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
];
return array_merge($defaultArgs, $filterData);
}
private function getRestaurantImageSAASSettingQuery(): Builder
{
return $this->getQuery()
->select(
"{$this->table}.id",
"{$this->table}.header_logo_light_theme",
"{$this->table}.header_logo_dark_theme",
"{$this->table}.footer_logo_light_theme",
"{$this->table}.footer_logo_dark_theme",
"{$this->table}.banner_image",
"{$this->table}.status",
"{$this->table}.created_at",
"{$this->table}.deleted_at"
);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where("{$this->table}.name", 'LIKE', $searchable)
->orWhere("{$this->table}.status", 'LIKE', $searchable);
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getRestaurantImageSAASSettingQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (! empty($filter['search'])) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$item = $this->getRestaurantImageSAASSettingQuery()
->where($columnName, $columnValue)
->first($selects);
if (empty($item)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $item;
}
/**
* @throws Exception
*/
public function create(array $data): object
{
$data = $this->prepareForDB($data);
$id = $this->getQuery()->insertGetId($data);
return RestaurantImageSAASSetting::find($id);
}
/**
* @throws Exception
*/
public function update(int $id, array $data): object
{
$item = RestaurantImageSAASSetting::findOrFail($id);
$data = $this->prepareForDB($data, $item);
parent::update($id, $data);
return $this->getById($id);
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
$data['status'] = 1;
if (! empty($data['header_logo_light_theme']) && $data['header_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_light_theme']);
}
if (! empty($data['header_logo_dark_theme']) && $data['header_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_dark_theme']);
}
if (! empty($data['footer_logo_light_theme']) && $data['footer_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_light_theme']);
}
if (! empty($data['footer_logo_dark_theme']) && $data['footer_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_dark_theme']);
}
if (! empty($data['banner_image']) && $data['banner_image'] instanceof \Illuminate\Http\UploadedFile) {
$data['banner_image'] = fileUploader('restaurant_image_settings/', 'png', $data['banner_image']);
}
} else {
if (! empty($data['header_logo_light_theme']) && $data['header_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_light_theme'], $item->header_logo_light_theme);
}
if (! empty($data['header_logo_dark_theme']) && $data['header_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_dark_theme'], $item->header_logo_dark_theme);
}
if (! empty($data['footer_logo_light_theme']) && $data['footer_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_light_theme'], $item->footer_logo_light_theme);
}
if (! empty($data['footer_logo_dark_theme']) && $data['footer_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_dark_theme'], $item->footer_logo_dark_theme);
}
if (! empty($data['banner_image']) && $data['banner_image'] instanceof \Illuminate\Http\UploadedFile) {
$data['banner_image'] = fileUploader('restaurant_image_settings/', 'png', $data['banner_image'], $item->banner_image);
}
$data['updated_at'] = now();
}
return $data;
}
protected function getExceptionMessages(): array
{
return [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'RestaurantImageSAASSetting does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'RestaurantImageSAASSetting could not be deleted.',
];
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Modules\Authentication\Models\RestaurantImageSetting;
use Symfony\Component\HttpFoundation\Response;
class RestaurantImageSettingRepository extends EntityRepository
{
public string $table = RestaurantImageSetting::TABLE_NAME;
protected array $fillableColumns = [
'restaurant_id',
'header_logo_light_theme',
'header_logo_dark_theme',
'footer_logo_light_theme',
'footer_logo_dark_theme',
'banner_image',
'status',
'created_at',
'updated_at',
'deleted_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
];
return array_merge($defaultArgs, $filterData);
}
private function getRestaurantImageSettingQuery(): Builder
{
return $this->getQuery()
->select(
"{$this->table}.id",
"{$this->table}.restaurant_id",
"{$this->table}.header_logo_light_theme",
"{$this->table}.header_logo_dark_theme",
"{$this->table}.footer_logo_light_theme",
"{$this->table}.footer_logo_dark_theme",
"{$this->table}.banner_image",
"{$this->table}.status",
"{$this->table}.created_at",
"{$this->table}.deleted_at"
);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where("{$this->table}.name", 'LIKE', $searchable)
->orWhere("{$this->table}.status", 'LIKE', $searchable);
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getRestaurantImageSettingQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (! empty($filter['search'])) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$item = $this->getRestaurantImageSettingQuery()
->where($columnName, $columnValue)
->first($selects);
if (empty($item)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $item;
}
/**
* @throws Exception
*/
public function create(array $data): object
{
$data = $this->prepareForDB($data);
$id = $this->getQuery()->insertGetId($data);
return RestaurantImageSetting::find($id);
}
/**
* @throws Exception
*/
public function update(int $id, array $data): object
{
$item = RestaurantImageSetting::findOrFail($id);
$data = $this->prepareForDB($data, $item);
parent::update($id, $data);
return $this->getById($id);
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
$data['restaurant_id'] = $this->getCurrentRestaurantId();
$data['status'] = 1;
if (! empty($data['header_logo_light_theme']) && $data['header_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_light_theme']);
}
if (! empty($data['header_logo_dark_theme']) && $data['header_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_dark_theme']);
}
if (! empty($data['footer_logo_light_theme']) && $data['footer_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_light_theme']);
}
if (! empty($data['footer_logo_dark_theme']) && $data['footer_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_dark_theme']);
}
if (! empty($data['banner_image']) && $data['banner_image'] instanceof \Illuminate\Http\UploadedFile) {
$data['banner_image'] = fileUploader('restaurant_image_settings/', 'png', $data['banner_image']);
}
} else {
if (! empty($data['header_logo_light_theme']) && $data['header_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_light_theme'], $item->header_logo_light_theme);
}
if (! empty($data['header_logo_dark_theme']) && $data['header_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['header_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['header_logo_dark_theme'], $item->header_logo_dark_theme);
}
if (! empty($data['footer_logo_light_theme']) && $data['footer_logo_light_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_light_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_light_theme'], $item->footer_logo_light_theme);
}
if (! empty($data['footer_logo_dark_theme']) && $data['footer_logo_dark_theme'] instanceof \Illuminate\Http\UploadedFile) {
$data['footer_logo_dark_theme'] = fileUploader('restaurant_image_settings/', 'png', $data['footer_logo_dark_theme'], $item->footer_logo_dark_theme);
}
if (! empty($data['banner_image']) && $data['banner_image'] instanceof \Illuminate\Http\UploadedFile) {
$data['banner_image'] = fileUploader('restaurant_image_settings/', 'png', $data['banner_image'], $item->banner_image);
}
$data['updated_at'] = now();
}
return $data;
}
protected function getExceptionMessages(): array
{
return [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'RestaurantImageSetting does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'RestaurantImageSetting could not be deleted.',
];
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Services\RestaurantSetupService;
use Symfony\Component\HttpFoundation\Response;
class RestaurantRepository extends EntityRepository
{
public string $table = Restaurant::TABLE_NAME;
protected array $fillableColumns = [
'owner_id',
'assigned_to',
'name',
'email',
'address',
'restaurant_type',
'phone',
'domain',
'platform',
'last_active_time',
'logo',
'status',
'theme_id',
'created_at',
'updated_at',
'deleted_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getRestaurantQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (isset($filter['search']) && strlen($filter['search']) > 0) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
];
return array_merge($defaultArgs, $filterData);
}
private function getRestaurantQuery(): Builder
{
return $this->getQuery()
->leftJoin('users as owner', 'restaurants.owner_id', '=', 'owner.id')
->leftJoin('users as assigned', 'restaurants.assigned_to', '=', 'assigned.id')
->leftJoin('themes', 'restaurants.theme_id', '=', 'themes.id')
->select([
'restaurants.id',
'restaurants.owner_id',
'owner.first_name as owner_name',
'restaurants.assigned_to',
'assigned.first_name as assigned_name',
'restaurants.name',
'restaurants.email',
'restaurants.address',
'restaurants.restaurant_type',
'restaurants.phone',
'restaurants.domain',
'restaurants.platform',
'restaurants.last_active_time',
'restaurants.logo',
'restaurants.status',
'restaurants.theme_id',
'themes.name as theme_name',
'restaurants.created_at',
'restaurants.deleted_at',
]);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where('restaurants.name', 'LIKE', $searchable)
->orWhere('restaurants.email', 'LIKE', $searchable)
->orWhere('restaurants.restaurant_type', 'LIKE', $searchable)
->orWhere('restaurants.phone', 'LIKE', $searchable)
->orWhere('restaurants.domain', 'LIKE', $searchable);
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$user = $this->getRestaurantQuery()
->where($columnName, $columnValue)
->first();
if (empty($user)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $user;
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws \Exception
*/
public function create(array $data): object
{
try {
return DB::transaction(function () use ($data) {
// Step 1: Prepare data for DB (handles file uploads, default timestamps, owner_id, etc.)
$data = $this->prepareForDB($data);
// Step 2: Insert restaurant and get the ID
$restaurantId = $this->getQuery()->insertGetId($data);
$restaurant = Restaurant::find($restaurantId);
// Step 3: Owner info from currently authenticated user
$authUser = Auth::user();
$ownerData = [
'name' => $authUser->first_name,
'email' => $authUser->email,
'phone' => $authUser->phone,
'avatar' => $authUser->avatar,
'package_id' => $data['package_id'] ?? null,
'price' => $data['price'] ?? null,
];
// Step 4: Setup restaurant (owner assignment, settings, subscription, domain)
RestaurantSetupService::setup($restaurant, $ownerData, false);
// Step 5: Return the restaurant object
return $restaurant;
});
} catch (Exception $exception) {
// Rollback is automatic on exception in transaction closure
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
$data['owner_id'] = getUserId();
$data['assigned_to'] = $this->getCurrentUserId();
$data['created_by'] = $this->getCurrentUserId();
$data['status'] = 1;
if (! empty($data['logo']) && $data['logo'] instanceof \Illuminate\Http\UploadedFile) {
$data['logo'] = fileUploader('restaurants/', 'png', $data['logo']);
}
} else {
if (! empty($data['logo']) && $data['logo'] instanceof \Illuminate\Http\UploadedFile) {
$data['logo'] = fileUploader('restaurants/', 'png', $data['logo'], $item->logo);
}
$data['updated_at'] = now();
}
return $data;
}
/**
* @throws Exception
*/
public function update(int $id, array $data): ?object
{
try {
$restaurant = Restaurant::find($id);
$data = $this->prepareForDB($data, $restaurant);
parent::update($id, $data);
return $this->getById($id);
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
protected function getExceptionMessages(): array
{
$exceptionMessages = parent::getExceptionMessages();
$userExceptionMessages = [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'Restaurant does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'Restaurant could not be deleted.',
];
return array_merge($exceptionMessages, $userExceptionMessages);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Modules\Authentication\Models\SAASFaq;
use Symfony\Component\HttpFoundation\Response;
class SAASFaqRepository extends EntityRepository
{
public string $table = SAASFaq::TABLE_NAME;
protected array $fillableColumns = [
'question',
'answer',
'status',
'created_by',
'created_at',
'updated_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getSAASFaqQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (isset($filter['search']) && strlen($filter['search']) > 0) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
];
return array_merge($defaultArgs, $filterData);
}
private function getSAASFaqQuery(): Builder
{
return $this->getQuery()
->select(
's_a_a_s_faqs.id',
's_a_a_s_faqs.question',
's_a_a_s_faqs.answer',
's_a_a_s_faqs.status',
's_a_a_s_faqs.created_at'
);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where('s_a_a_s_faqs.question', 'LIKE', $searchable)
->orWhere('s_a_a_s_faqs.answer', 'LIKE', $searchable)
->orWhere('s_a_a_s_faqs.status', 'LIKE', $searchable);
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$user = $this->getSAASFaqQuery()
->where($columnName, $columnValue)
->first();
if (empty($user)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $user;
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws Exception
*/
public function create(array $data): object
{
try {
$data = $this->prepareForDB($data);
$userId = $this->getQuery()->insertGetId($data);
$user = SAASFaq::find($userId);
return $user;
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
$data['created_by'] = $this->getCurrentUserId();
$data['status'] = 1;
} else {
$data['updated_at'] = now();
}
return $data;
}
/**
* @throws Exception
*/
public function update(int $id, array $data): ?object
{
try {
$user = SAASFaq::find($id);
$data = $this->prepareForDB($data, $user);
parent::update($id, $data);
return $this->getById($id);
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
protected function getExceptionMessages(): array
{
$exceptionMessages = parent::getExceptionMessages();
$userExceptionMessages = [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'SAASFaq does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'SAASFaq could not be deleted.',
];
return array_merge($exceptionMessages, $userExceptionMessages);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Modules\Authentication\Models\SAASSubscription;
use Symfony\Component\HttpFoundation\Response;
class SAASSubscriptionRepository extends EntityRepository
{
public string $table = SAASSubscription::TABLE_NAME;
protected array $fillableColumns = [
'email',
'created_at',
'updated_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getSAASSubscriptionQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (isset($filter['search']) && strlen($filter['search']) > 0) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
];
return array_merge($defaultArgs, $filterData);
}
private function getSAASSubscriptionQuery(): Builder
{
return $this->getQuery()
->select(
's_a_a_s_subscriptions.id',
's_a_a_s_subscriptions.email',
's_a_a_s_subscriptions.created_at'
);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where('s_a_a_s_subscriptions.email', 'LIKE', $searchable)
->orWhere('s_a_a_s_subscriptions.created_at', 'LIKE', $searchable);
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$user = $this->getSAASSubscriptionQuery()
->where($columnName, $columnValue)
->first();
if (empty($user)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $user;
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws Exception
*/
public function create(array $data): object
{
try {
$data = $this->prepareForDB($data);
$userId = $this->getQuery()->insertGetId($data);
$user = SAASSubscription::find($userId);
return $user;
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
} else {
$data['updated_at'] = now();
}
return $data;
}
/**
* @throws Exception
*/
public function update(int $id, array $data): ?object
{
try {
$user = SAASSubscription::find($id);
$data = $this->prepareForDB($data, $user);
parent::update($id, $data);
return $this->getById($id);
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
protected function getExceptionMessages(): array
{
$exceptionMessages = parent::getExceptionMessages();
$userExceptionMessages = [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'SAASSubscription does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'SAASSubscription could not be deleted.',
];
return array_merge($exceptionMessages, $userExceptionMessages);
}
}

View File

@@ -0,0 +1,289 @@
<?php
namespace Modules\Authentication\Repositories;
use App\Abstracts\EntityRepository;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Hash;
use Modules\Authentication\Models\User;
use Symfony\Component\HttpFoundation\Response;
class UserRepository extends EntityRepository
{
public string $table = User::TABLE_NAME;
protected array $fillableColumns = [
'restaurant_id',
'first_name',
'last_name',
'username',
'email',
'phone',
'password',
'otp_code',
'isVerified',
'email_verified_at',
'address',
'avatar',
'role_id',
'user_type',
'facebook',
'twitter',
'linkedin',
'google_plus',
'nid',
'platform',
'device_info',
'last_active_time',
'status',
'bio',
'fcm_token',
'qr_code',
'department_id',
'designation_id',
'salary_type_id',
'address',
'gender',
'date_of_birth',
'joining_date',
'basic_salary',
'created_by',
'deleted_at',
];
protected function getQuery(): Builder
{
return parent::getQuery();
}
public function getAll(array $filterData = []): Paginator
{
$filter = $this->getFilterData($filterData);
$query = $this->getUserQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
if (isset($filter['search']) && strlen($filter['search']) > 0) {
$query = $this->filterSearchQuery($query, $filter['search']);
}
if (! empty($filter['user_type'])) {
$query->where("{$this->table}.user_type", $filter['user_type']);
}
return $query
->orderBy($filter['orderBy'], $filter['order'])
->paginate($filter['perPage']);
}
protected function getFilterData(array $filterData = []): array
{
$defaultArgs = [
'perPage' => 10,
'search' => '',
'orderBy' => 'id',
'order' => 'desc',
'with_deleted' => false,
'user_type' => $filterData['user_type'] ?? null, // ⬅️ ADD THIS
];
return array_merge($defaultArgs, $filterData);
}
private function getUserQuery(): Builder
{
return $this->getQuery()
->whereNotIn('id', [3])
->select(
'users.id',
'users.restaurant_id',
'users.first_name',
'users.last_name',
'users.username',
'users.email',
'users.phone',
'users.isVerified',
'users.email_verified_at',
'users.address',
'users.avatar',
'users.role_id',
'users.user_type',
'users.facebook',
'users.twitter',
'users.linkedin',
'users.google_plus',
'users.nid',
'users.platform',
'users.device_info',
'users.last_active_time',
'users.status',
'users.bio',
'users.fcm_token',
'users.qr_code',
'users.department_id',
'users.designation_id',
'users.salary_type_id',
'users.address',
'users.gender',
'users.date_of_birth',
'users.joining_date',
'users.basic_salary',
'users.created_by',
'users.deleted_at'
);
}
protected function filterSearchQuery(Builder|EloquentBuilder $query, string $searchedText): Builder
{
$searchable = "%$searchedText%";
return $query->where('users.first_name', 'LIKE', $searchable)
->orWhere('users.last_name', 'LIKE', $searchable)
->orWhere('users.email', 'LIKE', $searchable)
->orWhere('users.phone', 'LIKE', $searchable)
->orWhere('users.status', 'LIKE', $searchable);
}
/**
* @throws Exception
*/
public function getByColumn(string $columnName, $columnValue, array $selects = ['*']): ?object
{
$user = $this->getUserQuery()
->where($columnName, $columnValue)
->first();
if (empty($user)) {
throw new Exception(
$this->getExceptionMessage(static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE),
Response::HTTP_NOT_FOUND
);
}
return $user;
}
public function getCount(array $filterData = []): int
{
$filter = $this->getFilterData($filterData);
$query = $this->getQuery();
if (! $filter['with_deleted']) {
$query->whereNull("{$this->table}.deleted_at");
}
return $query->count();
}
/**
* @throws Exception
*/
public function create(array $data): object
{
try {
$data['password'] = empty($data['password']) ? '123456' : $data['password'];
$data = $this->prepareForDB($data);
$userId = $this->getQuery()->insertGetId($data);
$user = User::find($userId);
return $user;
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws Exception
*/
public function prepareForDB(array $data, ?object $item = null): array
{
$data = parent::prepareForDB($data, $item);
if (empty($item)) {
$data['created_at'] = now();
$data['created_by'] = $this->getCurrentUserId();
$data['restaurant_id'] = $this->getCurrentRestaurantId();
$data['status'] = 1;
if (! empty($data['avatar']) && $data['avatar'] instanceof \Illuminate\Http\UploadedFile) {
$data['avatar'] = fileUploader('users/', 'png', $data['avatar']);
}
} else {
if (! empty($data['avatar']) && $data['avatar'] instanceof \Illuminate\Http\UploadedFile) {
$data['avatar'] = fileUploader('users/', 'png', $data['avatar'], $item->avatar);
}
$data['updated_at'] = now();
}
if (! empty($data['password'])) {
$data['password'] = Hash::make($data['password']);
} else {
unset($data['password']);
}
return $data;
}
/**
* @throws Exception
*/
public function update(int $id, array $data): ?object
{
try {
$user = User::find($id);
$data = $this->prepareForDB($data, $user);
parent::update($id, $data);
return $this->getById($user->id);
} catch (Exception $exception) {
throw new Exception($exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws Exception
*/
public function getDropdown(): array
{
$dropdowns = parent::getDropdown();
$dropdownsData = [];
foreach ($dropdowns as $dropdownItem) {
$dropdownItem->name = $dropdownItem->first_name.' '.$dropdownItem->last_name.' #'.$dropdownItem->id;
unset($dropdownItem->first_name, $dropdownItem->last_name, $dropdownItem->phone);
$dropdownsData[] = $dropdownItem;
}
return $dropdownsData;
}
protected function getExceptionMessages(): array
{
$exceptionMessages = parent::getExceptionMessages();
$userExceptionMessages = [
static::MESSAGE_ITEM_DOES_NOT_EXIST_MESSAGE => 'User does not exist.',
static::MESSAGE_ITEM_COULD_NOT_BE_DELETED => 'User could not be deleted.',
];
return array_merge($exceptionMessages, $userExceptionMessages);
}
protected function getDropdownSelectableColumns(): array
{
return [
'first_name',
'last_name',
];
}
protected function getOrderByColumnWithOrders(): array
{
return [
'first_name' => 'asc',
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Modules\Authentication\Services;
use Modules\Authentication\Interfaces\PermissionInterface;
use Spatie\Permission\Models\Permission;
final class PermissionService implements PermissionInterface
{
public function __construct(
protected Permission $model
) {}
public function index($request, int $perPage = 50)
{
$orderColumn = request('sort_column', 'id');
$orderDirection = request('sort_direction', 'desc');
if (! in_array($orderColumn, ['id', 'name', 'created_at'])) {
$orderColumn = 'id';
}
if (! in_array($orderDirection, ['asc', 'desc'])) {
$orderDirection = 'desc';
}
return $this->model::query()
->when($request->search, function ($query) use ($request) {
$query->where('name', 'like', $request->search.'%');
})
->orderBy($orderColumn, $orderDirection)
->paginate($perPage);
}
public function getAll($request)
{
$orderColumn = request('sort_column', 'id');
$orderDirection = request('sort_direction', 'desc');
if (! in_array($orderColumn, ['id', 'name', 'created_at'])) {
$orderColumn = 'id';
}
if (! in_array($orderDirection, ['asc', 'desc'])) {
$orderDirection = 'desc';
}
return $this->model::query()
->when($request->search, function ($query) use ($request) {
$query->where('name', 'like', $request->search.'%');
})
->orderBy($orderColumn, $orderDirection)
->get();
}
public function getById(int $id)
{
return $this->model::where('id', $id)->first();
}
public function create(array $data)
{
return $this->model::create($data);
}
public function update(int $id, array $data)
{
$model = $this->model::where('id', $id)->first();
$model->update($data);
return $model;
}
public function delete(int $id)
{
$model = $this->model::where('id', $id)->first();
return $model->delete();
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace Modules\Authentication\Services;
use App\Mail\RestaurantOnboardingMail;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Modules\Authentication\Models\Package;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Models\Subscription;
use Modules\Authentication\Models\SubscriptionItem;
use Modules\Authentication\Models\User;
class RestaurantSetupService
{
/**
* Initialize restaurant related setup including default settings, subscriptions, and domain.
*
* @param array $ownerData
* Expected keys: name, email, phone, password, avatar (optional), package_id, price
*/
public static function setup(Restaurant $restaurant, array $ownerData, bool $sendMail = true): User
{
// Step 1: Check if user already exists
$user = User::where('email', $ownerData['email'])->first();
if (! $user) {
// Create a new user only if not exists
$user = User::create([
'first_name' => $ownerData['name'],
'email' => $ownerData['email'],
'phone' => $ownerData['phone'],
'password' => Hash::make($ownerData['password']),
'avatar' => $ownerData['avatar'] ?? null,
'role_id' => 2,
'user_type' => 'System Admin',
]);
// Assign role
$role = \Spatie\Permission\Models\Role::find(2);
$user->assignRole($role);
}
// Step 2: Assign this user as restaurant owner
$restaurant->owner_id = $user->id;
$restaurant->save();
// Step 3: Insert default settings
self::insertDefaultSettings($restaurant);
// Step 4: Create Subscription & SubscriptionItem if package_id is provided
$package = Package::first();
$startDate = Carbon::now()->startOfDay();
$endDate = Carbon::now()->addDays($package->duration - 1)->endOfDay();
// Create subscription
$subscription = Subscription::updateOrCreate([
'user_id' => $user->id,
'restaurant_id' => $restaurant->id,
'package_id' => $package->id,
], [
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
]);
// Create subscription item
SubscriptionItem::updateOrCreate([
'user_id' => $user->id,
'restaurant_id' => $restaurant->id,
'subscription_id' => $subscription->id,
'package_id' => $package->id,
], [
'amount' => (float) $package->price,
'start_date' => $startDate,
'end_date' => $endDate,
]);
// Step 5: Add domain to Vercel if restaurant domain exists
if (! empty($restaurant->domain)) {
try {
$vercelDomainResponse = self::addDomainToVercel($restaurant->domain);
if (! $vercelDomainResponse['success']) {
Log::warning('Vercel domain add failed', [
'domain' => $restaurant->domain,
'response' => $vercelDomainResponse,
]);
}
} catch (Exception $e) {
Log::error('Vercel domain add exception: '.$e->getMessage());
}
}
// Step 6: Send onboarding mail only if user is new
if ($sendMail && $user->wasRecentlyCreated) {
Mail::to($ownerData['email'])->send(new RestaurantOnboardingMail([
'restaurant_name' => $restaurant->name,
'restaurant_id' => $restaurant->id,
'email' => $ownerData['email'],
'password' => $ownerData['password'],
'package_name' => $package->name ?? 'Trial Package',
'trial_days' => $package->duration ?? 14,
'trial_end_date' => now()->addDays($package->duration ?? 14)->format('Y-m-d'),
'login_url' => url('/login'),
]));
}
return $user;
}
/**
* Example placeholder for adding domain to Vercel
*
* @param string $domain
* @return array
*/
public static function addDomainToVercel($fullDomain)
{
$client = new Client;
$projectId = get_saas_option('vercel_project_id');
$token = get_saas_option('vercel_token');
$url = "https://api.vercel.com/v9/projects/{$projectId}/domains";
try {
$response = $client->post($url, [
'headers' => [
'Authorization' => 'Bearer '.$token,
'Content-Type' => 'application/json',
],
'json' => [
'name' => $fullDomain,
],
]);
return [
'success' => true,
'data' => json_decode($response->getBody(), true),
];
} catch (\GuzzleHttp\Exception\ClientException $e) {
return [
'success' => false,
'error_type' => 'client',
'message' => $e->getMessage(),
];
} catch (Exception $e) {
return [
'success' => false,
'error_type' => 'general',
'message' => $e->getMessage(),
];
}
}
private static function insertDefaultSettings(Restaurant $restaurant)
{
DB::table('settings')->insert([
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'restaurant_name', 'option_value' => 'Restaurant'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'site_title', 'option_value' => 'restaurant'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'phone', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'email', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'language', 'option_value' => 'en'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'google_map', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'address', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'on_google_map', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'restaurant_code', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'timezone', 'option_value' => 'Asia/Dhaka'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'currency_symbol', 'option_value' => '$'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'mail_type', 'option_value' => 'mail'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'logo', 'option_value' => 'logo.png'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'disabled_website', 'option_value' => 'no'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'copyright_text', 'option_value' => '&copy; Copyright 2025. All Rights Reserved by FueDevs LTD'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'facebook_link', 'option_value' => 'https://www.facebook.com/'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'google_plus_link', 'option_value' => 'https://www.google.com/'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'youtube_link', 'option_value' => 'https://www.youtube.com/'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'whats_app_link', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'twitter_link', 'option_value' => 'https://www.twitter.com'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'eiin_code', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'sms_gateway', 'option_value' => 'twilio'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'bulk_sms_api_key', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'bulk_sms_sender_id', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'twilio_sid', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'twilio_token', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'twilio_from_number', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'header_notice', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'app_version', 'option_value' => '1.0.0'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'app_url', 'option_value' => 'drive-link'],
// 🎨 Restaurant Identity & Display
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'tagline', 'option_value' => 'Delicious Food, Fresh Taste'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'favicon', 'option_value' => 'favicon.png'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'theme_color', 'option_value' => '#ff6b00'],
['restaurant_id' => $restaurant->id, 'type' => 'general', 'option_key' => 'background_image', 'option_value' => 'bg.jpg'],
// 💰 Finance / POS / Invoice
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'tax_type', 'option_value' => 'exclusive'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'tax_percentage', 'option_value' => '10'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'service_charge', 'option_value' => '5'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'default_currency', 'option_value' => 'USD'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'billing_prefix', 'option_value' => 'INV-'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'invoice_footer', 'option_value' => 'Thank you! Visit again.'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'enable_kitchen_print', 'option_value' => 'yes'],
['restaurant_id' => $restaurant->id, 'type' => 'pos', 'option_key' => 'enable_customer_copy', 'option_value' => 'yes'],
// 🚚 Online Ordering & Delivery
['restaurant_id' => $restaurant->id, 'type' => 'order', 'option_key' => 'enable_online_order', 'option_value' => 'yes'],
['restaurant_id' => $restaurant->id, 'type' => 'order', 'option_key' => 'delivery_charge', 'option_value' => '50'],
['restaurant_id' => $restaurant->id, 'type' => 'order', 'option_key' => 'minimum_order_amount', 'option_value' => '100'],
['restaurant_id' => $restaurant->id, 'type' => 'order', 'option_key' => 'auto_accept_order', 'option_value' => 'no'],
['restaurant_id' => $restaurant->id, 'type' => 'order', 'option_key' => 'estimated_preparation_time', 'option_value' => '30'],
// 🔔 Notifications / Integrations
['restaurant_id' => $restaurant->id, 'type' => 'integration', 'option_key' => 'slack_webhook_url', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'integration', 'option_key' => 'telegram_bot_token', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'integration', 'option_key' => 'telegram_chat_id', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'integration', 'option_key' => 'twilio_sms_enabled', 'option_value' => 'yes'],
['restaurant_id' => $restaurant->id, 'type' => 'integration', 'option_key' => 'email_notifications', 'option_value' => 'yes'],
['restaurant_id' => $restaurant->id, 'type' => 'integration', 'option_key' => 'whatsapp_notifications', 'option_value' => 'no'],
// 🧾 Reports & Logs
['restaurant_id' => $restaurant->id, 'type' => 'system', 'option_key' => 'auto_backup', 'option_value' => 'daily'],
['restaurant_id' => $restaurant->id, 'type' => 'system', 'option_key' => 'report_timezone', 'option_value' => 'Asia/Dhaka'],
['restaurant_id' => $restaurant->id, 'type' => 'system', 'option_key' => 'data_retention_days', 'option_value' => '365'],
// 💻 UI/UX Preferences
['restaurant_id' => $restaurant->id, 'type' => 'ui', 'option_key' => 'sidebar_collapsed', 'option_value' => 'no'],
['restaurant_id' => $restaurant->id, 'type' => 'ui', 'option_key' => 'dark_mode', 'option_value' => 'no'],
['restaurant_id' => $restaurant->id, 'type' => 'ui', 'option_key' => 'default_dashboard', 'option_value' => 'sales'],
// 💳 Payment Gateways
['restaurant_id' => $restaurant->id, 'type' => 'payment', 'option_key' => 'razorpay_key', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'payment', 'option_key' => 'razorpay_secret', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'payment', 'option_key' => 'stripe_key', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'payment', 'option_key' => 'stripe_secret', 'option_value' => ''],
['restaurant_id' => $restaurant->id, 'type' => 'payment', 'option_key' => 'cash_on_delivery', 'option_value' => 'yes'],
// 👨‍🍳 Kitchen & Staff
['restaurant_id' => $restaurant->id, 'type' => 'staff', 'option_key' => 'max_table_capacity', 'option_value' => '10'],
['restaurant_id' => $restaurant->id, 'type' => 'staff', 'option_key' => 'default_shift_start', 'option_value' => '09:00'],
['restaurant_id' => $restaurant->id, 'type' => 'staff', 'option_key' => 'default_shift_end', 'option_value' => '23:00'],
['restaurant_id' => $restaurant->id, 'type' => 'staff', 'option_key' => 'auto_logout_idle_minutes', 'option_value' => '60'],
// 🎨 Color Combination
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'primary_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'secondary_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'primary_container_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'dark_primary_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'dark_secondary_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'dark_container_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'text_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'dark_text_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'sidebar_selected_bg_color', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'system_color', 'option_key' => 'sidebar_selected_text_color', 'option_value' => null],
// 🚚 Delivery / Online features
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'is_online', 'option_value' => false],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'latitude', 'option_value' => '23.8103'],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'longitude', 'option_value' => '90.4125'],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'delivery_radius_km', 'option_value' => 5.00],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'delivery_fee', 'option_value' => 0],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'delivery_partner_count', 'option_value' => 0],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'delivery_time_avg', 'option_value' => 30],
['restaurant_id' => $restaurant->id, 'type' => 'delivery', 'option_key' => 'pickup_enabled', 'option_value' => true],
// 🕒 Operational hours & capacity
['restaurant_id' => $restaurant->id, 'type' => 'operational', 'option_key' => 'opening_time', 'option_value' => '09:00:00'],
['restaurant_id' => $restaurant->id, 'type' => 'operational', 'option_key' => 'closing_time', 'option_value' => '22:00:00'],
['restaurant_id' => $restaurant->id, 'type' => 'operational', 'option_key' => 'auto_accept_orders', 'option_value' => false],
['restaurant_id' => $restaurant->id, 'type' => 'operational', 'option_key' => 'pre_order_enabled', 'option_value' => false],
['restaurant_id' => $restaurant->id, 'type' => 'operational', 'option_key' => 'max_order_capacity', 'option_value' => 50],
// 📊 Analytics / reviews
['restaurant_id' => $restaurant->id, 'type' => 'analytics', 'option_key' => 'avg_rating', 'option_value' => 0.00],
['restaurant_id' => $restaurant->id, 'type' => 'analytics', 'option_key' => 'review_count', 'option_value' => 0],
['restaurant_id' => $restaurant->id, 'type' => 'analytics', 'option_key' => 'total_orders', 'option_value' => 0],
['restaurant_id' => $restaurant->id, 'type' => 'analytics', 'option_key' => 'last_order_time', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'analytics', 'option_key' => 'last_active_time', 'option_value' => null],
// 🎯 Marketing & social
['restaurant_id' => $restaurant->id, 'type' => 'marketing', 'option_key' => 'loyalty_points_enabled', 'option_value' => false],
['restaurant_id' => $restaurant->id, 'type' => 'marketing', 'option_key' => 'offers_enabled', 'option_value' => false],
['restaurant_id' => $restaurant->id, 'type' => 'marketing', 'option_key' => 'social_media_links', 'option_value' => json_encode([
'facebook' => '',
'instagram' => '',
'twitter' => '',
'linkedin' => '',
])],
// 💻 Future-proof / extra
['restaurant_id' => $restaurant->id, 'type' => 'system', 'option_key' => 'settings', 'option_value' => json_encode([])],
['restaurant_id' => $restaurant->id, 'type' => 'system', 'option_key' => 'uuid', 'option_value' => uniqid('rest_')],
['restaurant_id' => $restaurant->id, 'type' => 'email_config', 'option_key' => 'email_smtp_host', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'email_config', 'option_key' => 'email_smtp_port', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'email_config', 'option_key' => 'email_smtp_username', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'email_config', 'option_key' => 'email_smtp_password', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'email_config', 'option_key' => 'email_smtp_encryption', 'option_value' => null],
// SMS Config
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'twilio_api_key', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'twilio_api_secret', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'twilio_sender_id', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'twilio_api_url', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'twilio_is_default', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'nexmo_api_key', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'nexmo_api_secret', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'nexmo_sender_id', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'nexmo_api_url', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'nexmo_is_default', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'muthofun_api_key', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'smsglobal_api_key', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'smsglobal_api_secret', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'smsglobal_sender_id', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'smsglobal_api_url', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'smsglobal_extra_key', 'option_value' => null],
['restaurant_id' => $restaurant->id, 'type' => 'sms_config', 'option_key' => 'smsglobal_is_default', 'option_value' => null],
]);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Modules\Authentication\Services;
use App\Traits\Authenticatable;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Interfaces\RoleInterface;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
final class RoleService implements RoleInterface
{
use Authenticatable;
public function __construct(
protected Role $model
) {}
public function index($request, int $perPage = 50)
{
$orderColumn = request('sort_column', 'id');
$orderDirection = request('sort_direction', 'desc');
if (! in_array($orderColumn, ['id', 'name', 'created_at'])) {
$orderColumn = 'id';
}
if (! in_array($orderDirection, ['asc', 'desc'])) {
$orderDirection = 'desc';
}
return $this->model::query()
// ->where('restaurant_id', auth()->user()->restaurant_id)
->with(['permissions'])
->when($request->search, function ($query) use ($request) {
$query->where('name', 'like', $request->search.'%');
})
->orderBy($orderColumn, $orderDirection)
->paginate($perPage);
}
public function getById(int $id)
{
return $this->model::where('id', $id)->first();
}
public function create(array $data)
{
$role = $this->model::create([
'name' => $data['name'],
'restaurant_id' => $this->getCurrentRestaurantId(),
'guard_name' => 'web',
'description' => $data['description'] ?? null,
]);
$role->permissions()->sync($data['permissions']);
return $role;
}
public function update(int $id, array $data)
{
$model = $this->getById($id);
$model->update($data);
if (! empty($data['permissions'])) {
$permissions = Permission::whereIn('id', $data['permissions'])->pluck('name')->toArray();
$model->syncPermissions($permissions);
}
return $model;
}
public function delete(int $id)
{
return DB::table('roles')->where('id', $id)->delete();
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Models\User;
use Modules\HRM\Models\Attendance;
use Modules\HRM\Models\AttendanceLog;
class AttendanceSyncController extends Controller
{
public function allUsersGet(Request $request)
{
$request->validate([
'restaurant_id' => 'required|integer|exists:restaurants,id',
]);
$users = User::select('id', 'first_name', 'last_name', 'user_type', 'role_id')
->where('restaurant_id', (int) $request->restaurant_id)
->get();
return $this->responseSuccess(
$users,
_lang('Users have been fetched successfully.')
);
}
public function sync(Request $request)
{
$request->validate([
'logs' => ['required', 'array'],
'logs.*.id' => ['required', 'integer'],
'logs.*.timestamp' => ['required', 'date'],
'logs.*.type' => ['required', 'integer'],
]);
$restaurantId = 1; // or from device
$inserted = 0;
$skipped = 0;
DB::beginTransaction();
try {
foreach ($request->logs as $log) {
$employeeId = $log['id'];
$dateTime = Carbon::parse($log['timestamp']);
$date = $dateTime->toDateString();
$type = $this->mapPunchType($log['type']);
/**
* 1️⃣ Avoid duplicate punch
*/
$exists = AttendanceLog::where([
'employee_id' => $employeeId,
'restaurant_id' => $restaurantId,
'punch_time' => $dateTime,
'type' => $type,
])->exists();
if ($exists) {
$skipped++;
continue;
}
/**
* 2️⃣ Get or create daily attendance
*/
$attendance = Attendance::firstOrCreate(
[
'employee_id' => $employeeId,
'date' => $date,
],
[
'restaurant_id' => $restaurantId,
'status' => 'present',
]
);
/**
* 3️⃣ Insert punch log
*/
AttendanceLog::create([
'attendance_id' => $attendance->id,
'employee_id' => $employeeId,
'restaurant_id' => $restaurantId,
'type' => $type,
'punch_time' => $dateTime,
]);
/**
* 4️⃣ Recalculate attendance summary
*/
$this->recalculateWorkedHours($attendance);
$inserted++;
}
DB::commit();
return $this->responseSuccess([
'inserted' => $inserted,
'skipped' => $skipped,
'message' => 'Attendance synced successfully',
], 'Profile fetched successfully.');
} catch (\Throwable $e) {
DB::rollBack();
return $this->responseError([
'message' => 'Attendance sync failed',
'error' => $e->getMessage(),
], $e->getMessage());
}
}
private function mapPunchType(int $type): string
{
return in_array($type, [0, 4]) ? 'in' : 'out';
}
/**
* RECALCULATE TOTAL WORKED HOURS BASED ON ALL LOGS
*/
private function recalculateWorkedHours(Attendance $attendance)
{
$logs = AttendanceLog::where('attendance_id', $attendance->id)
->orderBy('punch_time')
->get();
if ($logs->count() < 2) {
return;
}
$first = Carbon::parse($logs->first()->punch_time);
$last = Carbon::parse($logs->last()->punch_time);
$hours = round($first->diffInMinutes($last) / 60, 2);
$attendance->update([
'first_clock_in' => $first,
'last_clock_out' => $last,
'hours_worked' => $hours,
]);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\ResponseTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Modules\Authentication\Models\User;
class AuthController extends Controller
{
use ResponseTrait;
public function user(Request $request): JsonResponse
{
try {
$user = User::where('id', Auth::id())->first();
if ($user) {
$permissions = $user->role?->permissions->pluck('name')->toArray() ?? []; // Convert permissions to an array
$user = [
'id' => $user->id,
'name' => $user->first_name.' '.$user->last_name,
'email' => $user->email,
'phone' => $user->phone,
'image' => $user->avatar,
'role' => $user->role?->name ?? 'no-role-assign',
'status' => $user->status,
'restaurant_info' => $user?->restaurant,
'rider_info' => $user?->rider,
'permissions' => $permissions,
];
return $this->responseSuccess($user, '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 login(Request $request): JsonResponse
{
$user = User::where('phone', $request->phone)->with('rider')->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
return $this->responseError([], 'The provided credentials are incorrect..', 403);
}
$accessToken = $user->createToken('app')->accessToken;
return $this->responseSuccess([
'user' => $user,
'access_token' => $accessToken,
'check' => 'Droplet Update',
]);
}
public function profileUpdate(Request $request): JsonResponse
{
try {
// ✅ Validate input
$request->validate([
'name' => 'nullable|string|max:255',
'email' => 'nullable|email|max:255',
'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg,webp|max:2048',
]);
$user = Auth::user();
$updateData = [];
// 🧠 Update name if provided
if ($request->filled('name')) {
$updateData['first_name'] = $request->input('name');
$updateData['last_name'] = null;
}
// 🧠 Update email if provided
if ($request->filled('email')) {
$updateData['email'] = $request->input('email');
}
// 🧹 Handle avatar upload using fileUploader()
if ($request->hasFile('avatar')) {
// 🔁 Delete old avatar if exists
if (
$user->avatar &&
Storage::disk('public')->exists('users/'.$user->avatar)
) {
Storage::disk('public')->delete('users/'.$user->avatar);
}
// 💾 Use custom uploader and store only the filename
$filename = fileUploader('users/', 'png', $request->file('avatar')); // should return filename only
$updateData['avatar'] = $filename;
}
// 💾 Perform update
if (! empty($updateData)) {
$user->update($updateData);
}
return $this->responseSuccess([
'name' => $user->first_name.$user->last_name,
'email' => $user->email,
'avatar' => $user->avatar, // ✅ manual full URL from filename
], 'Profile updated successfully.');
} catch (ValidationException $e) {
return $this->responseError($e->errors(), 'Validation failed.', 422);
} catch (Exception $e) {
return $this->responseError([], 'Something went wrong.', 500);
}
}
public function changePassword(Request $request): JsonResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return $this->responseSuccess([], 'Password Changes Successfully Done.');
}
public function logout(Request $request)
{
try {
$result = $request->user()->token()->revoke();
if ($result) {
return $this->responseSuccess([], 'Logout Success');
}
} catch (Exception $e) {
return $this->responseSuccess([], 'Logout Failed');
}
}
}

View File

@@ -0,0 +1,253 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Mail\BillingInvoiceMail;
use App\Traits\Authenticatable;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Models\Subscription;
use Modules\Authentication\Models\SubscriptionItem;
class BillingController extends Controller
{
use Authenticatable;
public function expiringSoon(Request $request): JsonResponse
{
try {
$days = $request->get('days', 7); // default next 7 days
$query = Subscription::with([
'package:id,name',
'restaurant:id,name', // include restaurant info
'user:id,name,email',
])
->where('status', 1)
->whereNotNull('end_date')
->whereBetween('end_date', [
now()->startOfDay(),
now()->addDays($days)->endOfDay(),
]);
// Optional date range filter
if ($request->filled(['from_date', 'to_date'])) {
$query->whereBetween('end_date', [
Carbon::parse($request->from_date)->startOfDay(),
Carbon::parse($request->to_date)->endOfDay(),
]);
}
// Optional filtering by restaurant_id
if ($request->filled('restaurant_id')) {
$query->where('restaurant_id', $request->restaurant_id);
}
// Sorting
$sortBy = $request->get('sort_by', 'end_date');
$sortOrder = $request->get('sort_order', 'asc');
$subscriptions = $query->orderBy($sortBy, $sortOrder)
->paginate($request->get('per_page', 20));
return $this->responseSuccess($subscriptions, 'Expiring subscriptions fetched successfully.');
} catch (\Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function transactions(Request $request): JsonResponse
{
$query = SubscriptionItem::with(['subscription', 'package', 'user']);
// Optional Restaurant Filter
if ($request->filled('restaurant_id')) {
$query->where('restaurant_id', $request->restaurant_id);
}
// Status filter
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Date range
if ($request->filled(['from_date', 'to_date'])) {
$query->whereBetween('created_at', [
Carbon::parse($request->from_date)->startOfDay(),
Carbon::parse($request->to_date)->endOfDay(),
]);
}
// Sorting
$sortBy = $request->get('sort_by', 'created_at');
$sortOrder = $request->get('sort_order', 'desc');
$transactions = $query->orderBy($sortBy, $sortOrder)
->paginate($request->get('per_page', 20));
return $this->responseSuccess($transactions, 'Transactions list fetched successfully.');
}
public function invoices(Request $request): JsonResponse
{
$query = SubscriptionItem::with(['subscription.restaurant', 'package', 'user'])
->where('status', 1);
// Optional Restaurant Filter
if ($request->filled('restaurant_id')) {
$query->where('restaurant_id', $request->restaurant_id);
}
// Date range
if ($request->filled(['from_date', 'to_date'])) {
$query->whereBetween('created_at', [
Carbon::parse($request->from_date)->startOfDay(),
Carbon::parse($request->to_date)->endOfDay(),
]);
}
$invoices = $query
->orderBy($request->get('sort_by', 'created_at'), $request->get('sort_order', 'desc'))
->paginate($request->get('per_page', 20));
// Append Invoice Number
$invoices->getCollection()->transform(function ($item) {
$item->invoice_no = 'INV-'.str_pad($item->id, 6, '0', STR_PAD_LEFT);
return $item;
});
return $this->responseSuccess($invoices, 'Invoices list fetched successfully.');
}
public function revenueAnalytics(Request $request): JsonResponse
{
$baseQuery = SubscriptionItem::where('status', 1);
// Optional Restaurant Filter
if ($request->filled('restaurant_id')) {
$baseQuery->where('restaurant_id', $request->restaurant_id);
}
// Date range
if ($request->filled(['from_date', 'to_date'])) {
$baseQuery->whereBetween('created_at', [
Carbon::parse($request->from_date)->startOfDay(),
Carbon::parse($request->to_date)->endOfDay(),
]);
}
$totalRevenue = (clone $baseQuery)->sum('amount');
$totalTransactions = (clone $baseQuery)->count();
$monthlyRevenue = (clone $baseQuery)
->selectRaw('YEAR(created_at) year, MONTH(created_at) month, SUM(amount) total')
->groupBy('year', 'month')
->orderBy('year', 'desc')
->get();
$packageWise = (clone $baseQuery)
->selectRaw('package_id, SUM(amount) total')
->groupBy('package_id')
->with('package:id,name')
->get();
return $this->responseSuccess([
'total_revenue' => $totalRevenue,
'total_transactions' => $totalTransactions,
'monthly_revenue' => $monthlyRevenue,
'package_wise' => $packageWise,
], 'Revenue analytics fetched successfully.');
}
public function sendBillingNotification(Request $request): JsonResponse
{
$request->validate([
'restaurant_ids' => 'required|array',
'type' => 'required|in:email,sms,both',
'notification_for' => 'required|string',
]);
$restaurants = Restaurant::with(['subscriptions.package'])
->whereIn('id', $request->restaurant_ids)
->get();
foreach ($restaurants as $restaurant) {
$subscription = $restaurant->subscriptions()
->where('status', 1)
->latest()
->first();
if (! $subscription) {
continue;
}
$invoiceData = $this->buildInvoiceData($restaurant, $subscription);
$paymentUrl = $this->generatePaymentUrl($restaurant, $subscription);
$domainInfo = $this->getDomainInfo($restaurant);
/** EMAIL **/
if (in_array($request->type, ['email', 'both'])) {
Mail::to($restaurant->email)
->queue(new BillingInvoiceMail(
$restaurant,
$subscription,
$invoiceData,
$paymentUrl,
$domainInfo
));
}
/** SMS **/
if (in_array($request->type, ['sms', 'both'])) {
// TODO: Implement SMS sending logic here
// $this->sendSms(
// $restaurant->phone,
// $this->buildSmsMessage($restaurant, $subscription, $paymentUrl)
// );
}
}
return $this->responseSuccess([], 'Billing notification sent successfully');
}
private function buildInvoiceData($restaurant, $subscription)
{
return (object) [
'invoice_no' => 'INV-'.str_pad($subscription->id, 6, '0', STR_PAD_LEFT),
'amount' => $subscription->package->price,
'package_name' => $subscription->package->name,
'start_date' => $subscription->start_date,
'end_date' => $subscription->end_date,
'status' => 'Unpaid',
'month' => now()->format('Y-m'),
];
}
private function getDomainInfo($restaurant)
{
return [
'domain' => $restaurant->domain ?? null,
'domain_expiry' => $restaurant->domain_expiry ?? null,
];
}
private function generatePaymentUrl($restaurant, $subscription)
{
return config('app.frontend_url')
.'/billing/upgrade?restaurant_id='
.encrypt($restaurant->id);
}
private function buildSmsMessage($restaurant, $subscription, $paymentUrl)
{
return "Hello {$restaurant->name}, your subscription expires on "
.Carbon::parse($subscription->end_date)->format('d M Y')
.". Upgrade now: {$paymentUrl}";
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Http\Requests\Settings\MailConfigUpdateRequest;
use Modules\Authentication\Http\Requests\Settings\SAASSettingUpdateRequest;
use Modules\Authentication\Models\Package;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Models\SAASSetting;
class DashboardController extends Controller
{
public function saasDashboardData(Request $request): JsonResponse
{
$year = (int) $request->year ?? now()->year;
$totalRestaurantsCount = Restaurant::count();
$totalPackages = Package::count();
$totalActiveRestaurant = Restaurant::where('status', 1)->count();
$totalInactiveRestaurant = Restaurant::where('status', 2)->count();
// Monthly transaction collection for current year
$monthlyData = DB::table('subscription_items')
->selectRaw('MONTH(created_at) as month, SUM(amount) as total_paid')
->whereYear('created_at', $year)
->whereNull('deleted_at')
->groupBy(DB::raw('MONTH(created_at)'))
->pluck('total_paid', 'month');
$monthlyReport = collect(range(1, 12))->map(function ($month) use ($monthlyData) {
return [
'month' => Carbon::create()->month($month)->format('F'),
'total_paid' => $monthlyData->get($month, 0),
];
});
// Total unique restaurants with active subscriptions
$totalRestaurants = DB::table('subscriptions')
->where('status', 'active')
->whereNull('deleted_at')
->distinct('restaurant_id')
->count('restaurant_id');
// Plan usage with percentages
$packageUsage = DB::table('subscriptions')
->join('packages', 'subscriptions.package_id', '=', 'packages.id')
->where('subscriptions.status', 'active')
->whereNull('subscriptions.deleted_at')
->select('packages.id as package_id', 'packages.name as plan_name', DB::raw('COUNT(DISTINCT subscriptions.restaurant_id) as restaurant_count'))
->groupBy('packages.id', 'packages.name')
->get()
->map(function ($item) use ($totalRestaurants) {
return [
'package_id' => $item->package_id,
'plan_name' => $item->plan_name,
'restaurant_count' => $item->restaurant_count,
'usage_percent' => $totalRestaurants > 0
? round(($item->restaurant_count / $totalRestaurants) * 100, 2)
: 0,
];
});
return $this->responseSuccess([
'total_restaurants' => $totalRestaurantsCount,
'total_packages' => $totalPackages,
'total_active_restaurant' => $totalActiveRestaurant,
'total_inactive_restaurant' => $totalInactiveRestaurant,
'transactions' => $monthlyReport,
'package_report' => $packageUsage,
], _lang('SaaS Dashboard data has been fetched successfully.'));
}
public function getSAASSettings(Request $request)
{
$saas_settings = DB::table('s_a_a_s_settings')->pluck('value', 'name')->toArray();
return $this->responseSuccess($saas_settings, _lang('SaaS settings fetched successfully.'));
}
public function saasSettingsUpdate(SAASSettingUpdateRequest $request): JsonResponse
{
try {
// If POST request, update or create settings
DB::beginTransaction();
foreach ($request->validated() as $key => $value) {
if ($key === '_token') {
continue;
}
SAASSetting::updateOrInsert(
[
'name' => $key,
],
[
'value' => $value,
'updated_at' => now(),
'created_at' => now(),
]
);
}
DB::commit();
return $this->responseSuccess([], _lang('SAAS Settings have been successfully updated.'));
} catch (\Exception $e) {
DB::rollBack();
return $this->responseError(['error' => $e->getMessage()], _lang('Failed to store records unsuccessfully.'));
}
}
public function getMailConfig()
{
$mailConfig = [
'MAIL_MAILER' => env('MAIL_MAILER'),
'MAIL_HOST' => env('MAIL_HOST'),
'MAIL_PORT' => env('MAIL_PORT'),
'MAIL_USERNAME' => env('MAIL_USERNAME'),
'MAIL_PASSWORD' => env('MAIL_PASSWORD') ? '********' : null, // mask password
'MAIL_FROM_ADDRESS' => env('MAIL_FROM_ADDRESS'),
'MAIL_FROM_NAME' => env('MAIL_FROM_NAME'),
];
return $this->responseSuccess(
$mailConfig,
_lang('Mail configuration fetched successfully.')
);
}
public function updateMailConfig(MailConfigUpdateRequest $request)
{
foreach ($request->validated() as $key => $value) {
setEnvValue($key, $value);
}
// Clear config cache so changes apply immediately
Artisan::call('config:clear');
Artisan::call('cache:clear');
return $this->responseSuccess(
[],
_lang('Mail configuration updated successfully.')
);
}
public function uploadLogo(Request $request): JsonResponse
{
$request->validate([
'logo' => 'required|image|mimes:jpeg,png,jpg|max:8192',
]);
$imageUrl = null;
if (isset($request['logo'])) {
if (isset($request['logo']) && $request['logo']->isValid()) {
$imageUrl = fileUploader('logos/', 'png', $request['logo']);
}
}
// Prepare data
$data = [
'name' => 'logo',
'value' => $imageUrl,
'updated_at' => Carbon::now(),
];
// Update or create logo setting
$saasSetting = SAASSetting::updateOrCreate(
[
'name' => 'logo',
],
$data
);
return $this->responseSuccess(
$saasSetting,
_lang('SAAS settings logo have been successfully updated')
);
}
public function getPackages(Request $request): JsonResponse
{
$perPage = $request->get('per_page', 10);
$getPackages = Package::paginate($perPage);
return $this->responseSuccess($getPackages, 'Packages fetched successfully.');
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\RequestSanitizerTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\Authentication\Models\Feedback;
use Modules\Authentication\Repositories\FeedbackRepository;
class FeedbackController extends Controller
{
use RequestSanitizerTrait;
public function __construct(private FeedbackRepository $feedback) {}
public function getFeedbacks(Request $request): JsonResponse
{
$perPage = $request->get('per_page', 10);
$getFeedbacks = Feedback::with('user')->paginate($perPage);
return $this->responseSuccess($getFeedbacks, 'Feedbacks fetched successfully.');
}
public function index(): JsonResponse
{
try {
return $this->responseSuccess(
$this->feedback->getAll(request()->all()),
'Feedback has been fetched successfully.'
);
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(Request $request): JsonResponse
{
try {
return $this->responseSuccess(
$this->feedback->create($request->all()),
'Feedback has been created successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->feedback->getById($id),
'Feedback has been fetched successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function update(Request $request, int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->feedback->update($id, $this->getUpdateRequest($request)),
'Feedback has been updated successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function destroy(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->feedback->delete($id),
'Feedback has been deleted successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Modules\Authentication\Models\Gallery;
class GalleryController extends Controller
{
public function index()
{
$galleries = Gallery::all();
return $this->responseSuccess(
$galleries,
'Galleries has been fetched successfully.'
);
}
public function store(Request $request)
{
$request->validate([
'title' => 'nullable|string|max:255',
'image' => 'required',
]);
$image = $request->file('image');
$path = $image->store('gallery', 'public');
$gallery = Gallery::create([
'title' => $request->title,
'type' => $request->type,
'image' => asset('storage/'.$path),
]);
return response()->json($gallery, 201);
}
public function update(Request $request, $id)
{
$gallery = Gallery::where('id', $id)->first();
if (! $gallery) {
return $this->responseError([], 'Gallery Image Not Found');
}
$request->validate([
'title' => 'nullable|string|max:255',
'image' => 'nullable',
]);
if ($request->hasFile('image')) {
// Delete old image
$oldPath = str_replace(asset('storage/'), '', $gallery->image);
Storage::disk('public')->delete($oldPath);
// Store new image
$image = $request->file('image');
$path = $image->store('gallery', 'public');
$gallery->image = asset('storage/'.$path);
}
if ($request->has('title')) {
$gallery->title = $request->title;
$gallery->type = $request->type;
}
$gallery->save();
return $this->responseSuccess(
$gallery,
'Gallery image update successfully.'
);
}
public function destroy(int $id)
{
$gallery = Gallery::where('id', $id)->first();
if (! $gallery) {
return $this->responseError([], 'Gallery Image Not Found');
}
// Delete file from storage
$path = str_replace(asset('storage/'), '', $gallery->image);
Storage::disk('public')->delete($path);
// Delete DB record
$gallery->delete();
return $this->responseSuccess(
$gallery,
'Gallery image delete successfully.'
);
}
}

View File

@@ -0,0 +1,257 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Models\Language;
class LanguageController extends Controller
{
/**
* List all languages or get a specific language by name
*/
public function index(Request $request): JsonResponse
{
try {
$name = $request->query('name');
if ($name) {
$lang = Language::where('name', $name)->first();
if (! $lang) {
return $this->responseError([], 'Language not found', 404);
}
$filePath = storage_path("app/public/{$lang->file_path}");
if (! file_exists($filePath)) {
return $this->responseError([], 'Language file not found', 404);
}
$json = json_decode(file_get_contents($filePath), true);
return $this->responseSuccess([
'name' => $lang->name,
'code' => $lang->code,
'flag_url' => $lang->flag ? asset("storage/flags/{$lang->flag}") : null,
'file_url' => $lang->file_path ? asset("storage/{$lang->file_path}") : null,
'data' => $json,
'is_default' => $lang->is_default,
'status' => $lang->status,
], 'Language fetched successfully.');
}
// Fetch all languages, default first
$languages = Language::orderByDesc('is_default')->get()->map(function ($lang) {
return [
'id' => $lang->id,
'name' => $lang->name,
'code' => $lang->code,
'flag_url' => $lang->flag ? asset("storage/flags/{$lang->flag}") : null,
'file_url' => $lang->file_path ? asset("storage/{$lang->file_path}") : null,
'is_default' => $lang->is_default,
'status' => $lang->status,
];
});
return $this->responseSuccess($languages, 'Languages fetched successfully.');
} catch (\Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
/**
* Store a new language
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'name' => 'required|string',
'code' => 'required|string|max:10|unique:languages,code',
'file' => 'required|file|mimes:json',
'flag' => 'nullable|image|mimes:png,jpg,jpeg,svg',
'is_default' => 'nullable|boolean',
]);
DB::beginTransaction();
try {
$fileName = $request->name.'.json';
$filePath = $request->file('file')->storeAs('languages', $fileName, 'public');
$flagPath = null;
if ($request->hasFile('flag')) {
$flagPath = fileUploader('flags/', 'png', $request->file('flag'));
}
$lang = Language::create([
'name' => $request->name,
'code' => $request->code,
'file_path' => $filePath,
'flag' => $flagPath,
'is_default' => $request->input('is_default', false),
'status' => 'active',
]);
DB::commit();
return $this->responseSuccess($lang, 'Language created successfully.');
} catch (\Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
/**
* Update language (name, code, file, flag, default)
*/
public function update(Request $request, int $id): JsonResponse
{
$lang = Language::findOrFail($id);
$request->validate([
'name' => 'sometimes|required|string',
'code' => 'sometimes|required|string|max:10|unique:languages,code,'.$lang->id,
'file' => 'sometimes|file|mimes:json',
'flag' => 'nullable|image|mimes:png,jpg,jpeg,svg|max:2048',
'is_default' => 'nullable|boolean',
]);
DB::beginTransaction();
try {
if ($request->has('name')) {
$lang->name = $request->name;
}
if ($request->has('code')) {
$lang->code = $request->code;
}
if ($request->hasFile('file')) {
$fileName = $lang->name.'.json';
$lang->file_path = $request->file('file')->storeAs('languages', $fileName, 'public');
}
if ($request->hasFile('flag')) {
$lang->flag = fileUploader('flags/', 'png', $request->file('flag'), $lang->flag);
}
if ($request->has('is_default') && $request->is_default) {
Language::where('is_default', true)->update(['is_default' => false]);
$lang->is_default = true;
}
$lang->save();
DB::commit();
return $this->responseSuccess($lang, 'Language updated successfully.');
} catch (\Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
/**
* Export JSON file
*/
public function export($id)
{
try {
$lang = Language::findOrFail($id);
$filePath = storage_path("app/public/{$lang->file_path}");
if (! file_exists($filePath)) {
return $this->responseError([], 'File not found', 404);
}
return response()->download($filePath, $lang->name.'.json');
} catch (\Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
/**
* Set default language
*/
public function setDefault($id): JsonResponse
{
DB::beginTransaction();
try {
// Get the language to set as default
$lang = Language::findOrFail($id);
// Set all languages is_default = false
Language::where('is_default', true)->update(['is_default' => false]);
// Set selected language as default
$lang->is_default = true;
$lang->save();
DB::commit();
return $this->responseSuccess([], "$lang->name has been set as default.");
} catch (\Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
/**
* Delete language
*/
public function destroy(int $id): JsonResponse
{
try {
$lang = Language::findOrFail($id);
if ($lang->file_path && file_exists(storage_path("app/public/{$lang->file_path}"))) {
unlink(storage_path("app/public/{$lang->file_path}"));
}
if ($lang->flag && file_exists(storage_path("app/public/{$lang->flag}"))) {
unlink(storage_path("app/public/{$lang->flag}"));
}
$lang->delete();
return $this->responseSuccess([], 'Language deleted successfully.');
} catch (\Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function updateKey(Request $request, $name): JsonResponse
{
$request->validate([
'key' => 'required|string',
'value' => 'required|string',
]);
$lang = Language::where('name', $name)->firstOrFail();
$path = storage_path("app/public/{$lang->file_path}");
if (! file_exists($path)) {
return response()->json(['error' => 'Language file not found'], 404);
}
$json = json_decode(file_get_contents($path), true) ?? [];
$json[$request->key] = $request->value;
file_put_contents($path, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return response()->json(['success' => true, 'message' => 'Key updated']);
}
public function import(Request $request, $name): JsonResponse
{
$request->validate([
'file' => 'required|file|mimes:json',
]);
$lang = Language::where('code', $name)->firstOrFail();
$fileName = $name.'.json';
$path = $request->file('file')->storeAs('languages', $fileName, 'public');
$lang->update(['file_path' => $path]);
return response()->json(['success' => true, 'message' => 'Language updated']);
}
}

View File

@@ -0,0 +1,365 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\Authenticatable;
use App\Traits\ResponseTrait;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Modules\Authentication\Models\Package;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Models\Setting;
use Modules\Authentication\Models\Subscription;
use Modules\Authentication\Models\SubscriptionItem;
use Modules\Authentication\Models\User;
use Modules\Frontend\Models\Onboarding;
use Spatie\Permission\Models\Role;
class OnboardingsController extends Controller
{
use Authenticatable, ResponseTrait;
public function index(Request $request): JsonResponse
{
$query = Onboarding::query()
->whereNull('approved_by') // Only unapproved list
->orderBy('id', 'desc');
// 🔍 Search filter (restaurant name, email, phone)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('restaurant_name', 'LIKE', "%{$search}%")
->orWhere('restaurant_email', 'LIKE', "%{$search}%")
->orWhere('restaurant_phone', 'LIKE', "%{$search}%")
->orWhere('name', 'LIKE', "%{$search}%") // Owner name
->orWhere('email', 'LIKE', "%{$search}%"); // Owner email
});
}
// 🎯 Status filter
if ($request->filled('status')) {
$query->where('status', $request->status);
}
$onboardings = $query->get();
return $this->responseSuccess($onboardings);
}
public function approve(int $onboardingId): JsonResponse
{
// Start a transaction to ensure atomicity
DB::beginTransaction();
try {
// Fetch the data to approve
$onboarding = Onboarding::where('id', $onboardingId)->first();
if (! $onboarding) {
return $this->responseError([], 'No Data Found!', 404);
}
// Update the approval status and details
$onboarding->status = 'approved';
$onboarding->approved_by = $this->getCurrentUserId();
$onboarding->approved_at = Carbon::now();
$onboarding->save();
// Create the new restaurant
$restaurant = Restaurant::create([
'name' => $onboarding->restaurant_name,
'type' => $onboarding->restaurant_type,
'email' => $onboarding->restaurant_email,
'phone' => $onboarding->restaurant_phone,
'domain' => $onboarding->restaurant_domain,
'address' => $onboarding->restaurant_address,
'logo' => $onboarding->restaurant_logo,
]);
$defaultSettings = [
// 🏠 General Information
['type' => 'general', 'option_key' => 'restaurant_name', 'option_value' => 'Restaurant'],
['type' => 'general', 'option_key' => 'site_title', 'option_value' => 'restaurant'],
['type' => 'general', 'option_key' => 'phone', 'option_value' => '01XXXXXXXXX'],
['type' => 'general', 'option_key' => 'email', 'option_value' => 'restaurant@gmail.com'],
['type' => 'general', 'option_key' => 'language', 'option_value' => 'en'],
['type' => 'general', 'option_key' => 'google_map', 'option_value' => ''],
['type' => 'general', 'option_key' => 'address', 'option_value' => 'Asia,Dhaka-1219'],
['type' => 'general', 'option_key' => 'on_google_map', 'option_value' => ''],
['type' => 'general', 'option_key' => 'restaurant_code', 'option_value' => '987654'],
['type' => 'general', 'option_key' => 'currency_symbol', 'option_value' => '$'],
['type' => 'general', 'option_key' => 'logo', 'option_value' => 'logo.png'],
['type' => 'general', 'option_key' => 'mail_type', 'option_value' => 'mail'],
['type' => 'general', 'option_key' => 'disabled_website', 'option_value' => 'no'],
['type' => 'general', 'option_key' => 'copyright_text', 'option_value' => '&copy; Copyright 2025. All Rights Reserved by FueDevs LTD'],
['type' => 'general', 'option_key' => 'facebook_link', 'option_value' => 'https://www.facebook.com/'],
['type' => 'general', 'option_key' => 'google_plus_link', 'option_value' => 'https://www.google.com/'],
['type' => 'general', 'option_key' => 'youtube_link', 'option_value' => 'https://www.youtube.com/'],
['type' => 'general', 'option_key' => 'whats_app_link', 'option_value' => ''],
['type' => 'general', 'option_key' => 'twitter_link', 'option_value' => 'https://www.twitter.com'],
['type' => 'general', 'option_key' => 'eiin_code', 'option_value' => ''],
['type' => 'general', 'option_key' => 'sms_gateway', 'option_value' => 'twilio'],
['type' => 'general', 'option_key' => 'bulk_sms_api_key', 'option_value' => ''],
['type' => 'general', 'option_key' => 'bulk_sms_sender_id', 'option_value' => ''],
['type' => 'general', 'option_key' => 'twilio_sid', 'option_value' => ''],
['type' => 'general', 'option_key' => 'twilio_token', 'option_value' => ''],
['type' => 'general', 'option_key' => 'twilio_from_number', 'option_value' => ''],
['type' => 'general', 'option_key' => 'header_notice', 'option_value' => ''],
['type' => 'general', 'option_key' => 'app_version', 'option_value' => '1.0.0'],
['type' => 'general', 'option_key' => 'app_url', 'option_value' => 'drive-link'],
// 🎨 Restaurant Identity & Display
['type' => 'general', 'option_key' => 'tagline', 'option_value' => 'Delicious Food, Fresh Taste'],
['type' => 'general', 'option_key' => 'favicon', 'option_value' => 'favicon.png'],
['type' => 'general', 'option_key' => 'theme_color', 'option_value' => '#ff6b00'],
['type' => 'general', 'option_key' => 'background_image', 'option_value' => 'bg.jpg'],
// 💰 Finance / POS / Invoice
['type' => 'pos', 'option_key' => 'tax_type', 'option_value' => 'exclusive'],
['type' => 'pos', 'option_key' => 'tax_percentage', 'option_value' => '10'],
['type' => 'pos', 'option_key' => 'service_charge', 'option_value' => '5'],
['type' => 'pos', 'option_key' => 'default_currency', 'option_value' => 'USD'],
['type' => 'pos', 'option_key' => 'billing_prefix', 'option_value' => 'INV-'],
['type' => 'pos', 'option_key' => 'invoice_footer', 'option_value' => 'Thank you! Visit again.'],
['type' => 'pos', 'option_key' => 'enable_kitchen_print', 'option_value' => 'yes'],
['type' => 'pos', 'option_key' => 'enable_customer_copy', 'option_value' => 'yes'],
// 🚚 Online Ordering & Delivery
['type' => 'order', 'option_key' => 'enable_online_order', 'option_value' => 'yes'],
['type' => 'order', 'option_key' => 'delivery_charge', 'option_value' => '50'],
['type' => 'order', 'option_key' => 'minimum_order_amount', 'option_value' => '100'],
['type' => 'order', 'option_key' => 'auto_accept_order', 'option_value' => 'no'],
['type' => 'order', 'option_key' => 'estimated_preparation_time', 'option_value' => '30'],
// 🔔 Notifications / Integrations
['type' => 'integration', 'option_key' => 'slack_webhook_url', 'option_value' => ''],
['type' => 'integration', 'option_key' => 'telegram_bot_token', 'option_value' => ''],
['type' => 'integration', 'option_key' => 'telegram_chat_id', 'option_value' => ''],
['type' => 'integration', 'option_key' => 'twilio_sms_enabled', 'option_value' => 'yes'],
['type' => 'integration', 'option_key' => 'email_notifications', 'option_value' => 'yes'],
['type' => 'integration', 'option_key' => 'whatsapp_notifications', 'option_value' => 'no'],
// 🧾 Reports & Logs
['type' => 'system', 'option_key' => 'auto_backup', 'option_value' => 'daily'],
['type' => 'system', 'option_key' => 'report_timezone', 'option_value' => 'Asia/Dhaka'],
['type' => 'system', 'option_key' => 'data_retention_days', 'option_value' => '365'],
// 💻 UI/UX Preferences
['type' => 'ui', 'option_key' => 'sidebar_collapsed', 'option_value' => 'no'],
['type' => 'ui', 'option_key' => 'dark_mode', 'option_value' => 'no'],
['type' => 'ui', 'option_key' => 'default_dashboard', 'option_value' => 'sales'],
// 💳 Payment Gateways
['type' => 'payment', 'option_key' => 'razorpay_key', 'option_value' => ''],
['type' => 'payment', 'option_key' => 'razorpay_secret', 'option_value' => ''],
['type' => 'payment', 'option_key' => 'stripe_key', 'option_value' => ''],
['type' => 'payment', 'option_key' => 'stripe_secret', 'option_value' => ''],
['type' => 'payment', 'option_key' => 'cash_on_delivery', 'option_value' => 'yes'],
// 👨‍🍳 Kitchen & Staff
['type' => 'staff', 'option_key' => 'max_table_capacity', 'option_value' => '10'],
['type' => 'staff', 'option_key' => 'default_shift_start', 'option_value' => '09:00'],
['type' => 'staff', 'option_key' => 'default_shift_end', 'option_value' => '23:00'],
['type' => 'staff', 'option_key' => 'auto_logout_idle_minutes', 'option_value' => '60'],
// 🎨 Color Combination
['type' => 'system_color', 'option_key' => 'primary_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'secondary_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'primary_container_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'dark_primary_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'dark_secondary_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'dark_container_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'text_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'dark_text_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'sidebar_selected_bg_color', 'option_value' => null],
['type' => 'system_color', 'option_key' => 'sidebar_selected_text_color', 'option_value' => null],
// 🚚 Delivery / Online features
['type' => 'delivery', 'option_key' => 'is_online', 'option_value' => false],
['type' => 'delivery', 'option_key' => 'latitude', 'option_value' => '23.8103'],
['type' => 'delivery', 'option_key' => 'longitude', 'option_value' => '90.4125'],
['type' => 'delivery', 'option_key' => 'delivery_radius_km', 'option_value' => 5.00],
['type' => 'delivery', 'option_key' => 'delivery_fee', 'option_value' => 0],
['type' => 'delivery', 'option_key' => 'delivery_partner_count', 'option_value' => 0],
['type' => 'delivery', 'option_key' => 'delivery_time_avg', 'option_value' => 30],
['type' => 'delivery', 'option_key' => 'pickup_enabled', 'option_value' => true],
// 🕒 Operational hours & capacity
['type' => 'operational', 'option_key' => 'opening_time', 'option_value' => '09:00:00'],
['type' => 'operational', 'option_key' => 'closing_time', 'option_value' => '22:00:00'],
['type' => 'operational', 'option_key' => 'auto_accept_orders', 'option_value' => false],
['type' => 'operational', 'option_key' => 'pre_order_enabled', 'option_value' => false],
['type' => 'operational', 'option_key' => 'max_order_capacity', 'option_value' => 50],
// 📊 Analytics / reviews
['type' => 'analytics', 'option_key' => 'avg_rating', 'option_value' => 0.00],
['type' => 'analytics', 'option_key' => 'review_count', 'option_value' => 0],
['type' => 'analytics', 'option_key' => 'total_orders', 'option_value' => 0],
['type' => 'analytics', 'option_key' => 'last_order_time', 'option_value' => null],
['type' => 'analytics', 'option_key' => 'last_active_time', 'option_value' => null],
// 🎯 Marketing & social
['type' => 'marketing', 'option_key' => 'loyalty_points_enabled', 'option_value' => false],
['type' => 'marketing', 'option_key' => 'offers_enabled', 'option_value' => false],
['type' => 'marketing', 'option_key' => 'social_media_links', 'option_value' => json_encode([
'facebook' => '',
'instagram' => '',
'twitter' => '',
'linkedin' => '',
])],
// 💻 Future-proof / extra
['type' => 'system', 'option_key' => 'settings', 'option_value' => json_encode([])],
['type' => 'system', 'option_key' => 'uuid', 'option_value' => uniqid('rest_')],
// Email Config
['type' => 'email_config', 'option_key' => 'email_smtp_host', 'option_value' => null],
['type' => 'email_config', 'option_key' => 'email_smtp_port', 'option_value' => null],
['type' => 'email_config', 'option_key' => 'email_smtp_username', 'option_value' => null],
['type' => 'email_config', 'option_key' => 'email_smtp_password', 'option_value' => null],
['type' => 'email_config', 'option_key' => 'email_smtp_encryption', 'option_value' => null],
// SMS Config
['type' => 'sms_config', 'option_key' => 'twilio_api_key', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'twilio_api_secret', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'twilio_sender_id', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'twilio_api_url', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'twilio_is_default', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'nexmo_api_key', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'nexmo_api_secret', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'nexmo_sender_id', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'nexmo_api_url', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'nexmo_is_default', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'muthofun_api_key', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'smsglobal_api_key', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'smsglobal_api_secret', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'smsglobal_sender_id', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'smsglobal_api_url', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'smsglobal_extra_key', 'option_value' => null],
['type' => 'sms_config', 'option_key' => 'smsglobal_is_default', 'option_value' => null],
];
foreach ($defaultSettings as &$setting) {
$setting['setting_type'] = 'Restaurant';
$setting['restaurant_id'] = $restaurant->id;
$setting['created_at'] = now();
$setting['updated_at'] = now();
}
Setting::insert($defaultSettings);
// Create the associated user
$user = User::create([
'first_name' => $onboarding->name,
'email' => $onboarding->email,
'phone' => $onboarding->phone,
'password' => $onboarding->password,
'restaurant_id' => $restaurant->id,
'avatar' => $onboarding->avatar,
'role_id' => 2,
]);
$role = Role::where('id', 2)->first();
$user->assignRole($role);
// Commit the transaction
DB::commit();
// Respond with a success message
return response()->json([
'status' => true,
'message' => 'Restaurant approved and created successfully.',
'data' => [
'restaurant' => $restaurant,
'user' => $user,
],
]);
} catch (Exception $e) {
// Rollback in case of any failure
DB::rollBack();
return response()->json([
'status' => false,
'message' => 'An error occurred: '.$e->getMessage(),
], 500);
}
}
public function upgrade(Request $request): JsonResponse
{
$validated = $request->validate([
'restaurant_id' => 'required|exists:restaurants,id',
'package_id' => 'required|exists:packages,id',
'extra_days' => 'nullable|integer|min:0',
'payment_method' => 'required|string',
'amount_paid' => 'required|numeric|min:0',
'notes' => 'nullable|string',
]);
$restaurantId = $validated['restaurant_id'];
$package = Package::findOrFail($validated['package_id']);
$extraDays = $validated['extra_days'] ?? 0;
$startDate = Carbon::now();
$endDate = $startDate->copy()->addDays($package->duration_days + $extraDays);
// Fetch or create subscription for the restaurant
$subscription = Subscription::where('restaurant_id', $restaurantId)->first();
if ($subscription) {
// Update existing subscription
$subscription->update([
'package_id' => $package->id,
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
'transactions_details' => json_encode([
'invoice_no' => 'INV-'.strtoupper(Str::random(6)),
'issued_at' => now()->toDateString(),
'notes' => $validated['notes'] ?? 'Upgraded Package',
'amount' => $validated['amount_paid'],
]),
]);
} else {
// Fallback (shouldnt happen unless subscription was deleted)
$subscription = Subscription::create([
'restaurant_id' => $restaurantId,
'package_id' => $package->id,
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
'user_id' => Auth::id(),
'transactions_details' => json_encode([
'invoice_no' => 'INV-'.strtoupper(Str::random(6)),
'issued_at' => now()->toDateString(),
'notes' => $validated['notes'] ?? 'Initial Subscription',
'amount' => $validated['amount_paid'],
]),
]);
}
// Create new subscription item
SubscriptionItem::create([
'user_id' => Auth::id(),
'restaurant_id' => $subscription->restaurant_id,
'subscription_id' => $subscription->id,
'reference_no' => 'PAY-'.strtoupper(Str::random(8)),
'amount_paid' => $validated['amount_paid'],
'payment_method' => $validated['payment_method'],
]);
// Respond with a success message
return $this->responseSuccess([
'restaurantId' => $restaurantId,
'package' => $package,
'extraDays' => $extraDays,
'subscription' => $subscription,
], 'Subscription updated successfully.');
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Requests\Package\PackageStoreRequest;
use App\Http\Requests\Package\PackageUpdateRequest;
use App\Services\Package\PackageService;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PackageController extends Controller
{
public function __construct(
private PackageService $service
) {}
public function index(Request $request): JsonResponse
{
$packages = $this->service->getAll($request);
return $this->responseSuccess($packages);
}
public function store(PackageStoreRequest $request)
{
DB::beginTransaction();
try {
$package = $this->service->create($request->validated());
DB::commit();
return $this->responseSuccess($package, 'Package has been create successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
public function show(int $id)
{
$package = $this->service->getById($id);
return $this->responseSuccess($package, 'Package has been fetch successfully.');
}
public function update(PackageUpdateRequest $request, int $id)
{
DB::beginTransaction();
try {
// Get validated data
$data = $request->validated();
// List of all checkbox/boolean fields
$booleanFields = [
'sms_management_enabled',
'chat_enable',
'landing_page_enable',
'blog_enable',
'auto_renew',
];
// Set missing checkboxes to 0
foreach ($booleanFields as $field) {
if (! isset($data[$field])) {
$data[$field] = 0;
}
}
$package = $this->service->update($id, $data);
DB::commit();
return $this->responseSuccess($package, 'Package has been updated successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
public function destroy(int $id)
{
DB::beginTransaction();
try {
$this->service->delete($id);
DB::commit();
return $this->responseSuccess([], 'Package has been delete successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage());
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Services\PermissionService;
class PermissionController extends Controller
{
public function __construct(
private PermissionService $service
) {}
public function index(Request $request): JsonResponse
{
$perPage = request('per_page') ?? 10;
$permissions = $this->service->index($request, $perPage);
if (! $permissions) {
return $this->responseError([], 'No Data Found!', 404);
}
return $this->responseSuccess($permissions);
}
public function store(Request $request): JsonResponse
{
DB::beginTransaction();
try {
$this->service->create($request->validated());
DB::commit();
return $this->responseSuccess([]);
} catch (\Exception $e) {
DB::rollBack();
return $this->responseError([], 'Data Process Error!');
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\RequestSanitizerTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\Authentication\Http\Requests\Restaurant\RestaurantStoreRequest;
use Modules\Authentication\Http\Requests\Restaurant\RestaurantUpdateRequest;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Repositories\RestaurantRepository;
class RestaurantController extends Controller
{
use RequestSanitizerTrait;
public function __construct(private RestaurantRepository $restaurant) {}
public function index(): JsonResponse
{
try {
return $this->responseSuccess(
$this->restaurant->getAll(request()->all()),
'Restaurant has been fetched successfully.'
);
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(RestaurantStoreRequest $request): JsonResponse
{
try {
return $this->responseSuccess(
$this->restaurant->create($request->all()),
'Restaurant has been created successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->restaurant->getById($id),
'Restaurant has been fetched successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function update(RestaurantUpdateRequest $request, int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->restaurant->update($id, $this->getUpdateRequest($request)),
'Restaurant has been updated successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function destroy(int $id): JsonResponse
{
try {
$restaurant = $this->restaurant->getById($id);
if (! $restaurant) {
return $this->responseError([], 'Restaurant Not Found');
}
if ($restaurant->logo) {
fileUploader('restaurant/', 'png', null, $restaurant->logo);
}
return $this->responseSuccess(
$this->restaurant->delete($id),
'Restaurant has been deleted successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function statusUpdate(int $restaurantId): JsonResponse
{
try {
$restaurant = Restaurant::find($restaurantId);
if ($restaurant) {
$restaurant->status = $restaurant->status == 1 ? 0 : 1;
$restaurant->save();
return $this->responseSuccess(
['status' => $restaurant->status],
'Restaurant status updated successfully.'
);
}
return $this->responseError([], 'Restaurant not found.', 404);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function restaurantTheme(Request $request)
{
$restaurant = Restaurant::where('id', $request->restaurant_id)->first();
$restaurant->update([
'theme_id' => $request->theme_id,
]);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\JsonResponse;
use Modules\Authentication\Http\Requests\RestaurantImageSAASSetting\RestaurantImageSAASSettingStoreRequest;
use Modules\Authentication\Http\Requests\RestaurantImageSAASSetting\RestaurantImageSAASSettingUpdateRequest;
use Modules\Authentication\Models\RestaurantImageSAASSetting;
use Modules\Authentication\Repositories\RestaurantImageSAASSettingRepository;
class RestaurantImageSAASSettingController extends Controller
{
public function __construct(private RestaurantImageSAASSettingRepository $repo) {}
public function index(): JsonResponse
{
try {
$restaurantImageSAASSetting = RestaurantImageSAASSetting::first();
return $this->responseSuccess($restaurantImageSAASSetting, 'RestaurantImageSAASSetting has been fetched successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(RestaurantImageSAASSettingStoreRequest $request): JsonResponse
{
try {
return $this->responseSuccess($this->repo->create($request->all()), 'RestaurantImageSAASSetting has been created successfully.');
} catch (\Illuminate\Database\QueryException $exception) {
return $this->responseError([], 'Database error: '.$exception->getMessage());
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess($this->repo->getById($id), 'RestaurantImageSAASSetting has been fetched successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function update(RestaurantImageSAASSettingUpdateRequest $request, int $id): JsonResponse
{
try {
return $this->responseSuccess($this->repo->update($id, $request->all()), 'RestaurantImageSAASSetting has been updated successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function destroy(int $id): JsonResponse
{
try {
return $this->responseSuccess($this->repo->delete($id), 'RestaurantImageSAASSetting has been deleted successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\JsonResponse;
use Modules\Authentication\Http\Requests\RestaurantImageSetting\RestaurantImageSettingStoreRequest;
use Modules\Authentication\Http\Requests\RestaurantImageSetting\RestaurantImageSettingUpdateRequest;
use Modules\Authentication\Models\RestaurantImageSetting;
use Modules\Authentication\Repositories\RestaurantImageSettingRepository;
class RestaurantImageSettingController extends Controller
{
public function __construct(private RestaurantImageSettingRepository $repo) {}
public function index(): JsonResponse
{
try {
$restaurantImageSetting = RestaurantImageSetting::where('restaurant_id', getUserRestaurantId())->first();
return $this->responseSuccess($restaurantImageSetting, 'RestaurantImageSetting has been fetched successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(RestaurantImageSettingStoreRequest $request): JsonResponse
{
try {
return $this->responseSuccess($this->repo->create($request->all()), 'RestaurantImageSetting has been created successfully.');
} catch (\Illuminate\Database\QueryException $exception) {
return $this->responseError([], 'Database error: '.$exception->getMessage());
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess($this->repo->getById($id), 'RestaurantImageSetting has been fetched successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function update(RestaurantImageSettingUpdateRequest $request, int $id): JsonResponse
{
try {
return $this->responseSuccess($this->repo->update($id, $request->all()), 'RestaurantImageSetting has been updated successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function destroy(int $id): JsonResponse
{
try {
return $this->responseSuccess($this->repo->delete($id), 'RestaurantImageSetting has been deleted successfully.');
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\HasPermissionTrait;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Authentication\Http\Requests\Role\RoleStoreRequest;
use Modules\Authentication\Http\Requests\Role\RoleUpdateRequest;
use Modules\Authentication\Models\Restaurant;
use Modules\Authentication\Services\PermissionService;
use Modules\Authentication\Services\RoleService;
class RoleController extends Controller
{
use HasPermissionTrait;
public function __construct(
private RoleService $service,
private PermissionService $permissionService
) {}
public function index(Request $request)
{
$perPage = request('perPage') ?? env('PER_PAGE');
$roles = $this->service->index($request, (int) $perPage);
if (! $roles) {
return $this->responseSuccess([], 'No Data Found!');
}
return $this->responseSuccess($roles);
}
public function store(RoleStoreRequest $request)
{
DB::beginTransaction();
try {
$this->service->create($request->validated());
DB::commit();
return $this->responseSuccess([]);
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage().' in '.$e->getFile().' on line '.$e->getLine());
}
}
public function show(int $id)
{
$role = $this->service->getById($id);
if (! $role) {
return $this->responseSuccess([], 'No Data Found!');
}
return $this->responseSuccess($role);
}
public function edit(Request $request, int $id)
{
$permissions = $this->permissionService->getAll($request);
$groupedPermissions = [];
foreach ($permissions as $permission) {
$group = explode('-', $permission->name)[0];
if (! isset($groupedPermissions[$group])) {
$groupedPermissions[$group] = [];
}
$groupedPermissions[$group][] = $permission;
}
return $this->responseSuccess([]);
}
public function update(RoleUpdateRequest $request, int $id)
{
// Block system generated roles (ID: 17)
if ($id >= 1 && $id <= 7) {
return $this->responseError([], 'System-generated roles cannot be updated.', 422);
}
DB::beginTransaction();
try {
$this->service->update($id, $request->validated());
DB::commit();
return $this->responseSuccess([]);
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage().' in '.$e->getFile().' on line '.$e->getLine());
}
}
public function destroy(int $id)
{
// Block system generated roles (ID: 17)
if ($id >= 1 && $id <= 7) {
return $this->responseError([], 'System-generated roles cannot be deleted.', 422);
}
DB::beginTransaction();
try {
$data = $this->service->delete($id);
if (! $data) {
return $this->responseError([], 'Data Not Found!');
}
DB::commit();
return $this->responseSuccess([], 'Data successfully delete!');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], $e->getMessage().' in '.$e->getFile().' on line '.$e->getLine());
}
}
public function restaurantTheme(Request $request)
{
// Validate request
$validated = $request->validate([
'restaurant_id' => 'required|integer|exists:restaurants,id',
'theme_id' => 'required|integer|exists:themes,id',
]);
// Fetch restaurant safely
$restaurant = Restaurant::findOrFail($validated['restaurant_id']);
// Update theme
$restaurant->update([
'theme_id' => $validated['theme_id'],
]);
// Return formatted API response
return response()->json([
'status' => true,
'message' => 'Restaurant theme updated successfully.',
'data' => [
'restaurant_id' => $restaurant->id,
'theme_id' => $restaurant->theme_id,
],
], 200);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\RequestSanitizerTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\Authentication\Models\SAASFaq;
use Modules\Authentication\Repositories\SAASFaqRepository;
use Modules\Frontend\Http\Requests\Faq\FaqStoreRequest;
use Modules\Frontend\Http\Requests\Faq\FaqUpdateRequest;
class SAASFaqController extends Controller
{
use RequestSanitizerTrait;
public function __construct(private SAASFaqRepository $faqQuestion) {}
public function getSaasFaqs(Request $request): JsonResponse
{
$perPage = $request->get('per_page', 10);
$getSaasFaqs = SAASFaq::paginate($perPage);
return $this->responseSuccess($getSaasFaqs, 'Saas Faqs fetched successfully.');
}
public function index(): JsonResponse
{
try {
return $this->responseSuccess(
$this->faqQuestion->getAll(request()->all()),
'Faq has been fetched successfully.'
);
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(FaqStoreRequest $request): JsonResponse
{
try {
return $this->responseSuccess(
$this->faqQuestion->create($request->all()),
'Faq has been created successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->faqQuestion->getById($id),
'Faq has been fetched successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function update(FaqUpdateRequest $request, int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->faqQuestion->update($id, $this->getUpdateRequest($request)),
'Faq has been updated successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function destroy(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->faqQuestion->delete($id),
'Faq has been deleted successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\RequestSanitizerTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\Authentication\Http\Requests\SAASSubscription\SAASSubscriptionStoreRequest;
use Modules\Authentication\Http\Requests\SAASSubscription\SAASSubscriptionUpdateRequest;
use Modules\Authentication\Models\SAASSubscription;
use Modules\Authentication\Repositories\SAASSubscriptionRepository;
class SAASSubscriptionController extends Controller
{
use RequestSanitizerTrait;
public function __construct(private SAASSubscriptionRepository $sAASSubscriptionRepository) {}
public function getSAASSubscriptions(Request $request): JsonResponse
{
$perPage = $request->get('per_page', 10);
$getSAASSubscriptions = SAASSubscription::paginate($perPage);
return $this->responseSuccess($getSAASSubscriptions, 'Saas Subscriptions fetched successfully.');
}
public function index(): JsonResponse
{
try {
return $this->responseSuccess(
$this->sAASSubscriptionRepository->getAll(request()->all()),
'SAAS Subscription has been fetched successfully.'
);
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(SAASSubscriptionStoreRequest $request): JsonResponse
{
try {
return $this->responseSuccess(
$this->sAASSubscriptionRepository->create($request->all()),
'SAAS Subscription has been created successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->sAASSubscriptionRepository->getById($id),
'SAAS Subscription has been fetched successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function update(SAASSubscriptionUpdateRequest $request, int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->sAASSubscriptionRepository->update($id, $this->getUpdateRequest($request)),
'SAAS Subscription has been updated successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function destroy(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->sAASSubscriptionRepository->delete($id),
'SAAS Subscription has been deleted successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function publicStore(SAASSubscriptionStoreRequest $request): JsonResponse
{
$subscriptions = SAASSubscription::create([
'email' => $request->email,
]);
return $this->responseSuccess(
$subscriptions,
'SAAS Subscription has been created successfully.'
);
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
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\DB;
use Illuminate\Support\Str;
use Modules\Authentication\Models\Package;
use Modules\Authentication\Models\Subscription;
use Modules\Authentication\Models\SubscriptionItem;
use Modules\Authentication\Models\SubscriptionUpgradeRequest;
class SAASSubscriptionUpgradeController extends Controller
{
use Authenticatable;
public function requestUpgradeList(Request $request): JsonResponse
{
$perPage = $request->input('per_page', 15);
$search = $request->input('search');
$query = SubscriptionUpgradeRequest::where('status', 'pending')
->with(['restaurant', 'package'])
->when($search, function ($q) use ($search) {
$q->whereHas('restaurant', function ($q2) use ($search) {
$q2->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('phone', 'like', "%{$search}%");
});
})
->orderBy('id', 'desc');
$upgradeRequests = $query->paginate($perPage);
return $this->responseSuccess($upgradeRequests, 'Upgrade requests fetched successfully.');
}
public function requestUpgrade(Request $request): JsonResponse
{
$validated = $request->validate([
'package_id' => 'required|exists:packages,id',
'notes' => 'nullable|string',
]);
$restaurantId = $this->getCurrentRestaurantId(); // assume this gets the current user's restaurant ID
$package = Package::findOrFail($validated['package_id']);
// Check for existing pending upgrade request for the same restaurant & package
$existingPending = SubscriptionUpgradeRequest::where('restaurant_id', $restaurantId)
// ->where('package_id', $package->id)
->where('status', 'pending')
->first();
if ($existingPending) {
// Update the existing pending request
$existingPending->update([
'extra_days' => (int) $package->duration_days,
'payment_method' => 'online', // or from config
'amount_paid' => $package->price,
'notes' => $validated['notes'] ?? null,
]);
$upgradeRequest = $existingPending;
} else {
// Create a new upgrade request
$upgradeRequest = SubscriptionUpgradeRequest::create([
'restaurant_id' => $restaurantId,
'package_id' => $package->id,
'extra_days' => (int) $package->duration_days,
'payment_method' => 'online',
'amount_paid' => $package->price,
'notes' => $validated['notes'] ?? null,
'status' => 'pending',
]);
}
return $this->responseSuccess($upgradeRequest, 'Upgrade request submitted successfully.');
}
public function approveRequest(int $requestId): JsonResponse
{
DB::beginTransaction();
try {
$subscriptionRequest = SubscriptionUpgradeRequest::where('id', $requestId)->where('status', 'pending')->first();
if (! $subscriptionRequest) {
return $this->responseError([], 'No pending request found.', 404);
}
$package = Package::findOrFail($subscriptionRequest->package_id);
$extraDays = $subscriptionRequest->extra_days ?? 0;
$startDate = Carbon::now();
$endDate = $startDate->copy()->addDays($package->duration_days + $extraDays);
// Update or create subscription
$subscription = Subscription::where('restaurant_id', $subscriptionRequest->restaurant_id)->first();
if ($subscription) {
$subscription->update([
'package_id' => $package->id,
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
'transactions_details' => json_encode([
'invoice_no' => 'INV-'.strtoupper(Str::random(6)),
'issued_at' => now()->toDateString(),
'notes' => $subscriptionRequest->notes,
'amount' => $subscriptionRequest->amount_paid,
]),
]);
} else {
$subscription = Subscription::create([
'restaurant_id' => $subscriptionRequest->restaurant_id,
'package_id' => $package->id,
'user_id' => Auth::id(),
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
'transactions_details' => json_encode([
'invoice_no' => 'INV-'.strtoupper(Str::random(6)),
'issued_at' => now()->toDateString(),
'notes' => $subscriptionRequest->notes,
'amount' => $subscriptionRequest->amount_paid,
]),
]);
}
SubscriptionItem::create([
'user_id' => Auth::id(),
'restaurant_id' => $subscriptionRequest->restaurant_id,
'subscription_id' => $subscription->id,
'reference_no' => 'PAY-'.strtoupper(Str::random(8)),
'amount_paid' => $subscriptionRequest->amount_paid,
'payment_method' => $subscriptionRequest->payment_method,
]);
$subscriptionRequest->update([
'status' => 'approved',
'approved_by' => auth()->id(),
'approved_at' => now(),
]);
DB::commit();
return $this->responseSuccess($subscription, 'Subscription request approved successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], 'Error: '.$e->getMessage());
}
}
public function paymentSubscriptions(Request $request): JsonResponse
{
$request->validate([
'restaurant_id' => 'required|exists:restaurants,id',
'package_id' => 'required|exists:packages,id',
'upgrade_request_id' => 'nullable|exists:subscription_upgrade_requests,id',
'payment_method' => 'nullable|string',
'notes' => 'nullable|string',
'amount_paid' => 'nullable|numeric',
'extra_days' => 'nullable|integer',
]);
DB::beginTransaction();
try {
$package = Package::findOrFail((int) $request->package_id);
$startDate = Carbon::now();
$extraDays = $request->input('extra_days', 0);
$endDate = $startDate->copy()->addDays($package->duration_days + $extraDays);
$subscription = Subscription::where('restaurant_id', (int) $request->restaurant_id)->first();
$invoiceData = [
'invoice_no' => 'INV-'.strtoupper(Str::random(6)),
'issued_at' => now()->toDateString(),
'notes' => $request->input('notes', 'Payment for upgrade'),
'amount' => $request->input('amount_paid', $package->price),
];
if ($subscription) {
$subscription->update([
'package_id' => $package->id,
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
'transactions_details' => $invoiceData,
]);
} else {
$subscription = Subscription::create([
'restaurant_id' => (int) $request->restaurant_id,
'package_id' => $package->id,
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 1,
'transactions_details' => $invoiceData,
]);
}
SubscriptionItem::create([
'subscription_id' => $subscription->id,
'reference_no' => 'PAY-'.strtoupper(Str::random(8)),
'amount_paid' => $invoiceData['amount'],
'payment_method' => $request->input('payment_method', 'online'),
]);
// If upgrading through a pending request, mark it as approved
if ($request->filled('upgrade_request_id')) {
$upgradeRequest = SubscriptionUpgradeRequest::where('id', $request->upgrade_request_id)
->where('status', 'pending')
->first();
if (! $upgradeRequest) {
return $this->responseError([], 'No pending upgrade request found.', 404);
}
$upgradeRequest->update([
'status' => 'approved',
'approved_by' => auth()->id(),
'approved_at' => now(),
]);
}
DB::commit();
return $this->responseSuccess($subscription, 'Subscription payment processed successfully.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError([], 'Error: '.$e->getMessage());
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\RequestSanitizerTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Modules\Authentication\Http\Requests\Auth\UserCreateRequest;
use Modules\Authentication\Http\Requests\Auth\UserUpdateRequest;
use Modules\Authentication\Repositories\UserRepository;
class UserController extends Controller
{
use RequestSanitizerTrait;
public function __construct(private UserRepository $user) {}
public function index(): JsonResponse
{
try {
return $this->responseSuccess(
$this->user->getAll(request()->all()),
'User has been fetched successfully.'
);
} catch (Exception $e) {
return $this->responseError([], $e->getMessage());
}
}
public function store(UserCreateRequest $request): JsonResponse
{
try {
return $this->responseSuccess(
$this->user->create($request->all()),
'User has been created successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function show(int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->user->getById($id),
'User has been fetched successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function update(UserUpdateRequest $request, int $id): JsonResponse
{
try {
return $this->responseSuccess(
$this->user->update($id, $this->getUpdateRequest($request)),
'User has been updated successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
public function destroy(int $id): JsonResponse
{
try {
$user = $this->user->getById($id);
if (! $user) {
return $this->responseError([], 'User Not Found');
}
if ($user->image) {
fileRemover('users/', $user->image);
}
return $this->responseSuccess(
$this->user->delete($id),
'User has been deleted successfully.'
);
} catch (Exception $exception) {
return $this->responseError([], $exception->getMessage(), $exception->getCode());
}
}
}

View File

@@ -0,0 +1,447 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Helper\DomainHelper;
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\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Modules\Authentication\Http\Requests\Settings\SettingUpdateRequest;
use Modules\Authentication\Models\Setting;
use Modules\Authentication\Models\User;
use Modules\HRM\Models\Attendance;
use Modules\HRM\Models\AttendanceLog;
class UtilityController extends Controller
{
use Authenticatable;
public function restaurantSettingsUpdate(SettingUpdateRequest $request): JsonResponse
{
try {
if ($request->isMethod('get')) {
$settings = Setting::where('restaurant_id', $this->getCurrentRestaurantId())
->pluck('option_value', 'option_key'); // Return settings as key-value pairs
return $this->responseSuccess($settings, 'System Settings fetched successfully.');
}
// If POST request, update or create settings
DB::beginTransaction();
foreach ($request->validated() as $key => $value) {
if ($key === '_token') {
continue;
}
Setting::updateOrInsert(
[
'restaurant_id' => $this->getCurrentRestaurantId(),
'option_key' => $key,
],
[
'option_value' => $value,
'updated_at' => now(),
'created_at' => now(),
]
);
}
DB::commit();
return $this->responseSuccess([], 'System Settings have been successfully updated.');
} catch (Exception $e) {
DB::rollBack();
return $this->responseError(['error' => $e->getMessage()], 'Failed to store records unsuccessfully.');
}
}
public function restaurantLogoUpdate(Request $request): JsonResponse
{
$request->validate([
'logo' => 'required|image|mimes:jpeg,png,jpg|max:8192',
]);
$image = $request->file('logo');
$name = 'logo.'.$image->getClientOriginalExtension();
$destinationPath = public_path('/uploads/logos');
$image->move($destinationPath, $name);
$data = [];
$data['option_value'] = $name;
$data['updated_at'] = Carbon::now();
if (Setting::where('restaurant_id', $this->getCurrentRestaurantId())->where('option_key', 'app_logo')->exists()) {
Setting::where('restaurant_id', $this->getCurrentRestaurantId())->where('option_key', '=', 'app_logo')->update($data);
} else {
$data['option_value'] = 'app_logo';
$data['created_at'] = Carbon::now();
Setting::insert($data);
}
return $this->responseSuccess(
[],
'Restaurant logo have been successfully updated'
);
}
public function getAllImageFolderUrls(): JsonResponse
{
$uploadBasePath = public_path('uploads');
$storageBasePath = public_path('storage');
$uploadBaseUrl = url('uploads');
$storageBaseUrl = url('storage');
$data = [];
// Helper to recursively get folders and create URLs
$getFolders = function ($basePath, $baseUrl, $prefix = '') use (&$data) {
if (! File::exists($basePath)) {
return;
}
$folders = File::directories($basePath);
foreach ($folders as $folderPath) {
$relativePath = str_replace($basePath.DIRECTORY_SEPARATOR, '', $folderPath);
$key = str_replace(['/', '\\'], '_', ($prefix.$relativePath)).'_url';
$data[$key] = str_replace('\\', '/', $baseUrl.'/'.$relativePath.'/');
// Recursive scan subfolders
$thisFolder = $basePath.DIRECTORY_SEPARATOR.$relativePath;
$getSub = File::directories($thisFolder);
foreach ($getSub as $subFolder) {
$subRelative = str_replace($basePath.DIRECTORY_SEPARATOR, '', $subFolder);
$key = str_replace(['/', '\\'], '_', ($prefix.$subRelative)).'_url';
$data[$key] = str_replace('\\', '/', $baseUrl.'/'.$subRelative.'/');
}
}
};
// Scan and build URLs from public/uploads
$getFolders($uploadBasePath, $uploadBaseUrl);
// Scan and build URLs from public/storage
$getFolders($storageBasePath, $storageBaseUrl, 'storage_');
// Add any manually defined static folders (optional)
$data['system_logo_url'] = url('/uploads/logos/');
return response()->json([
'status' => 'success',
'message' => 'All image folder URLs fetched successfully.',
'data' => $data,
]);
}
public function linkStorage(): JsonResponse
{
$publicStorage = public_path('storage');
$target = storage_path('app/public');
try {
if (! File::exists($publicStorage)) {
File::link($target, $publicStorage);
return response()->json(['status' => 'success', 'message' => 'Storage link created.']);
} else {
return response()->json(['status' => 'info', 'message' => 'Storage link already exists.']);
}
} catch (Exception $e) {
return response()->json(['status' => 'error', 'message' => $e->getMessage()]);
}
}
public function unlinkStorage(): JsonResponse
{
$publicStorage = public_path('storage');
try {
if (is_link($publicStorage)) {
unlink($publicStorage);
} elseif (is_dir($publicStorage)) {
// Attempt to delete via Laravel
if (! File::deleteDirectory($publicStorage)) {
// Fallback for stubborn directories
exec('rm -rf '.escapeshellarg($publicStorage));
}
}
return response()->json([
'status' => 'success',
'message' => 'The storage link or directory has been removed.',
]);
} catch (Exception $e) {
return response()->json([
'status' => 'error',
'message' => 'Failed: '.$e->getMessage(),
]);
}
}
public function clearAllCache(): JsonResponse
{
try {
Artisan::call('config:clear');
Artisan::call('cache:clear');
Artisan::call('route:clear');
Artisan::call('view:clear');
return response()->json(['status' => 'success', 'message' => 'All caches cleared.']);
} catch (Exception $e) {
return response()->json(['status' => 'error', 'message' => $e->getMessage()]);
}
}
// Predefined models map (from your dump)
protected $models = [
'gallery' => 'Modules\\Authentication\\Models\\Gallery',
'restaurant' => 'Modules\\Authentication\\Models\\Restaurant',
'setting' => 'Modules\\Authentication\\Models\\Setting',
'user' => 'Modules\\Authentication\\Models\\User',
'aboutus' => 'Modules\\Frontend\\Models\\AboutUs',
'banner' => 'Modules\\Frontend\\Models\\Banner',
'contact' => 'Modules\\Frontend\\Models\\Contact',
'faqquestion' => 'Modules\\Frontend\\Models\\FaqQuestion',
'mobileappsection' => 'Modules\\Frontend\\Models\\MobileAppSection',
'onboarding' => 'Modules\\Frontend\\Models\\Onboarding',
'ourhistory' => 'Modules\\Frontend\\Models\\OurHistory',
'page' => 'Modules\\Frontend\\Models\\Page',
'policy' => 'Modules\\Frontend\\Models\\Policy',
'readytojoinus' => 'Modules\\Frontend\\Models\\ReadyToJoinUs',
'testimonial' => 'Modules\\Frontend\\Models\\Testimonial',
'theme' => 'Modules\\Frontend\\Models\\Theme',
'whychooseus' => 'Modules\\Frontend\\Models\\WhyChooseUs',
'paymentrequest' => 'Modules\\Gateways\\Models\\PaymentRequest',
'addon' => 'Modules\\Restaurant\\Models\\Addon',
'addonfood' => 'Modules\\Restaurant\\Models\\AddonFood',
'customer' => 'Modules\\Restaurant\\Models\\Customer',
'foodavailability' => 'Modules\\Restaurant\\Models\\FoodAvailability',
'foodcategory' => 'Modules\\Restaurant\\Models\\FoodCategory',
'fooditem' => 'Modules\\Restaurant\\Models\\FoodItem',
'foodvariant' => 'Modules\\Restaurant\\Models\\FoodVariant',
'foodvariantingredient' => 'Modules\\Restaurant\\Models\\FoodVariantIngredient',
'ingredient' => 'Modules\\Restaurant\\Models\\Ingredient',
'ingredientdamage' => 'Modules\\Restaurant\\Models\\IngredientDamage',
'menutype' => 'Modules\\Restaurant\\Models\\MenuType',
'menusection' => 'Modules\\Restaurant\\Models\\MenuSection',
'menucategory' => 'Modules\\Restaurant\\Models\\MenuCategory',
'order' => 'Modules\\Restaurant\\Models\\Order',
'orderitem' => 'Modules\\Restaurant\\Models\\OrderItem',
'paymentmethod' => 'Modules\\Restaurant\\Models\\PaymentMethod',
'purchase' => 'Modules\\Restaurant\\Models\\Purchase',
'purchaseitem' => 'Modules\\Restaurant\\Models\\PurchaseItem',
'purchasereturn' => 'Modules\\Restaurant\\Models\\PurchaseReturn',
'reservation' => 'Modules\\Restaurant\\Models\\Reservation',
'reservationunavailable' => 'Modules\\Restaurant\\Models\\ReservationUnavailable',
'stock' => 'Modules\\Restaurant\\Models\\Stock',
'supplier' => 'Modules\\Restaurant\\Models\\Supplier',
'table' => 'Modules\\Restaurant\\Models\\Table',
'unit' => 'Modules\\Restaurant\\Models\\Unit',
'restaurantschedules' => 'Modules\\Restaurant\\Models\\RestaurantSchedule',
'kitchen' => 'Modules\\Restaurant\\Models\\Kitchen',
'unitmeasurements' => 'Modules\\Restaurant\\Models\\Kitchen',
'hotel' => 'Modules\\Booking\\Models\\Hotel',
'floor' => 'Modules\\Booking\\Models\\Floor',
'roomtype' => 'Modules\\Booking\\Models\\RoomType',
'room' => 'Modules\\Booking\\Models\\Room',
'booking' => 'Modules\\Booking\\Models\\Booking',
];
public function globalStatusUpdate(Request $request)
{
$request->validate([
'id' => 'required|integer',
'type' => 'required|string',
'status' => 'required|string',
]);
$type = strtolower($request->type);
$id = $request->id;
$status = $request->status;
if (! isset($this->models[$type])) {
return $this->responseError([], 'Invalid type', 400);
}
$modelClass = $this->models[$type];
$item = $modelClass::find($id);
if (! $item) {
return $this->responseError([], 'Invalid type', 400);
}
$item->status = $status;
$item->save();
return $this->responseSuccess(
$item,
'Status updated successfully.'
);
}
private function getClassFromFile($file, $modulePath)
{
$relative = str_replace(base_path().'/', '', $file->getPathname());
$class = str_replace('/', '\\', substr($relative, 0, -4)); // remove .php
return $class;
}
public function domainCheck(Request $request): JsonResponse
{
$domain = $request->query('domain');
$info = DomainHelper::getDomainInfo($domain);
// Add expiration warning
$warning = DomainHelper::expirationWarning($info['expiration'] ?? null);
$info['expiration_warning'] = $warning;
return response()->json([
'success' => true,
'data' => $info,
]);
}
public function allUsersGet(Request $request)
{
$request->validate([
'restaurant_id' => 'required|integer|exists:restaurants,id',
]);
$users = User::select('id', 'first_name', 'last_name', 'user_type', 'role_id')
->where('restaurant_id', (int) $request->restaurant_id)
->get();
return $this->responseSuccess(
$users,
_lang('Users have been fetched successfully.')
);
}
public function sync(Request $request)
{
$request->validate([
'logs' => ['required', 'array'],
'logs.*.id' => ['required', 'integer'],
'logs.*.timestamp' => ['required', 'date'],
'logs.*.type' => ['required', 'integer'],
]);
$restaurantId = $this->getCurrentRestaurantId(); // or from device
$inserted = 0;
$skipped = 0;
DB::beginTransaction();
try {
foreach ($request->logs as $log) {
$employeeId = $log['id'];
$dateTime = Carbon::parse($log['timestamp']);
$date = $dateTime->toDateString();
$type = $this->mapPunchType($log['type']);
/**
* 1️⃣ Avoid duplicate punch
*/
$exists = AttendanceLog::where([
'employee_id' => $employeeId,
'restaurant_id' => $restaurantId,
'punch_time' => $dateTime,
'type' => $type,
])->exists();
if ($exists) {
$skipped++;
continue;
}
/**
* 2️⃣ Get or create daily attendance
*/
$attendance = Attendance::firstOrCreate(
[
'employee_id' => $employeeId,
'date' => $date,
],
[
'restaurant_id' => $restaurantId,
'status' => 'present',
]
);
/**
* 3️⃣ Insert punch log
*/
AttendanceLog::create([
'attendance_id' => $attendance->id,
'employee_id' => $employeeId,
'restaurant_id' => $restaurantId,
'type' => $type,
'punch_time' => $dateTime,
]);
/**
* 4️⃣ Recalculate attendance summary
*/
$this->recalculateWorkedHours($attendance);
$inserted++;
}
DB::commit();
return response()->json([
'status' => true,
'inserted' => $inserted,
'skipped' => $skipped,
'message' => 'Attendance synced successfully',
]);
} catch (\Throwable $e) {
DB::rollBack();
return response()->json([
'status' => false,
'message' => 'Attendance sync failed',
'error' => $e->getMessage(),
], 500);
}
}
private function mapPunchType(int $type): string
{
return in_array($type, [0, 4]) ? 'in' : 'out';
}
/**
* RECALCULATE TOTAL WORKED HOURS BASED ON ALL LOGS
*/
private function recalculateWorkedHours(Attendance $attendance)
{
$logs = AttendanceLog::where('attendance_id', $attendance->id)
->orderBy('punch_time')
->get();
if ($logs->count() < 2) {
return;
}
$first = Carbon::parse($logs->first()->punch_time);
$last = Carbon::parse($logs->last()->punch_time);
$hours = round($first->diffInMinutes($last) / 60, 2);
$attendance->update([
'first_clock_in' => $first,
'last_clock_out' => $last,
'hours_worked' => $hours,
]);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Modules\Authentication\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Traits\Authenticatable;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Modules\Authentication\Models\ZoomMeeting;
class ZoomController extends Controller
{
use Authenticatable;
private $zoomToken;
public function __construct()
{
$this->zoomToken = $this->generateZoomToken();
}
private function generateZoomToken()
{
$response = Http::asForm()->withHeaders([
'Authorization' => 'Basic '.base64_encode(config('zoom.client_id').':'.config('zoom.client_secret')),
])->post('https://zoom.us/oauth/token', [
'grant_type' => 'account_credentials',
'account_id' => config('zoom.account_id'),
]);
return $response->json()['access_token'] ?? null;
}
public function index(Request $request): JsonResponse
{
$perPage = request('per_page', 10);
$zoomMeetings = ZoomMeeting::where('start_time', '>=', Carbon::now('Asia/Dhaka')->toDateTimeString())
->latest()
->paginate($perPage);
return $this->responseSuccess(
$zoomMeetings,
'Zoom Meetings list fetch successfully.'
);
}
public function store(Request $request): JsonResponse
{
// Validate the request data
$request->validate([
'topic' => 'required|string|max:255',
'start_time' => 'required|date_format:Y-m-d\TH:i:s\Z', // ISO 8601 format (Asia/Dhaka)
'duration' => 'required|integer|min:1', // Duration in minutes
'password' => 'nullable|string|min:6|max:10', // Optional password, min 6 chars
'agenda' => 'nullable|string|max:5000',
]);
$startTimeUtc = Carbon::parse($request->start_time, 'Asia/Dhaka')->setTimezone('Asia/Dhaka')->toIso8601String();
$response = Http::withToken($this->zoomToken)->post(config('zoom.base_url').'users/me/meetings', [
'topic' => $request->topic,
'type' => 2, // Scheduled Meeting
'start_time' => $startTimeUtc,
'duration' => $request->duration,
'password' => '123456',
'agenda' => 'Live Class',
'timezone' => 'Asia/Dhaka',
'settings' => [
'host_video' => true,
'participant_video' => true,
'waiting_room' => false,
],
]);
if ($response->successful()) {
$data = $response->json();
$meeting = ZoomMeeting::create([
'restaurant_id' => $this->getCurrentRestaurantId(),
'meeting_id' => $data['id'],
'topic' => $data['topic'],
'agenda' => 'Live Class',
'start_time' => $data['start_time'],
'duration' => $data['duration'],
'password' => '123456',
'join_url' => $data['join_url'],
'start_url' => $data['start_url'],
'created_by' => Auth::id(),
]);
return $this->responseSuccess(
$meeting,
'Zoom Meeting create successfully.'
);
}
return $this->responseError([], 'Zoom API Error');
}
public function show(string $id): JsonResponse
{
$zoomMeeting = ZoomMeeting::where('meeting_id', $id)->first();
if (! $zoomMeeting) {
return $this->responseError($zoomMeeting, 'Zoom Meeting not found');
}
return $this->responseSuccess(
$zoomMeeting,
'Zoom Meeting fetch successfully.'
);
}
public function update(Request $request, int $id): JsonResponse
{
$zoomMeeting = ZoomMeeting::where('meeting_id', $id)->first();
if (! $zoomMeeting) {
return $this->responseError($zoomMeeting, 'Zoom Meeting not found');
}
$response = Http::withToken($this->zoomToken)->patch(config('zoom.base_url')."meetings/{$id}", [
'topic' => $request->topic ?? $zoomMeeting->topic,
'start_time' => $request->start_time ?? $zoomMeeting->start_time,
'duration' => $request->duration ?? $zoomMeeting->duration,
'password' => $request->password ?? $zoomMeeting->password,
]);
if ($response->successful()) {
$zoomMeeting->update($request->only(['topic', 'start_time', 'duration', 'password']));
return $this->responseSuccess(
$zoomMeeting,
'Zoom Meeting update successfully.'
);
}
return $this->responseError([], 'Failed to update meeting');
}
public function destroy(int $id): JsonResponse
{
$zoomMeeting = ZoomMeeting::where('meeting_id', $id)->first();
if (! $zoomMeeting) {
return $this->responseError($zoomMeeting, 'Zoom Meeting not found');
}
$response = Http::withToken($this->zoomToken)->delete(config('zoom.base_url')."meetings/{$id}");
if ($response->successful()) {
$zoomMeeting->delete();
return $this->responseSuccess(
[],
'Zoom Meeting deleted successfully.'
);
}
return $this->responseError([], 'Failed to delete meeting');
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Modules\Authentication\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Modules\Authentication\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class UserCreateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'first_name' => ['required', 'string', 'max:255', 'regex:/^[a-zA-Z\s]+$/'],
'last_name' => ['nullable', 'string', 'max:255', 'regex:/^[a-zA-Z\s]+$/'],
'email' => ['nullable', 'string', 'email', 'max:255', 'regex:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', 'unique:users,email'],
'phone' => ['required', 'string', 'regex:/^(\+?\d{1,4}|\d{1,4})?\d{7,15}$/', 'unique:users,phone'],
'password' => ['required', 'confirmed', 'string', 'min:8'],
'role_id' => ['required', 'integer', 'exists:roles,id'],
'avatar' => ['nullable'],
];
}
public function messages()
{
return [
'email.unique' => 'The email has already been taken.',
'phone.unique' => 'The phone number has already been registered.',
'password.confirmed' => 'The password confirmation does not match.',
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Modules\Authentication\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'first_name' => ['required', 'string', 'max:255', 'regex:/^[a-zA-Z\s]+$/'],
'last_name' => ['nullable', 'string', 'max:255', 'regex:/^[a-zA-Z\s]+$/'],
'email' => [
'nullable',
'string',
'email',
'max:255',
'regex:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
],
'phone' => [
'nullable',
'string',
'regex:/^(\+?\d{1,4}|\d{1,4})?\d{7,15}$/',
],
'role_id' => ['required', 'integer', 'exists:roles,id'],
'avatar' => ['nullable'],
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Modules\Authentication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PermissionStoreRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|max:255',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Modules\Authentication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PermissionUpdateRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|max:255',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Modules\Authentication\Http\Requests\Plan;
use Illuminate\Foundation\Http\FormRequest;
class PlanStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'student_limit' => 'required|integer|min:0',
'branch_limit' => 'required|integer|min:0',
'price' => 'required|numeric|min:0|max:999999.99',
'duration_days' => 'nullable|integer|min:1',
'is_custom' => 'required|boolean',
'is_free' => 'required|boolean',
'status' => 'nullable|in:0,1',
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Modules\Authentication\Http\Requests\Plan;
use Illuminate\Foundation\Http\FormRequest;
class PlanUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'student_limit' => 'sometimes|required|integer|min:0',
'branch_limit' => 'sometimes|required|integer|min:0',
'price' => 'sometimes|required|numeric|min:0|max:999999.99',
'duration_days' => 'nullable|integer|min:1',
'is_custom' => 'sometimes|required|boolean',
'is_free' => 'sometimes|required|boolean',
'status' => 'nullable|required|in:0,1',
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Modules\Authentication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Modules\Authentication\Models\User;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Modules\Authentication\Http\Requests\Restaurant;
use Illuminate\Foundation\Http\FormRequest;
class RestaurantStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100', 'regex:/^[a-zA-Z\s]+$/u'],
'email' => [
'nullable',
'string',
'email:rfc,dns',
'max:100',
'unique:users,email', // Ensure unique email for new users
],
'phone' => [
'required',
'string',
'regex:/^(\+?\d{1,4}|\d{1,4})?\d{7,15}$/',
'unique:users,phone', // Ensure unique phone for new users
],
'restaurant_type' => ['required', 'string', 'max:100'],
'domain' => ['required', 'string', 'max:100', 'regex:/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z]{2,}$/'],
'address' => ['required', 'string', 'max:191'],
'logo' => ['nullable'],
'platform' => ['nullable', 'string', 'in:WEB,APP'],
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Modules\Authentication\Http\Requests\Restaurant;
use Illuminate\Foundation\Http\FormRequest;
class RestaurantUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['nullable', 'string', 'max:100', 'regex:/^[a-zA-Z\s]+$/u'],
'restaurant_type' => ['nullable', 'string', 'max:100'],
'address' => ['nullable', 'string', 'max:191'],
'logo' => ['nullable'],
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\Authentication\Http\Requests\RestaurantImageSAASSetting;
use Illuminate\Foundation\Http\FormRequest;
class RestaurantImageSAASSettingStoreRequest extends FormRequest
{
public function rules(): array
{
return [
'header_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'header_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'banner_image' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\Authentication\Http\Requests\RestaurantImageSAASSetting;
use Illuminate\Foundation\Http\FormRequest;
class RestaurantImageSAASSettingUpdateRequest extends FormRequest
{
public function rules(): array
{
return [
'header_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'header_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'banner_image' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\Authentication\Http\Requests\RestaurantImageSetting;
use Illuminate\Foundation\Http\FormRequest;
class RestaurantImageSettingStoreRequest extends FormRequest
{
public function rules(): array
{
return [
'header_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'header_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'banner_image' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\Authentication\Http\Requests\RestaurantImageSetting;
use Illuminate\Foundation\Http\FormRequest;
class RestaurantImageSettingUpdateRequest extends FormRequest
{
public function rules(): array
{
return [
'header_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'header_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_light_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'footer_logo_dark_theme' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'banner_image' => ['nullable', 'image', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Modules\Authentication\Http\Requests\Role;
use Illuminate\Foundation\Http\FormRequest;
class RoleStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required',
'permissions' => 'array',
];
}
public function messages()
{
return [
'name.required' => 'Role name is required',
'permissions.array' => 'Permissions must be an array',
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Modules\Authentication\Http\Requests\Role;
use Illuminate\Foundation\Http\FormRequest;
class RoleUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required',
'permissions' => 'array',
];
}
public function messages()
{
return [
'name.required' => 'Role name is required',
'permissions.array' => 'Permissions must be an array',
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Modules\Authentication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RoleStoreRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:255',
'permissions' => 'required|array',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Modules\Authentication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RoleUpdateRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:255',
'permissions' => 'required|array',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Modules\Authentication\Http\Requests\SAASSubscription;
use Illuminate\Foundation\Http\FormRequest;
class SAASSubscriptionStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'email' => [
'nullable',
'string',
'email:rfc,dns',
'max:100',
'unique:s_a_a_s_subscriptions,email', // Ensure unique email for new s_a_a_s_subscriptions
],
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Modules\Authentication\Http\Requests\SAASSubscription;
use Illuminate\Foundation\Http\FormRequest;
class SAASSubscriptionUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'email' => [
'nullable',
'string',
'email:rfc,dns',
'max:100',
],
];
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Modules\Authentication\Http\Requests\Settings;
use Illuminate\Foundation\Http\FormRequest;
class MailConfigUpdateRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'MAIL_MAILER' => 'required|string|max:50',
'MAIL_HOST' => 'required|string|max:255',
'MAIL_PORT' => 'required|numeric',
'MAIL_USERNAME' => 'nullable|string|max:255',
'MAIL_PASSWORD' => 'nullable|string|max:255',
'MAIL_FROM_ADDRESS' => 'required|email|max:255',
'MAIL_FROM_NAME' => 'required|string|max:255',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Modules\Authentication\Http\Requests\Settings;
use Illuminate\Foundation\Http\FormRequest;
class SAASSettingUpdateRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'site_name' => 'nullable|string|max:255',
'site_title' => 'nullable|string|max:255',
'phone' => 'nullable|string|max:255',
'office_phone' => 'nullable|string|max:255',
'email' => 'nullable|string|max:255',
'language' => 'nullable|string|max:255',
'google_map' => 'nullable|string|max:255',
'address' => 'nullable|string|max:255',
'on_google_map' => 'nullable|string|max:255',
'currency_symbol' => 'nullable|string|max:255',
'logo' => 'nullable|string|max:255',
'disabled_website' => 'nullable|string|max:255',
'copyright_text' => 'nullable|string|max:255',
'facebook_link' => 'nullable|string|max:255',
'google_plus_link' => 'nullable|string|max:255',
'youtube_link' => 'nullable|string|max:255',
'whats_app_link' => 'nullable|string|max:255',
'twitter_link' => 'nullable|string|max:255',
'header_notice' => 'nullable|string|max:255',
'app_version' => 'nullable|string|max:255',
'app_url' => 'nullable|string|max:255',
'bkash' => 'nullable|string|max:255',
'paystack' => 'nullable|string|max:255',
'razor_pay' => 'nullable|string|max:255',
'stripe' => 'nullable|string|max:255',
'ssl_commerz' => 'nullable|string|max:255',
'pvit' => 'nullable|string|max:255',
'paypal' => 'nullable|string|max:255',
'paymob_accept' => 'nullable|string|max:255',
'flutterwave' => 'nullable|string|max:255',
'senang_pay' => 'nullable|string|max:255',
'paytm' => 'nullable|string|max:255',
'primary_color' => 'nullable|string|max:255',
'secondary_color' => 'nullable|string|max:255',
'primary_container_color' => 'nullable|string|max:255',
'dark_primary_color' => 'nullable|string|max:255',
'dark_secondary_color' => 'nullable|string|max:255',
'dark_container_color' => 'nullable|string|max:255',
'text_color' => 'nullable|string|max:255',
'dark_text_color' => 'nullable|string|max:255',
'sidebar_selected_bg_color' => 'nullable|string|max:255',
'sidebar_selected_text_color' => 'nullable|string|max:255',
'vercel_project_id' => 'nullable|string|max:255',
'vercel_token' => 'nullable|string|max:255',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Modules\Authentication\Http\Requests\Settings;
use Illuminate\Foundation\Http\FormRequest;
class SettingUpdateRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'restaurant_name' => 'nullable|string|max:255',
'site_title' => 'nullable|string|max:255',
'phone' => 'nullable|string|max:255',
'email' => 'nullable|string|max:255',
'language' => 'nullable|string|max:255',
'google_map' => 'nullable|string|max:255',
'address' => 'nullable|string|max:255',
'on_google_map' => 'nullable|string|max:255',
'restaurant_code' => 'nullable|string|max:255',
'currency_symbol' => 'nullable|string|max:255',
'logo' => 'nullable|string|max:255',
'mail_type' => 'nullable|string|max:255',
'disabled_website' => 'nullable|string|max:255',
'copyright_text' => 'nullable|string|max:255',
'facebook_link' => 'nullable|string|max:255',
'google_plus_link' => 'nullable|string|max:255',
'youtube_link' => 'nullable|string|max:255',
'whats_app_link' => 'nullable|string|max:255',
'twitter_link' => 'nullable|string|max:255',
'eiin_code' => 'nullable|string|max:255',
'sms_gateway' => 'nullable|string|max:255',
'bulk_sms_api_key' => 'nullable|string|max:255',
'bulk_sms_sender_id' => 'nullable|string|max:255',
'twilio_sid' => 'nullable|string|max:255',
'twilio_token' => 'nullable|string|max:255',
'twilio_from_number' => 'nullable|string|max:255',
'header_notice' => 'nullable|string|max:255',
'app_version' => 'nullable|string|max:255',
'app_url' => 'nullable|string|max:255',
'tagline' => 'nullable|string|max:255',
'favicon' => 'nullable|string|max:255',
'theme_color' => 'nullable|string|max:255',
'background_image' => 'nullable|string|max:255',
'tax_type' => 'nullable|string|max:255',
'tax_percentage' => 'nullable|string|max:255',
'service_charge' => 'nullable|string|max:255',
'default_currency' => 'nullable|string|max:255',
'billing_prefix' => 'nullable|string|max:255',
'invoice_footer' => 'nullable|string|max:255',
'enable_kitchen_print' => 'nullable|string|max:255',
'enable_customer_copy' => 'nullable|string|max:255',
'enable_online_order' => 'nullable|string|max:255',
'delivery_charge' => 'nullable|string|max:255',
'minimum_order_amount' => 'nullable|string|max:255',
'auto_accept_order' => 'nullable|string|max:255',
'estimated_preparation_time' => 'nullable|string|max:255',
'slack_webhook_url' => 'nullable|string|max:255',
'telegram_bot_token' => 'nullable|string|max:255',
'telegram_chat_id' => 'nullable|string|max:255',
'twilio_sms_enabled' => 'nullable|string|max:255',
'email_notifications' => 'nullable|string|max:255',
'whatsapp_notifications' => 'nullable|string|max:255',
'auto_backup' => 'nullable|string|max:255',
'report_timezone' => 'nullable|string|max:255',
'data_retention_days' => 'nullable|string|max:255',
'sidebar_collapsed' => 'nullable|string|max:255',
'dark_mode' => 'nullable|string|max:255',
'default_dashboard' => 'nullable|string|max:255',
'razorpay_key' => 'nullable|string|max:255',
'razorpay_secret' => 'nullable|string|max:255',
'stripe_key' => 'nullable|string|max:255',
'stripe_secret' => 'nullable|string|max:255',
'cash_on_delivery' => 'nullable|string|max:255',
'max_table_capacity' => 'nullable|string|max:255',
'default_shift_start' => 'nullable|string|max:255',
'default_shift_end' => 'nullable|string|max:255',
'auto_logout_idle_minutes' => 'nullable|string|max:255',
'primary_color' => 'nullable|string|max:255',
'secondary_color' => 'nullable|string|max:255',
'primary_container_color' => 'nullable|string|max:255',
'dark_primary_color' => 'nullable|string|max:255',
'dark_secondary_color' => 'nullable|string|max:255',
'dark_container_color' => 'nullable|string|max:255',
'text_color' => 'nullable|string|max:255',
'dark_text_color' => 'nullable|string|max:255',
'sidebar_selected_bg_color' => 'nullable|string|max:255',
'sidebar_selected_text_color' => 'nullable|string|max:255',
'is_online' => 'nullable|string|max:255',
'latitude' => 'nullable|string|max:255',
'longitude' => 'nullable|string|max:255',
'delivery_radius_km' => 'nullable|string|max:255',
'delivery_fee' => 'nullable|string|max:255',
'delivery_partner_count' => 'nullable|string|max:255',
'delivery_time_avg' => 'nullable|string|max:255',
'pickup_enabled' => 'nullable|string|max:255',
'opening_time' => 'nullable|string|max:255',
'closing_time' => 'nullable|string|max:255',
'auto_accept_orders' => 'nullable|string|max:255',
'pre_order_enabled' => 'nullable|string|max:255',
'max_order_capacity' => 'nullable|string|max:255',
'avg_rating' => 'nullable|string|max:255',
'review_count' => 'nullable|string|max:255',
'total_orders' => 'nullable|string|max:255',
'last_order_time' => 'nullable|string|max:255',
'last_active_time' => 'nullable|string|max:255',
'loyalty_points_enabled' => 'nullable|string|max:255',
'offers_enabled' => 'nullable|string|max:255',
'social_media_links' => 'nullable',
'settings' => 'nullable',
'uuid' => 'nullable',
];
}
public function message(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Feedback extends Model
{
use SoftDeletes;
protected $table = 'feedback';
public const TABLE_NAME = 'feedback';
protected $fillable = [
'restaurant_id',
'user_id',
'description',
'status',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id')->select('id', 'first_name', 'phone', 'email', 'avatar');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Gallery extends Model
{
use SoftDeletes;
protected $fillable = [
'restaurant_id',
'title',
'image',
'type',
];
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Language extends Model
{
use SoftDeletes;
protected $table = 'languages';
protected $fillable = [
'restaurant_id',
'name',
'code',
'file_path',
'flag',
'is_default',
'status',
];
protected $casts = [
'is_default' => 'boolean',
];
/**
* Accessor for full flag URL
*/
public function getFlagUrlAttribute()
{
return $this->flag ? asset("storage/{$this->flag}") : null;
}
/**
* Accessor for full JSON file URL
*/
public function getFileUrlAttribute()
{
return $this->file_path ? asset("storage/{$this->file_path}") : null;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Package extends Model
{
use SoftDeletes;
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Modules\Restaurant\Models\DeliveryCharge;
use Modules\Restaurant\Models\Order;
use Modules\Restaurant\Models\RestaurantSchedule;
use Modules\Restaurant\Models\Zone;
class Restaurant extends Model
{
use SoftDeletes;
public const TABLE_NAME = 'restaurants';
protected $fillable = [
'restaurant_id',
'owner_id',
'assigned_to',
'name',
'email',
'address',
'restaurant_type',
'phone',
'domain',
'platform',
'last_active_time',
'logo',
'status',
'theme_id',
'created_at',
'updated_at',
'deleted_at',
];
/**
* Attribute Casting
*/
protected $casts = [
'owner_id' => 'integer',
'assigned_to' => 'integer',
'last_active_time' => 'datetime',
'status' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* Get the owner of the restaurant.
*/
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
/**
* Get the user assigned to this restaurant.
*/
public function assignedUser(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_to');
}
public function subscription(): HasOne
{
return $this->hasOne(Subscription::class, 'restaurant_id', 'id')->with('package');
}
public function zone(): BelongsTo
{
return $this->belongsTo(Zone::class);
}
public function schedules(): HasMany
{
return $this->hasMany(RestaurantSchedule::class);
}
public function deliveryCharges(): HasMany
{
return $this->hasMany(DeliveryCharge::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
class RestaurantImageSAASSetting extends Model
{
protected $fillable = [
'restaurant_id',
'header_logo_light_theme',
'header_logo_dark_theme',
'footer_logo_light_theme',
'footer_logo_dark_theme',
'banner_image',
'status',
'created_at',
'updated_at',
'deleted_at',
];
public const TABLE_NAME = 'restaurant_image_s_a_a_s_settings';
protected $table = self::TABLE_NAME;
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
class RestaurantImageSetting extends Model
{
protected $fillable = [
'restaurant_id',
'header_logo_light_theme',
'header_logo_dark_theme',
'footer_logo_light_theme',
'footer_logo_dark_theme',
'banner_image',
'status',
'created_at',
'updated_at',
'deleted_at',
];
public const TABLE_NAME = 'restaurant_image_settings';
protected $table = self::TABLE_NAME;
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SAASFaq extends Model
{
use SoftDeletes;
protected $table = 's_a_a_s_faqs';
public const TABLE_NAME = 's_a_a_s_faqs';
protected $fillable = [
'restaurant_id',
'question',
'answer',
'status',
'created_by',
'created_at',
'updated_at',
];
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
class SAASSetting extends Model
{
protected $table = 's_a_a_s_settings';
protected $guarded = ['id'];
// cast value will be json decode
protected $casts = [
'payment_info' => 'array',
'sms_info' => 'array',
];
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SAASSubscription extends Model
{
use SoftDeletes;
protected $table = 's_a_a_s_subscriptions';
public const TABLE_NAME = 's_a_a_s_subscriptions';
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'restaurant_id',
'email',
'created_at',
'updated_at',
];
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = [
'restaurant_id',
'setting_type',
'option_key',
'option_value',
'deleted_at',
];
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Subscription extends Model
{
use SoftDeletes;
protected $guarded = ['id'];
protected $hidden = [
'updated_at',
'is_active',
'deleted_at',
];
protected function casts(): array
{
return [
'created_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function package(): BelongsTo
{
return $this->belongsTo(Package::class);
}
public function subscriptionItems(): HasMany
{
return $this->hasMany(SubscriptionItem::class);
}
public function restaurant(): BelongsTo
{
return $this->belongsTo(Restaurant::class, 'restaurant_id', 'id');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class SubscriptionItem extends Model
{
use SoftDeletes;
protected $guarded = ['id'];
protected $hidden = [
'updated_at',
'deleted_at',
];
protected $casts = [
'amount' => 'float',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function package(): HasOne
{
return $this->hasOne(Package::class, 'id', 'package_id');
}
public function restaurant(): BelongsTo
{
return $this->belongsTo(Restaurant::class, 'restaurant_id', 'id');
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SubscriptionUpgradeRequest extends Model
{
protected $fillable = [
'restaurant_id',
'package_id',
'extra_days',
'amount_paid',
'payment_method',
'notes',
'status',
'approved_by',
'approved_at',
];
public function restaurant(): BelongsTo
{
return $this->belongsTo(Restaurant::class)->select('id', 'name');
}
public function package(): BelongsTo
{
return $this->belongsTo(Package::class)->select('id', 'name', 'price', 'duration');
}
public function approver(): BelongsTo
{
return $this->belongsTo(User::class, 'approved_by');
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
use Modules\Authentication\Database\Factories\UserFactory;
use Modules\RestaurantDelivery\Models\Rider;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes;
public const TABLE_NAME = 'users';
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'restaurant_id',
'first_name',
'last_name',
'username',
'email',
'phone',
'password',
'otp_code',
'isVerified',
'email_verified_at',
'address',
'avatar',
'role_id',
'user_type',
'facebook',
'twitter',
'linkedin',
'google_plus',
'nid',
'platform',
'device_info',
'last_active_time',
'status',
'bio',
'fcm_token',
'qr_code',
'department_id',
'designation_id',
'salary_type_id',
'address',
'gender',
'date_of_birth',
'joining_date',
'basic_salary',
'created_by',
'deleted_at',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function role(): HasOne
{
return $this->hasOne(Role::class, 'id', 'role_id');
}
public function restaurant(): HasOne
{
return $this->hasOne(Restaurant::class, 'id', 'restaurant_id')->select('id', 'name', 'address', 'domain');
}
public function rider(): HasOne
{
return $this->hasOne(Rider::class, 'user_id', 'id');
}
public function customer(): HasOne
{
return $this->hasOne(User::class);
}
protected static function newFactory(): UserFactory
{
return UserFactory::new();
}
// Example: single FCM token
public function routeNotificationForFcm(): mixed
{
return $this->fcm_token;
}
// Or for multiple devices
public function getDeviceTokens(): mixed
{
return $this->deviceTokens()->pluck('token')->toArray();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Modules\Authentication\Models;
use App\Enum\ActionStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class UserLog extends Model
{
protected $fillable = [
'restaurant_id',
'user_id',
'ip_address',
'action',
'detail',
'previous_detail',
'model',
'model_id',
'url',
];
protected $casts = [
'action' => ActionStatus::class,
];
public function user(): HasOne
{
return $this->hasOne(User::class, 'id', 'user_id')->select('id', 'name');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\Authentication\Models;
use Illuminate\Database\Eloquent\Model;
class ZoomMeeting extends Model
{
protected $fillable = [
'restaurant_id',
'meeting_id',
'topic',
'agenda',
'start_time',
'duration',
'password',
'join_url',
'start_url',
'created_by',
'created_at',
'updated_at',
'deleted_at',
];
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Modules\Authentication\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Nwidart\Modules\Traits\PathNamespace;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class AuthenticationServiceProvider extends ServiceProvider
{
use PathNamespace;
protected string $name = 'Authentication';
protected string $nameLower = 'authentication';
/**
* 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
{
$relativeConfigPath = config('modules.paths.generator.config.path');
$configPath = module_path($this->name, $relativeConfigPath);
if (is_dir($configPath)) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$relativePath = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
$configKey = $this->nameLower.'.'.str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $relativePath);
$key = ($relativePath === 'config.php') ? $this->nameLower : $configKey;
$this->publishes([$file->getPathname() => config_path($relativePath)], 'config');
$this->mergeConfigFrom($file->getPathname(), $key);
}
}
}
}
/**
* 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);
$componentNamespace = $this->module_namespace($this->name, $this->app_path(config('modules.paths.generator.component-class.path')));
Blade::componentNamespace($componentNamespace, $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,30 @@
<?php
namespace Modules\Authentication\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,50 @@
<?php
namespace Modules\Authentication\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
protected string $name = 'Authentication';
/**
* 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();
$this->mapWebRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
protected function mapWebRoutes(): void
{
Route::middleware('web')->group(module_path($this->name, '/routes/web.php'));
}
/**
* 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'));
}
}

View File

@@ -0,0 +1,30 @@
{
"name": "nwidart/authentication",
"description": "",
"authors": [
{
"name": "Nicolas Widart",
"email": "n.widart@gmail.com"
}
],
"extra": {
"laravel": {
"providers": [],
"aliases": {
}
}
},
"autoload": {
"psr-4": {
"Modules\\Authentication\\": "app/",
"Modules\\Authentication\\Database\\Factories\\": "database/factories/",
"Modules\\Authentication\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Modules\\Authentication\\Tests\\": "tests/"
}
}
}

View File

@@ -0,0 +1,5 @@
<?php
return [
'name' => 'Authentication',
];

View File

@@ -0,0 +1,45 @@
<?php
namespace Modules\Authentication\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*/
protected $model = \Modules\Authentication\Models\User::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'first_name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'password' => Hash::make('12345678'), // default password for testing
'phone' => $this->faker->unique()->phoneNumber(),
'avatar' => asset('uploads/user/admin-avatar.png'),
'role_id' => 1, // default role
'email_verified_at' => now(),
'status' => 1, // default status
'qr_code' => (string) mt_rand(10000000, 99999999).mt_rand(10000000, 99999999),
'user_type' => 'Admin', // default user type
'facebook' => $this->faker->url(),
'twitter' => $this->faker->url(),
'linkedin' => $this->faker->url(),
'google_plus' => $this->faker->url(),
'remember_token' => Str::random(10),
'platform' => 'WEB',
'address' => 'Dhaka, Bangladesh',
'created_by' => 1,
'created_at' => now(),
'updated_at' => now(),
'deleted_at' => null, // no soft delete by default
];
}
}

View File

@@ -0,0 +1,98 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->foreignId('restaurant_id')->nullable();
$table->string('first_name');
$table->string('last_name')->nullable();
$table->string('username')->nullable();
$table->string('email')->nullable();
$table->string('phone', 50);
$table->string('password')->nullable();
$table->integer('otp_code')->nullable();
$table->integer('isVerified')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->tinyInteger('role_id')->default(1)->comment('1=super-admin, 2=admin, 3=owner, 4=waiter');
$table->string('user_type', 20)->default('waiter');
$table->mediumText('address')->nullable();
$table->string('avatar')->nullable();
$table->string('facebook', 192)->nullable();
$table->string('twitter', 192)->nullable();
$table->string('linkedin', 192)->nullable();
$table->string('google_plus', 192)->nullable();
$table->string('nid')->nullable();
$table->enum('platform', ['APP', 'WEB'])->default('APP');
$table->longText('device_info')->nullable();
$table->timestamp('last_active_time')->nullable();
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
// FOR Employee
$table->unsignedBigInteger('department_id')->nullable();
$table->unsignedBigInteger('designation_id')->nullable();
$table->unsignedBigInteger('salary_type_id')->nullable();
// 🏠 Personal Details
$table->enum('gender', ['male', 'female', 'other'])->nullable();
$table->date('date_of_birth')->nullable();
// 💼 Job Info
$table->date('joining_date')->nullable();
$table->decimal('basic_salary', 12, 2)->nullable()->default(0);
$table->string('bio')->nullable();
$table->string('fcm_token')->nullable();
$table->string('qr_code', 32)->nullable()->index();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->index('restaurant_id'); // Filter users by restaurant
$table->index('status'); // Filter active/inactive users
$table->index('user_type'); // Filter by user type
$table->index('phone'); // Search by phone
$table->index('first_name'); // Search by first name
// Composite indexes for frequent searches
$table->index(['restaurant_id', 'status']);
$table->index(['restaurant_id', 'user_type']);
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@@ -0,0 +1,25 @@
<?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('zones', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('restaurant_id')->nullable();
$table->string('name');
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('zones');
}
};

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('restaurants', function (Blueprint $table) {
$table->id();
$table->foreignId('owner_id')->nullable()->constrained('users')->cascadeOnDelete();
$table->foreignId('assigned_to')->nullable()->constrained('users')->cascadeOnDelete();
$table->string('name', 100);
$table->string('email')->nullable()->unique();
$table->string('address')->nullable();
$table->string('restaurant_type', 100)->nullable();
$table->string('phone', 15)->unique()->nullable();
$table->string('domain', 100)->unique()->nullable();
$table->string('logo')->nullable();
$table->foreignId('zone_id')->nullable();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->decimal('delivery_radius_km', 8, 2)->default(5.00);
$table->boolean('is_verified')->default(false);
$table->boolean('is_open')->default(true);
$table->enum('platform', ['WEB', 'APP'])->default('APP');
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->unsignedBigInteger('theme_id')->nullable();
$table->timestamp('last_active_time')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('restaurants');
}
};

View File

@@ -0,0 +1,40 @@
<?php
use App\Enum\ActionStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('restaurant_id')->constrained('restaurants')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->comment('Which User do this action');
$table->string('ip_address')->nullable();
$table->enum('action', ActionStatus::values())
->default(ActionStatus::Create->value);
$table->string('detail')->nullable();
$table->string('previous_detail')->nullable();
$table->string('model')->nullable();
$table->unsignedBigInteger('model_id')->nullable();
$table->string('url')->nullable();
$table->timestamps();
$table->index(['user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_logs');
}
};

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->foreignId('restaurant_id')->nullable();
$table->string('setting_type')->nullable()->default('Admin')->comment('Admin , Restaurant');
$table->string('type');
$table->string('option_key');
$table->mediumText('option_value')->nullable();
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
// UNIQUE: one option_key per restaurant
$table->unique(['restaurant_id', 'option_key'], 'restaurant_option_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('settings');
}
};

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('packages', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->decimal('price', 10, 2)->default(0.00);
$table->enum('package_type', ['recurring', 'one_time'])->default('recurring');
$table->enum('audience_type', ['custom', 'standard'])->default('custom');
$table->integer('duration')->unsigned()->default(0)->comment('Duration in days');
$table->boolean('auto_renew')->default(true);
$table->boolean('is_active')->default(true);
$table->smallInteger('status')->default(1)->comment('1=Active, 2=Inactive');
// Limits
$table->integer('max_food_items')->unsigned()->default(0);
$table->integer('max_categories')->unsigned()->default(0);
$table->integer('max_food_item_attributes')->unsigned()->default(0);
$table->integer('employee_limit')->unsigned()->default(0);
$table->integer('order_limit')->unsigned()->default(0);
$table->integer('storage_limit')->unsigned()->default(0);
// Features
$table->boolean('sms_management_enabled')->default(false);
$table->boolean('chat_enable')->default(false);
$table->boolean('landing_page_enable')->default(false);
$table->boolean('blog_enable')->default(false);
$table->integer('extra_buffer_time')->default(0);
// Flexible features for future upgrades
$table->json('extra_features')->nullable()->comment('Store new features in JSON');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('packages');
}
};

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('restaurant_id')->constrained('restaurants')->cascadeOnDelete();
$table->foreignId('package_id')->constrained('packages')->cascadeOnDelete();
$table->dateTime('start_date')->nullable();
$table->dateTime('end_date')->nullable();
$table->text('transactions_details')->nullable();
$table->integer('click_count')->default(0);
$table->integer('max_clicks')->default(100);
$table->integer('extra_buffer_time')->nullable()->comment('Extra time in days for subscription extension/grace period');
$table->smallInteger('status')->default(1)->comment('1=Active, 2=Inactive');
// Flexible for future expansion
$table->json('meta')->nullable()->comment('Additional data for future upgrades');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscriptions');
}
};

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscription_items', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('restaurant_id')->constrained('restaurants')->cascadeOnDelete();
$table->foreignId('subscription_id')->constrained('subscriptions')->cascadeOnDelete();
$table->foreignId('package_id')->nullable()->constrained('packages')->cascadeOnDelete();
$table->decimal('amount', 10, 2)->default(0.00);
$table->dateTime('start_date')->nullable();
$table->dateTime('end_date')->nullable();
$table->text('transactions_details')->nullable();
$table->smallInteger('status')->default(1)->comment('1=Active, 2=Inactive');
// Future flexibility
$table->json('meta')->nullable()->comment('Additional data for future upgrades');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_items');
}
};

View File

@@ -0,0 +1,148 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->foreignId('restaurant_id')->nullable();
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary(
[$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary'
);
} else {
$table->primary(
[$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary'
);
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary(
[$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary'
);
} else {
$table->primary(
[$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary'
);
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('feedback', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->text('description')->nullable();
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
$table->softDeletes();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('feedback');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('s_a_a_s_faqs', function (Blueprint $table) {
$table->id();
$table->string('question', 255);
$table->longText('answer')->nullable();
$table->smallInteger('status')->default(1)->comment('1=Active, 2=InActive');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('s_a_a_s_faqs');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscription_upgrade_requests', function (Blueprint $table) {
$table->id();
$table->foreignId('restaurant_id')->constrained()->onDelete('cascade');
$table->foreignId('package_id')->constrained()->onDelete('cascade');
$table->integer('extra_days')->default(0);
$table->decimal('amount_paid', 10, 2)->default(0);
$table->string('payment_method')->nullable();
$table->string('notes')->nullable();
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
$table->unsignedBigInteger('approved_by')->nullable();
$table->timestamp('approved_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_upgrade_requests');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('s_a_a_s_settings', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->longText('value')->nullable();
$table->longText('payment_info')->nullable();
$table->longText('sms_info')->nullable();
$table->string('type')->nullable()->default('general');
$table->string('mode')->nullable();
$table->string('status')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('s_a_a_s_settings');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('s_a_a_s_subscriptions', function (Blueprint $table) {
$table->id();
$table->string('email')->unique();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('s_a_a_s_subscriptions');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('galleries', function (Blueprint $table) {
$table->id();
$table->string('title')->nullable();
$table->string('image'); // Full public path
$table->string('type')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('galleries');
}
};

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('zoom_meetings', function (Blueprint $table) {
$table->id();
$table->foreignId('restaurant_id')->constrained('restaurants')->cascadeOnDelete();
$table->string('meeting_id');
$table->text('topic');
$table->text('agenda')->nullable();
$table->string('start_time');
$table->integer('duration')->nullable();
$table->string('password')->nullable();
$table->longText('join_url');
$table->longText('start_url');
$table->foreignId('created_by')->nullable()->constrained('users')->cascadeOnDelete();
$table->timestamps();
$table->softDeletes();
// Combine unique constraint
$table->unique(['restaurant_id', 'meeting_id'], 'unique_combination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('zoom_meetings');
}
};

View File

@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('oauth_auth_codes', function (Blueprint $table) {
$table->char('id', 80)->primary();
$table->foreignId('user_id')->index();
$table->foreignUuid('client_id');
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->dateTime('expires_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('oauth_auth_codes');
}
/**
* Get the migration connection name.
*/
public function getConnection(): ?string
{
return $this->connection ?? config('passport.connection');
}
};

Some files were not shown because too many files have changed in this diff Show More