notifs
This commit is contained in:
+2
-4
@@ -8,8 +8,7 @@ import { UploaderModule } from './modules/uploader/uploader.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { RestaurantsModule } from './modules/restaurants/restaurants.module';
|
||||
import { FoodModule } from './modules/foods/food.module';
|
||||
|
||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
@@ -33,8 +32,7 @@ import { CatalogueModule } from './modules/catalogue/catalogue.module';
|
||||
},
|
||||
]),
|
||||
ScheduleModule.forRoot(),
|
||||
RestaurantsModule,
|
||||
FoodModule,
|
||||
|
||||
NotificationsModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
BusinessModule,
|
||||
|
||||
@@ -7,18 +7,16 @@ import { AdminRepository } from './repositories/admin.repository';
|
||||
|
||||
import { AdminController } from './controllers/admin.controller';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { AdminRole } from './entities/adminRole.entity';
|
||||
import { AdminRoleRepository } from './repositories/admin-role.repository';
|
||||
|
||||
import { BusinessModule } from '../business/business.module';
|
||||
|
||||
@Module({
|
||||
providers: [AdminService, AdminRepository, AdminRoleRepository],
|
||||
providers: [AdminService, AdminRepository, ],
|
||||
controllers: [AdminController],
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Admin, AdminRole]),
|
||||
MikroOrmModule.forFeature([Admin]),
|
||||
JwtModule,
|
||||
UtilsModule,
|
||||
forwardRef(() => RestaurantsModule),
|
||||
forwardRef(() => BusinessModule),
|
||||
],
|
||||
exports: [AdminService, AdminRepository],
|
||||
})
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { Controller, Post, Body, HttpCode, HttpStatus, Get, UseGuards, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { AdminService } from '../providers/admin.service';
|
||||
import { CreateAdminDto } from '../dto/create-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('admin')
|
||||
@@ -32,16 +29,6 @@ export class AdminController {
|
||||
return this.adminService.finAdminById(adminId, restId);
|
||||
}
|
||||
|
||||
@Post('admin/admins')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new admin' })
|
||||
@ApiBody({ type: CreateAdminDto })
|
||||
async create(@Body() dto: CreateAdminDto, @RestId() restId: string) {
|
||||
const admin = await this.adminService.createAdminForMyRestaurant(restId, dto);
|
||||
return admin;
|
||||
}
|
||||
|
||||
@Patch('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@@ -60,45 +47,25 @@ export class AdminController {
|
||||
return this.adminService.finAdminById(adminId, restId);
|
||||
}
|
||||
|
||||
@Delete('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'Delete an admin by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Admin ID' })
|
||||
deleteById(@Param('adminId') adminId: string, @RestId() restId: string) {
|
||||
return this.adminService.remove(adminId, restId);
|
||||
}
|
||||
// @Delete('admin/admins/:adminId')
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Permissions(Permission.MANAGE_ADMINS)
|
||||
// @ApiOperation({ summary: 'Delete an admin by ID' })
|
||||
// @ApiParam({ name: 'id', description: 'Admin ID' })
|
||||
// deleteById(@Param('adminId') adminId: string, @RestId() restId: string) {
|
||||
// return this.adminService.remove(adminId, restId);
|
||||
// }
|
||||
|
||||
/** Super Admin Endpoints */
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Get('super-admin/restaurants/:restaurantId/admins')
|
||||
@ApiOperation({ summary: 'Get admins for a specific restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
getAdminForRestaurant(@Param('restaurantId') restaurantId: string) {
|
||||
return this.adminService.getAdminsForRestaurantBySuperAdmin(restaurantId);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Post('super-admin/restaurants/:restaurantId/admins')
|
||||
@ApiOperation({ summary: 'Create admin for a specific restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
@ApiBody({ type: CreateMyRestaurantAdminDto })
|
||||
createAdminForRestaurant(
|
||||
@Param('restaurantId') restaurantId: string,
|
||||
@Body() dto: CreateMyRestaurantAdminDto,
|
||||
): Promise<Admin> {
|
||||
return this.adminService.createAdminForRestaurantBySuperAdmin(restaurantId, dto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Delete('super-admin/restaurants/:restaurantId/admins/:adminId')
|
||||
@ApiOperation({ summary: 'Revoke admin role from a restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
@ApiParam({ name: 'adminId', description: 'Admin ID' })
|
||||
revokeAdminFromRestaurant(
|
||||
@Param('restaurantId') restaurantId: string,
|
||||
@Param('adminId') adminId: string,
|
||||
) {
|
||||
return this.adminService.remove(adminId, restaurantId);
|
||||
}
|
||||
// @UseGuards(SuperAdminAuthGuard)
|
||||
// @Delete('super-admin/restaurants/:restaurantId/admins/:adminId')
|
||||
// @ApiOperation({ summary: 'Revoke admin role from a restaurant (Super Admin only)' })
|
||||
// @ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
// @ApiParam({ name: 'adminId', description: 'Admin ID' })
|
||||
// revokeAdminFromRestaurant(
|
||||
// @Param('restaurantId') restaurantId: string,
|
||||
// @Param('adminId') adminId: string,
|
||||
// ) {
|
||||
// return this.adminService.remove(adminId, restaurantId);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateMyRestaurantAdminDto {
|
||||
@ApiProperty({ description: 'Mobile phone number' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ description: 'First name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Last name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Role id for the admin' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
roleId!: string;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Cascade, Collection, Entity, OneToMany, Property } from '@mikro-orm/core';
|
||||
import { Entity, OneToOne, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { AdminRole } from './adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
import { Business } from 'src/modules/business/entities/business.entity';
|
||||
|
||||
@Entity({ tableName: 'admins' })
|
||||
export class Admin extends BaseEntity {
|
||||
@@ -11,21 +10,10 @@ export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
@Property()
|
||||
phone: string
|
||||
|
||||
@Property({ unique: true })
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
|
||||
@OneToMany(() => AdminRole, adminRole => adminRole.admin, {
|
||||
cascade: [Cascade.ALL],
|
||||
orphanRemoval: true,
|
||||
})
|
||||
roles = new Collection<AdminRole>(this);
|
||||
@OneToOne(() => Business, (business) => business.admin, { owner: true })
|
||||
business: Business
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Entity, Index, ManyToOne, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { Admin } from './admin.entity';
|
||||
|
||||
@Entity({ tableName: 'admin_roles' })
|
||||
@Unique({ properties: ['admin', 'restaurant'] })
|
||||
@Index({ properties: ['admin', 'restaurant'] })
|
||||
@Index({ properties: ['admin'] })
|
||||
@Index({ properties: ['restaurant'] })
|
||||
export class AdminRole extends BaseEntity {
|
||||
@ManyToOne(() => Admin)
|
||||
admin!: Admin;
|
||||
|
||||
|
||||
@ManyToOne(() => Restaurant, { nullable: true })
|
||||
restaurant?: Restaurant;
|
||||
}
|
||||
@@ -1,26 +1,18 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { wrap } from '@mikro-orm/core';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
import { AdminRepository } from '../repositories/admin.repository';
|
||||
import { AdminRoleRepository } from '../repositories/admin-role.repository';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly adminRoleRepository: AdminRoleRepository,
|
||||
private readonly em: EntityManager,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async findByPhone(phone: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
return this.adminRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async finAdminById(
|
||||
@@ -28,106 +20,27 @@ export class AdminService {
|
||||
restId: string,
|
||||
) {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: ['roles'], exclude: ['roles'] },
|
||||
{ id: adminId },
|
||||
{},
|
||||
);
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin not found');
|
||||
}
|
||||
const adminPlain = wrap(admin).toObject();
|
||||
const adminRole = await this.adminRoleRepository.findOne({ admin: admin, restaurant: { id: restId } });
|
||||
|
||||
|
||||
return { ...adminPlain };
|
||||
}
|
||||
|
||||
async findAllByRestaurantId(restId: string) {
|
||||
return this.adminRepository.find({ roles: { restaurant: { id: restId } } }, { populate: [] });
|
||||
return this.adminRepository.find({}, { populate: [] });
|
||||
}
|
||||
|
||||
async createAdminForMyRestaurant(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.em.persistAndFlush(admin);
|
||||
}
|
||||
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
let adminRole = await this.adminRoleRepository.findOne({
|
||||
admin: admin,
|
||||
restaurant: restaurant,
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = this.adminRoleRepository.create({
|
||||
admin: admin,
|
||||
restaurant,
|
||||
});
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(adminRole);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async createAdminForRestaurantBySuperAdmin(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.em.persistAndFlush(admin);
|
||||
}
|
||||
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
let adminRole = await this.adminRoleRepository.findOne({
|
||||
admin: admin,
|
||||
restaurant: restaurant,
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = this.adminRoleRepository.create({
|
||||
admin: admin,
|
||||
restaurant,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(adminRole);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async getAdminsForRestaurantBySuperAdmin(restId: string) {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
return this.adminRepository.find({ roles: { restaurant: restaurant } }, { populate: [ ] });
|
||||
}
|
||||
|
||||
async update(adminId: string, restId: string, dto: UpdateAdminDto) {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: [ ] },
|
||||
{ id: adminId },
|
||||
{ populate: [] },
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
@@ -144,26 +57,19 @@ export class AdminService {
|
||||
admin.lastName = rest.lastName;
|
||||
}
|
||||
if (rest.phone !== undefined && rest.phone !== admin.phone) {
|
||||
const normalizedPhone = normalizePhone(rest.phone);
|
||||
const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
if (exists) {
|
||||
throw new ConflictException('This Phone Number is already taken');
|
||||
}
|
||||
admin.phone = normalizedPhone;
|
||||
// const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
// if (exists) {
|
||||
// throw new ConflictException('This Phone Number is already taken');
|
||||
// }
|
||||
// admin.phone = normalizedPhone;
|
||||
}
|
||||
|
||||
// Update role if roleId is provided
|
||||
|
||||
|
||||
|
||||
await this.em.persistAndFlush(admin);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async remove(adminId: string, restId: string): Promise<void> {
|
||||
const adminRole = await this.adminRoleRepository.findOne({ admin: { id: adminId }, restaurant: { id: restId } });
|
||||
if (!adminRole) {
|
||||
throw new NotFoundException('Admin role not found');
|
||||
}
|
||||
return this.em.removeAndFlush(adminRole);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRoleRepository extends EntityRepository<AdminRole> {
|
||||
constructor(
|
||||
readonly em: EntityManager,
|
||||
) {
|
||||
super(em, AdminRole);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +1,13 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { RestRepository } from '../../restaurants/repositories/rest.repository';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRepository extends EntityRepository<Admin> {
|
||||
constructor(
|
||||
readonly em: EntityManager,
|
||||
private readonly restRepository: RestRepository,
|
||||
) {
|
||||
) {
|
||||
super(em, Admin);
|
||||
}
|
||||
|
||||
async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
console.log('phone', phone);
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
// First, find the restaurant by slug using the same repository as auth service
|
||||
const restaurant = await this.restRepository.findOne({ slug });
|
||||
if (!restaurant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find AdminRole that matches the admin phone and restaurant
|
||||
const adminRole = await this.em.findOne(
|
||||
AdminRole,
|
||||
{
|
||||
admin: { phone: normalizedPhone },
|
||||
restaurant: { id: restaurant.id },
|
||||
},
|
||||
{ populate: ['admin', 'restaurant'] },
|
||||
);
|
||||
|
||||
if (!adminRole || !adminRole.admin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure all roles are populated
|
||||
|
||||
|
||||
return adminRole.admin;
|
||||
}
|
||||
async findAdminsWithPermission(restaurantId: string, permission: string): Promise<Admin[]> {
|
||||
const admins = await this.em.find(
|
||||
Admin,
|
||||
{ roles: { restaurant: { id: restaurantId }, } },
|
||||
{ populate: [] },
|
||||
);
|
||||
return admins;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { UtilsModule } from '../utils/utils.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { TokensService } from './services/tokens.service';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { BusinessModule } from '../business/business.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -30,7 +30,7 @@ import { NotificationsModule } from '../notifications/notifications.module';
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
forwardRef(() => AdminModule),
|
||||
forwardRef(() => RestaurantsModule),
|
||||
forwardRef(() => BusinessModule),
|
||||
MikroOrmModule.forFeature([RefreshToken]),
|
||||
forwardRef(() => NotificationsModule),
|
||||
|
||||
|
||||
@@ -14,52 +14,52 @@ import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard';
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
@Post('public/auth/otp/request')
|
||||
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
otpRequest(@Body() dto: RequestOtpDto) {
|
||||
return this.authService.requestOtp(dto, false);
|
||||
}
|
||||
// @Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
// @Post('public/auth/otp/request')
|
||||
// @ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
// @ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
// otpRequest(@Body() dto: RequestOtpDto) {
|
||||
// return this.authService.requestOtp(dto, false);
|
||||
// }
|
||||
|
||||
|
||||
@RefreshTokenRateLimit()
|
||||
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
||||
@Post('public/auth/refresh')
|
||||
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
// @RefreshTokenRateLimit()
|
||||
// @ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
||||
// @Post('public/auth/refresh')
|
||||
// refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
// return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
// }
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
@Post('admin/auth/otp/request')
|
||||
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
otpRequestAdmin(@Body() dto: RequestOtpDto) {
|
||||
return this.authService.requestOtp(dto, true);
|
||||
}
|
||||
// @Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
// @Post('admin/auth/otp/request')
|
||||
// @ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
// @ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
// otpRequestAdmin(@Body() dto: RequestOtpDto) {
|
||||
// return this.authService.requestOtp(dto, true);
|
||||
// }
|
||||
|
||||
@Post('admin/auth/otp/verify')
|
||||
@ApiOperation({ summary: 'Verify OTP code' })
|
||||
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||
otpVerifyAdmin(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp);
|
||||
}
|
||||
// @Post('admin/auth/otp/verify')
|
||||
// @ApiOperation({ summary: 'Verify OTP code' })
|
||||
// @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||
// otpVerifyAdmin(@Body() dto: VerifyOtpDto) {
|
||||
// return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp);
|
||||
// }
|
||||
|
||||
@RefreshTokenRateLimit()
|
||||
@ApiOperation({ summary: 'refresh the admin access token / refresh token' })
|
||||
@Post('admin/auth/refresh')
|
||||
refreshTokenAdmin(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
// @RefreshTokenRateLimit()
|
||||
// @ApiOperation({ summary: 'refresh the admin access token / refresh token' })
|
||||
// @Post('admin/auth/refresh')
|
||||
// refreshTokenAdmin(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
// return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
// }
|
||||
|
||||
//super admin routes
|
||||
// //super admin routes
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Post('super-admin/auth/direct-login')
|
||||
@ApiOperation({ summary: 'Direct login for DSC - returns login credentials' })
|
||||
@ApiBody({ type: DirectLoginDto, description: 'Phone number and restaurant slug for direct login' })
|
||||
directloginForDsc(@Body() dto: DirectLoginDto) {
|
||||
return this.authService.loginAdminForDsc(dto.phone, dto.slug);
|
||||
}
|
||||
// @UseGuards(SuperAdminAuthGuard)
|
||||
// @Post('super-admin/auth/direct-login')
|
||||
// @ApiOperation({ summary: 'Direct login for DSC - returns login credentials' })
|
||||
// @ApiBody({ type: DirectLoginDto, description: 'Phone number and restaurant slug for direct login' })
|
||||
// directloginForDsc(@Body() dto: DirectLoginDto) {
|
||||
// return this.authService.loginAdminForDsc(dto.phone, dto.slug);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { RequestOtpDto } from '../dto/request-otp.dto';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { SmsService } from '../../notifications/services/sms.service';
|
||||
import { randomInt } from 'crypto';
|
||||
import { randomInt } from 'crypto';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -17,9 +16,8 @@ export class AuthService {
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly tokensService: TokensService,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly tokensService: TokensService,
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.OTP_EXPIRATION_TIME = this.configService.get<number>('OTP_EXPIRATION_TIME') ?? 240;
|
||||
@@ -33,85 +31,85 @@ export class AuthService {
|
||||
return `otp-admin:${restaurantSlug}:${phone}`;
|
||||
}
|
||||
|
||||
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||
const { phone, slug } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
if (isAdmin) {
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
const code = this.generateOtpCode();
|
||||
const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone);
|
||||
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
|
||||
// async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||
// const { phone, slug } = dto;
|
||||
// const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
await this.smsService.sendotp(normalizedPhone, code);
|
||||
// if (isAdmin) {
|
||||
// const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
// if (!admin) {
|
||||
// throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
// }
|
||||
// }
|
||||
// const code = this.generateOtpCode();
|
||||
// const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone);
|
||||
// await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
|
||||
|
||||
return { code: null };
|
||||
}
|
||||
// await this.smsService.sendotp(normalizedPhone, code);
|
||||
|
||||
|
||||
// return { code: null };
|
||||
// }
|
||||
|
||||
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.adminOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
await this.cacheService.del(key);
|
||||
// async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||
// const normalizedPhone = normalizePhone(phone);
|
||||
// const key = this.adminOtpKey(slug, normalizedPhone);
|
||||
// const cachedCode = await this.cacheService.get(key);
|
||||
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
// if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
// if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
// await this.cacheService.del(key);
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
// // const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
// // if (!admin) {
|
||||
// // throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
// // }
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
// const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
|
||||
// if (!rest) {
|
||||
// throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
// }
|
||||
|
||||
return { tokens, admin };
|
||||
}
|
||||
// const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
|
||||
|
||||
|
||||
// return { tokens, admin };
|
||||
// }
|
||||
/**
|
||||
*
|
||||
to use for login directly from DSC
|
||||
*/
|
||||
async loginAdminForDsc(phone: string, slug: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
// async loginAdminForDsc(phone: string, slug: string) {
|
||||
// const normalizedPhone = normalizePhone(phone);
|
||||
// const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
// if (!admin) {
|
||||
// throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
// }
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
// const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
// if (!rest) {
|
||||
// throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
// }
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
// const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
|
||||
|
||||
return { tokens, admin };
|
||||
}
|
||||
|
||||
private generateOtpCode(): string {
|
||||
const code = randomInt(10000, 100000);
|
||||
return code.toString();
|
||||
}
|
||||
// return { tokens, admin };
|
||||
// }
|
||||
|
||||
refreshToken(oldRefreshToken: string) {
|
||||
return this.tokensService.refreshToken(oldRefreshToken);
|
||||
}
|
||||
// private generateOtpCode(): string {
|
||||
// const code = randomInt(10000, 100000);
|
||||
// return code.toString();
|
||||
// }
|
||||
|
||||
// refreshToken(oldRefreshToken: string) {
|
||||
// return this.tokensService.refreshToken(oldRefreshToken);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import dayjs from 'dayjs';
|
||||
import { AuthMessage } from '../../../common/enums/message.enum';
|
||||
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity';
|
||||
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class TokensService {
|
||||
private readonly logger = new Logger(TokensService.name);
|
||||
@@ -110,15 +109,15 @@ export class TokensService {
|
||||
const isAdmin = token.type === RefreshTokenType.ADMIN;
|
||||
|
||||
// Verify restaurant still exists
|
||||
const restaurant = await em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
}
|
||||
// const restaurant = await em.findOne(Restaurant, { id: restId });
|
||||
// if (!restaurant) {
|
||||
// throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
// }
|
||||
|
||||
try {
|
||||
// Generate new tokens first (before removing old token)
|
||||
// Pass the transaction EntityManager to ensure operations are within the transaction
|
||||
const tokens = await this.generateAccessAndRefreshToken(ownerId, restaurant.id, isAdmin, restaurant.slug, em);
|
||||
const tokens = await this.generateAccessAndRefreshToken(ownerId, "restaurant.id", isAdmin, "restaurant.slug", em);
|
||||
|
||||
// Remove old token only after new token is created
|
||||
em.remove(token);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { CreateBusinessDto } from './dto/create-business.dto';
|
||||
import { UpdateBusinessDto } from './dto/update-business.dto';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Business } from './entities/business.entity';
|
||||
import { Admin } from '../admin/entities/admin.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessService {
|
||||
@@ -10,8 +11,12 @@ export class BusinessService {
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
create(dto: CreateBusinessDto) {
|
||||
const business = this.em.create(Business, dto)
|
||||
async create(dto: CreateBusinessDto) {
|
||||
const admin = await this.em.findOne(Admin, {})
|
||||
if(!admin){
|
||||
throw new BadRequestException()
|
||||
}
|
||||
const business = this.em.create(Business, { ...dto, admin })
|
||||
return this.em.persistAndFlush(business)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, Opt, Property, Unique } from "@mikro-orm/core";
|
||||
import { Cascade, Collection, Entity, EntityRepositoryType,
|
||||
OneToMany, OneToOne, Opt, Property, Unique } from "@mikro-orm/core";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Catalogue } from "src/modules/catalogue/entities/catalogue.entity";
|
||||
import { BusinessRepository } from "../repositories/business.repository";
|
||||
import { Admin } from "src/modules/admin/entities/admin.entity";
|
||||
|
||||
@Entity({ repository: () => BusinessRepository })
|
||||
export class Business extends BaseEntity {
|
||||
@@ -35,22 +37,11 @@ export class Business extends BaseEntity {
|
||||
//=========================
|
||||
|
||||
@OneToMany(() => Catalogue, (catalogue) => catalogue.business, { cascade: [Cascade.ALL] })
|
||||
companies = new Collection<Catalogue>(this);
|
||||
catalogues = new Collection<Catalogue>(this);
|
||||
|
||||
@OneToOne(() => Admin, (admin) => admin.business, { deleteRule: 'restrict' })
|
||||
admin: Admin
|
||||
|
||||
|
||||
// @OneToMany(() => Ticket, (ticket) => ticket.business)
|
||||
// tickets = new Collection<Ticket>(this);
|
||||
|
||||
// @OneToMany(() => Announcement, (announcement) => announcement.business)
|
||||
// announcements = new Collection<Announcement>(this);
|
||||
|
||||
// @OneToMany(() => Criticism, (criticism) => criticism.business)
|
||||
// criticisms = new Collection<Criticism>(this);
|
||||
|
||||
|
||||
// @OneToMany(() => User, (user) => user.business)
|
||||
// users = new Collection<User>(this);
|
||||
|
||||
[EntityRepositoryType]?: BusinessRepository;
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { CategoryService } from '../providers/category.service';
|
||||
import { CreateCategoryDto } from '../dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from '../dto/update-category.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
ApiBearerAuth,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
|
||||
@ApiTags('category')
|
||||
@Controller()
|
||||
export class CategoryController {
|
||||
constructor(private readonly categoryService: CategoryService) { }
|
||||
|
||||
@Get('public/categories/restaurant/:slug')
|
||||
@ApiOperation({ summary: 'Get all categories by restaurant slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
|
||||
@ApiOkResponse({ description: 'List of all categories for the restaurant' })
|
||||
findAllByRestaurant(@Param('slug') slug: string) {
|
||||
return this.categoryService.findAllByRestaurant(slug);
|
||||
}
|
||||
|
||||
/*** Admin ***/
|
||||
|
||||
@ApiOperation({ summary: 'Create category' })
|
||||
@ApiBody({ type: CreateCategoryDto })
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Post('admin/categories')
|
||||
create(@Body() dto: CreateCategoryDto, @RestId() restId: string) {
|
||||
return this.categoryService.create(restId, dto);
|
||||
}
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@ApiOperation({ summary: 'my restaurant categories' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/categories')
|
||||
findAll(@RestId() restId: string) {
|
||||
return this.categoryService.findAllByRestaurantId(restId);
|
||||
}
|
||||
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/categories/:id')
|
||||
@ApiOperation({ summary: 'Get category' })
|
||||
@ApiParam({ name: 'id', required: true, type: String })
|
||||
findOne(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.categoryService.findOne(restId, id);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@Patch('admin/categories/:id')
|
||||
@ApiOperation({ summary: 'Update category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiBody({ type: UpdateCategoryDto })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateCategoryDto, @RestId() restId: string) {
|
||||
return this.categoryService.update(restId, id, dto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@Delete('admin/categories/:id')
|
||||
@ApiOperation({ summary: 'Delete category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiOkResponse({ description: 'Deleted' })
|
||||
remove(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.categoryService.remove(restId, id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common';
|
||||
import { FoodService } from '../providers/food.service';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { UpdateFoodDto } from '../dto/update-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiQuery,
|
||||
ApiBody,
|
||||
ApiParam,
|
||||
ApiBearerAuth,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { RestId, UserId } from 'src/common/decorators';
|
||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('foods')
|
||||
@Controller()
|
||||
export class FoodController {
|
||||
constructor(private readonly foodsService: FoodService) { }
|
||||
|
||||
@Get('public/foods/restaurant/:slug')
|
||||
@ApiOperation({ summary: 'Get all foods by restaurant slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
|
||||
findAllByRestaurant(@Param('slug') slug: string) {
|
||||
return this.foodsService.findByWeekDateAndMealType(slug);
|
||||
}
|
||||
|
||||
@Get('public/foods/:foodId')
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get a food by id' })
|
||||
@ApiParam({ name: 'foodId', required: true })
|
||||
findPublicFoodById(@Param('foodId') foodId: string, @UserId() userId?: string): Promise<any> {
|
||||
const a = this.foodsService.findPublicById(foodId, userId);
|
||||
return a;
|
||||
}
|
||||
|
||||
@Post('public/foods/favorite/:foodId')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'toggle a food as favorite' })
|
||||
@ApiParam({ name: 'foodId', required: true })
|
||||
setAsFavorute(@Param('foodId') foodId: string, @UserId() userId: string) {
|
||||
return this.foodsService.toggleFavorite(userId, foodId);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------- Admin ---------------------------------- */
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Post('admin/foods')
|
||||
@ApiOperation({ summary: 'Create a new food' })
|
||||
@ApiBody({ type: CreateFoodDto })
|
||||
create(@Body() createFoodDto: CreateFoodDto, @RestId() restId: string) {
|
||||
return this.foodsService.create(restId, createFoodDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Get('admin/foods')
|
||||
@ApiOperation({ summary: 'Get a paginated list of foods' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
@ApiQuery({ name: 'categoryId', required: false, type: String })
|
||||
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
|
||||
async findAll(@Query() dto: FindFoodsDto, @RestId() restId: string) {
|
||||
const result = await this.foodsService.findAll(restId, dto);
|
||||
return result;
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Get('admin/foods/:id')
|
||||
@ApiOperation({ summary: 'Get a food by id' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
findById(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.foodsService.findAdminById(restId, id);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Patch('admin/foods/:id')
|
||||
@ApiOperation({ summary: 'Update a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiBody({ type: UpdateFoodDto })
|
||||
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto, @RestId() restId: string) {
|
||||
return this.foodsService.update(restId, id, updateFoodDto);
|
||||
}
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Delete('admin/foods/:id')
|
||||
@ApiOperation({ summary: 'Delete (soft) a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
remove(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.foodsService.remove(restId, id);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
|
||||
@Injectable()
|
||||
export class FoodStockCrone {
|
||||
private readonly logger = new Logger(FoodStockCrone.name);
|
||||
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
|
||||
// run every day at 00:03
|
||||
@Cron('3 0 * * *', {
|
||||
name: 'resetAvailableStock',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
async handleCron() {
|
||||
try {
|
||||
this.logger.debug('Starting daily inventory reset (availableStock = totalStock)');
|
||||
|
||||
|
||||
|
||||
await this.em.transactional(async em => {
|
||||
|
||||
await em.flush();
|
||||
});
|
||||
|
||||
this.logger.log('Daily inventory reset completed');
|
||||
} catch (err) {
|
||||
this.logger.error(`FoodStockCrone failed: ${err?.message}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsInt, Min } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateCategoryDto {
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'ایرانی' })
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiPropertyOptional({ example: true })
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.png' })
|
||||
avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
order?: number;
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { MealType } from '../interface/food.interface';
|
||||
|
||||
export class CreateFoodDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
categoryId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'قرمه سبزی' })
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'توضیحات غذا' })
|
||||
desc?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
content?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsInt({ each: true })
|
||||
@Min(0, { each: true })
|
||||
@Max(6, { each: true })
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ type: [Number], example: [0, 1, 2, 3, 4, 5, 6] })
|
||||
weekDays?: number[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsEnum(MealType, { each: true })
|
||||
@ApiPropertyOptional({ enum: MealType, isArray: true })
|
||||
mealTypes?: MealType[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 120000 })
|
||||
price?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 15 })
|
||||
prepareTime?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
images?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
inPlaceServe?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
pickupServe?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
discount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ example: 50 })
|
||||
dailyStock: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
isSpecialOffer?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
order?: number;
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FindFoodsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10 })
|
||||
limit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'createdAt' })
|
||||
orderBy?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['asc', 'desc'])
|
||||
@ApiPropertyOptional({ example: 'desc' })
|
||||
order?: 'asc' | 'desc';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiPropertyOptional({ example: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCategoryDto } from './create-category.dto';
|
||||
|
||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateFoodDto } from './create-food.dto';
|
||||
|
||||
export class UpdateFoodDto extends PartialType(CreateFoodDto) {}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Entity, Index, Property, Collection, OneToMany, ManyToOne } from '@mikro-orm/core';
|
||||
import { Food } from './food.entity';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Entity({ tableName: 'categories' })
|
||||
@Index({ properties: ['restaurant', 'isActive'] })
|
||||
@Index({ properties: ['isActive'] })
|
||||
export class Category extends BaseEntity {
|
||||
@Property()
|
||||
title!: string;
|
||||
|
||||
@OneToMany(() => Food, food => food.category)
|
||||
foods = new Collection<Food>(this);
|
||||
|
||||
@Property({ default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
order?: number;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Entity, ManyToOne, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
|
||||
@Entity({ tableName: 'favorites' })
|
||||
export class Favorite extends BaseEntity {
|
||||
|
||||
|
||||
@ManyToOne(() => Food)
|
||||
food: Food;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Cascade, Collection, Entity, Index, ManyToOne, OneToMany, Property, OneToOne } from '@mikro-orm/core';
|
||||
import { Category } from './category.entity';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
import { MealType } from '../interface/food.interface';
|
||||
import { Favorite } from './favorite.entity';
|
||||
|
||||
@Entity({ tableName: 'foods' })
|
||||
@Index({ properties: ['restaurant', 'isActive'] })
|
||||
@Index({ properties: ['category', 'isActive'] })
|
||||
@Index({ properties: ['isActive'] })
|
||||
export class Food extends BaseEntity {
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant: Restaurant;
|
||||
|
||||
@ManyToOne(() => Category)
|
||||
category: Category;
|
||||
|
||||
@OneToMany(() => Favorite, favorite => favorite.food)
|
||||
favorites = new Collection<Favorite>(this);
|
||||
|
||||
@Property({ nullable: true })
|
||||
title?: string;
|
||||
|
||||
@Property({ type: 'text', nullable: true })
|
||||
desc?: string;
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
content?: string[];
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
price?: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
order?: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
prepareTime?: number; // in minutes
|
||||
|
||||
@Property({ type: 'jsonb', default: [] })
|
||||
weekDays: number[] = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
@Property({ type: 'jsonb', default: [] })
|
||||
mealTypes: MealType[] = [];
|
||||
|
||||
@Property({ type: 'boolean', default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
images?: string[];
|
||||
|
||||
@Property({ type: 'boolean', default: false })
|
||||
inPlaceServe: boolean = false;
|
||||
|
||||
@Property({ type: 'boolean', default: false })
|
||||
pickupServe: boolean = false;
|
||||
|
||||
@Property({ type: 'float', default: null })
|
||||
score?: number | null = null;
|
||||
|
||||
@Property({ type: 'float', default: 0 })
|
||||
discount: number = 0;
|
||||
|
||||
@Property({ type: 'boolean', default: false })
|
||||
isSpecialOffer: boolean = false;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FoodService } from './providers/food.service';
|
||||
import { FoodStockCrone } from './crone/food.crone';
|
||||
import { FoodController } from './controllers/food.controller';
|
||||
import { CategoryController } from './controllers/category.controller';
|
||||
import { CategoryService } from './providers/category.service';
|
||||
import { FoodRepository } from './repositories/food.repository';
|
||||
import { CategoryRepository } from './repositories/category.repository';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Category } from './entities/category.entity';
|
||||
import { Food } from './entities/food.entity';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { Favorite } from './entities/favorite.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Food, Category, Favorite]),
|
||||
RestaurantsModule,
|
||||
AuthModule,
|
||||
JwtModule,
|
||||
UtilsModule,
|
||||
],
|
||||
controllers: [FoodController, CategoryController],
|
||||
providers: [FoodService, CategoryService, FoodRepository, CategoryRepository, FoodStockCrone],
|
||||
exports: [FoodRepository, CategoryRepository],
|
||||
})
|
||||
export class FoodModule {}
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum MealType {
|
||||
BREAKFAST = 'breakfast',
|
||||
LUNCH = 'lunch',
|
||||
DINNER = 'dinner',
|
||||
SNACK = 'snack',
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateCategoryDto } from '../dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from '../dto/update-category.dto';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { Category } from '../entities/category.entity';
|
||||
import { CategoryMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
constructor(
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async create(restId: string, dto: CreateCategoryDto): Promise<Category> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const data: RequiredEntityData<Category> = {
|
||||
title: dto.title,
|
||||
isActive: dto.isActive ?? true,
|
||||
restaurant: restaurant,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
};
|
||||
|
||||
const category = this.categoryRepository.create(data);
|
||||
await this.em.persistAndFlush(category);
|
||||
return category;
|
||||
}
|
||||
|
||||
async findAllByRestaurant(slug: string): Promise<Category[]> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { slug });
|
||||
if (!restaurant || !restaurant.id) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
return this.categoryRepository.find(
|
||||
{ restaurant: restaurant, isActive: true }, { orderBy: { order: 'ASC' } });
|
||||
}
|
||||
|
||||
async findAllByRestaurantId(restId: string): Promise<Category[]> {
|
||||
return this.categoryRepository.find(
|
||||
{ restaurant: { id: restId } },
|
||||
{ orderBy: { order: 'asc' } });
|
||||
}
|
||||
|
||||
async findOne(restId: string, id: string): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['foods'] });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
|
||||
return {
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
isActive: cat.isActive,
|
||||
restaurant: cat.restaurant,
|
||||
avatarUrl: cat.avatarUrl,
|
||||
createdAt: cat.createdAt,
|
||||
updatedAt: cat.updatedAt,
|
||||
foods: cat.foods.getItems().map(f => ({ id: f.id, title: f.title })),
|
||||
} as unknown as Category;
|
||||
}
|
||||
|
||||
|
||||
async update(restId: string, id: string, dto: UpdateCategoryDto): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ id, restaurant: { id: restId } });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
this.em.assign(cat, dto);
|
||||
await this.em.persistAndFlush(cat);
|
||||
return cat;
|
||||
}
|
||||
|
||||
async remove(restId: string, id: string): Promise<void> {
|
||||
const cat = await this.categoryRepository.findOne({ id, restaurant: { id: restId } });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
cat.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(cat);
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import { FoodRepository } from '../repositories/food.repository';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
|
||||
import { Food } from '../entities/food.entity';
|
||||
import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { Favorite } from '../entities/favorite.entity';
|
||||
import { MealType } from '../interface/food.interface';
|
||||
|
||||
@Injectable()
|
||||
export class FoodService {
|
||||
constructor(
|
||||
private readonly foodRepository: FoodRepository,
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async create(restId: string, createFoodDto: CreateFoodDto) {
|
||||
const { categoryId, dailyStock = 0, ...rest } = createFoodDto;
|
||||
const restaurant = await this.restRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const category = await this.categoryRepository.findOne({ id: categoryId, restaurant: { id: restId } });
|
||||
if (!category) {
|
||||
throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { food } = await this.em.transactional(async em => {
|
||||
// prepare data with defaults for required fields so repository.create typing is satisfied
|
||||
const data: RequiredEntityData<Food> = {
|
||||
desc: rest.desc,
|
||||
isActive: rest.isActive ?? true,
|
||||
inPlaceServe: rest.inPlaceServe ?? false,
|
||||
pickupServe: rest.pickupServe ?? false,
|
||||
discount: rest.discount ?? 0,
|
||||
isSpecialOffer: rest.isSpecialOffer ?? false,
|
||||
order: rest.order ?? null,
|
||||
// map single-title/content DTO to localized fields
|
||||
title: rest.title,
|
||||
content: rest.content,
|
||||
// numeric/array fields
|
||||
price: rest.price ?? 0,
|
||||
prepareTime: rest.prepareTime ?? 0,
|
||||
images: rest.images ?? undefined,
|
||||
// new fields
|
||||
weekDays: rest.weekDays ?? [0, 1, 2, 3, 4, 5, 6],
|
||||
mealTypes: rest.mealTypes ?? [],
|
||||
restaurant: restaurant,
|
||||
category: category,
|
||||
};
|
||||
|
||||
const food = em.create(Food, data);
|
||||
|
||||
|
||||
await em.flush();
|
||||
return { food };
|
||||
});
|
||||
|
||||
// Re-load created entities with the primary EM to ensure they're attached
|
||||
const savedFood = food?.id
|
||||
? await this.foodRepository.findOne({ id: food.id }, { populate: ['category', 'restaurant'] })
|
||||
: null;
|
||||
|
||||
return { food: savedFood ?? food };
|
||||
}
|
||||
|
||||
findAll(restId: string, dto: FindFoodsDto) {
|
||||
return this.foodRepository.findAllPaginated(restId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public food detail (only active foods are visible).
|
||||
*/
|
||||
async findPublicById(foodId: string, userId?: string): Promise<any> {
|
||||
const food = await this.foodRepository.findOne({ id: foodId, isActive: true }, { populate: ['category',] });
|
||||
if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
let isFavorite = false;
|
||||
|
||||
return {
|
||||
...food,
|
||||
isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin food detail (scoped to the authenticated restaurant).
|
||||
*/
|
||||
async findAdminById(restId: string, id: string): Promise<Food> {
|
||||
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['category',] });
|
||||
|
||||
if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
return food;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active foods for a restaurant based on current week day and meal type in Iran timezone.
|
||||
* @param slug - Restaurant slug identifier
|
||||
* @returns Array of active foods matching current day and meal time
|
||||
*/
|
||||
async findByWeekDateAndMealType(slug: string): Promise<Food[]> {
|
||||
const restaurant = await this.restRepository.findOne({ slug });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { iranWeekDay, mealType } = this.getCurrentIranTimeContext();
|
||||
|
||||
const queryFilter: FilterQuery<Food> = {
|
||||
restaurant: { slug },
|
||||
isActive: true,
|
||||
weekDays: { $contains: iranWeekDay },
|
||||
...(mealType ? { mealTypes: { $contains: mealType } } : {}),
|
||||
} as unknown as FilterQuery<Food>;
|
||||
|
||||
return this.foodRepository.find(queryFilter, {
|
||||
populate: ['category'],
|
||||
orderBy: { order: 'asc' }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current Iran timezone context (weekday and meal type).
|
||||
* @returns Object containing Iran weekday (0-6, where 0=Saturday) and current meal type
|
||||
*/
|
||||
private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
|
||||
const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
|
||||
const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
|
||||
const currentHour = nowInIran.getHours();
|
||||
|
||||
// Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
|
||||
// JavaScript: Sunday=0, Monday=1, ..., Saturday=6
|
||||
// Iran week: Saturday=0, Sunday=1, ..., Friday=6
|
||||
const iranWeekDay = (weekDay + 1) % 7;
|
||||
|
||||
const mealType = this.getMealTypeByHour(currentHour);
|
||||
|
||||
return { iranWeekDay, mealType };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine meal type based on current hour in Iran timezone.
|
||||
* @param hour - Current hour (0-23)
|
||||
* @returns Meal type or null if outside meal hours
|
||||
*/
|
||||
private getMealTypeByHour(hour: number): MealType | null {
|
||||
const MEAL_TIME_RANGES = {
|
||||
BREAKFAST: { start: 6, end: 11 },
|
||||
LUNCH: { start: 11, end: 15 },
|
||||
SNACK: { start: 15, end: 19 },
|
||||
DINNER: { start: 19, end: 24 },
|
||||
} as const;
|
||||
|
||||
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
|
||||
return MealType.BREAKFAST;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
|
||||
return MealType.LUNCH;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
|
||||
return MealType.SNACK;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
|
||||
return MealType.DINNER;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async update(restId: string, id: string, dto: Partial<CreateFoodDto>): Promise<Food> {
|
||||
const { categoryId, dailyStock, ...rest } = dto;
|
||||
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['restaurant'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
|
||||
if (categoryId) {
|
||||
const category = await this.categoryRepository.findOne({ id: categoryId, restaurant: { id: restId } });
|
||||
|
||||
if (!category) {
|
||||
throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
this.em.assign(food, { category: category });
|
||||
}
|
||||
|
||||
// assign other fields from DTO (excluding dailyStock handled below)
|
||||
this.em.assign(food, rest);
|
||||
|
||||
// Persist food and update/create related inventory atomically
|
||||
await this.em.transactional(async em => {
|
||||
await em.persistAndFlush(food);
|
||||
|
||||
|
||||
await em.flush();
|
||||
});
|
||||
|
||||
// Re-load the food to ensure populated relations and stable instance
|
||||
const savedFood = await this.foodRepository.findOne({ id: food.id }, { populate: ['category', 'restaurant'] });
|
||||
|
||||
// Invalidate cache for the restaurant if present (optional)
|
||||
// await this.invalidateRestaurantFoodsCache(savedFood?.restaurant?.slug);
|
||||
|
||||
return savedFood ?? food;
|
||||
}
|
||||
|
||||
async remove(restId: string, id: string): Promise<void> {
|
||||
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['restaurant'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// const restaurantSlug = food.restaurant.slug;
|
||||
food.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(food);
|
||||
|
||||
// Invalidate cache for the restaurant
|
||||
// await this.invalidateRestaurantFoodsCache(restaurantSlug);
|
||||
}
|
||||
|
||||
|
||||
async toggleFavorite(userId: string, foodId: string) {
|
||||
|
||||
const favorite = await this.em.findOne(Favorite, { food: foodId });
|
||||
if (favorite) {
|
||||
return this.em.removeAndFlush(favorite);
|
||||
}
|
||||
const newFavorite = this.em.create(Favorite, {
|
||||
food: foodId,
|
||||
});
|
||||
return this.em.persistAndFlush(newFavorite);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Invalidate cache for restaurant foods
|
||||
*/
|
||||
// private async invalidateRestaurantFoodsCache(slug: string): Promise<void> {
|
||||
// const cacheKey = `${this.FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX}${slug}`;
|
||||
// await this.cacheService.del(cacheKey);
|
||||
// }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Category } from '../entities/category.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryRepository extends EntityRepository<Category> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Category);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { Food } from '../entities/food.entity';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
|
||||
type FindFoodsOpts = {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
orderBy?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
categoryId?: string;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FoodRepository extends EntityRepository<Food> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Food);
|
||||
}
|
||||
/**
|
||||
* Find foods with pagination and optional filters.
|
||||
* Supports: search (title/content), categoryId, isActive, ordering.
|
||||
*/
|
||||
async findAllPaginated(restId: string, opts: FindFoodsOpts = {}): Promise<PaginatedResult<Food>> {
|
||||
const { page = 1, limit = 10, search, orderBy = 'order', order = 'asc', categoryId, isActive } = opts;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: FilterQuery<Food> = { restaurant: { id: restId } };
|
||||
|
||||
if (typeof isActive === 'boolean') {
|
||||
where.isActive = isActive;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const pattern = `%${search}%`;
|
||||
where.$or = [{ title: { $ilike: pattern } }, { desc: { $ilike: pattern } }];
|
||||
}
|
||||
|
||||
if (categoryId) {
|
||||
// filter by related category (Food has a single `category` relation)
|
||||
Object.assign(where, { category: { id: categoryId } } as unknown as FilterQuery<Food>);
|
||||
}
|
||||
|
||||
const [data, total] = await this.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
populate: ['category' ],
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { NotifTitleEnum } from '../interfaces/notification.interface';
|
||||
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
||||
|
||||
@Entity({ tableName: 'notifications' })
|
||||
export class Notification extends BaseEntity {
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
|
||||
@ManyToOne(() => Admin, { nullable: true })
|
||||
admin?: Admin;
|
||||
|
||||
@Enum(() => NotifTitleEnum)
|
||||
title!: NotifTitleEnum;
|
||||
|
||||
@Property()
|
||||
content!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
seenAt?: Date;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { WsException } from '@nestjs/websockets';
|
||||
import { Socket } from 'socket.io';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
|
||||
export interface AuthenticatedSocket extends Socket {
|
||||
adminId?: string;
|
||||
restId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WsAdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(WsAdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const client: Socket = context.switchToWs().getClient<Socket>();
|
||||
|
||||
try {
|
||||
const token = this.extractToken(client);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn('No token provided in WebSocket connection', {
|
||||
socketId: client.id,
|
||||
auth: client.handshake.auth,
|
||||
query: client.handshake.query,
|
||||
});
|
||||
throw new WsException('Authentication required');
|
||||
}
|
||||
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new WsException('Invalid token payload');
|
||||
}
|
||||
|
||||
// Attach admin info to socket
|
||||
(client as AuthenticatedSocket).adminId = payload.adminId;
|
||||
(client as AuthenticatedSocket).restId = payload.restId;
|
||||
|
||||
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, restId: ${payload.restId}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WsException) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error('WebSocket authentication error', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
socketId: client.id,
|
||||
});
|
||||
throw new WsException('Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private extractToken(client: Socket): string | undefined {
|
||||
// Try to get token from handshake auth (recommended for socket.io)
|
||||
const auth = client.handshake.auth;
|
||||
const authToken = (auth?.token as string | undefined) || (auth?.authorization as string | undefined);
|
||||
|
||||
if (authToken) {
|
||||
// If it's "Bearer <token>", extract just the token
|
||||
if (typeof authToken === 'string' && authToken.startsWith('Bearer ')) {
|
||||
return authToken.substring(7);
|
||||
}
|
||||
return authToken;
|
||||
}
|
||||
|
||||
// Try to get from query parameters (fallback)
|
||||
const queryToken = client.handshake.query?.token || client.handshake.query?.authorization;
|
||||
|
||||
if (queryToken) {
|
||||
if (typeof queryToken === 'string' && queryToken.startsWith('Bearer ')) {
|
||||
return queryToken.substring(7);
|
||||
}
|
||||
return queryToken as string;
|
||||
}
|
||||
|
||||
// Try to get from headers (if available)
|
||||
const headers = client.handshake.headers;
|
||||
const authHeader = headers.authorization || headers['authorization'];
|
||||
|
||||
if (authHeader && typeof authHeader === 'string') {
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() === 'bearer' && token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// private extractToken(client: Socket): string | undefined {
|
||||
// const tryGet = (...values: any[]) => values.find(v => typeof v === 'string' && v.length > 0);
|
||||
|
||||
// let token = tryGet(
|
||||
// client.handshake.auth?.token,
|
||||
// client.handshake.auth?.authorization,
|
||||
// client.handshake.query?.token,
|
||||
// client.handshake.query?.authorization,
|
||||
// client.handshake.headers.authorization,
|
||||
// client.handshake.headers['authorization'],
|
||||
// );
|
||||
|
||||
// if (!token) return undefined;
|
||||
|
||||
// if (token.startsWith('Bearer ')) {
|
||||
// token = token.slice(7);
|
||||
// }
|
||||
|
||||
// return token;
|
||||
// }
|
||||
}
|
||||
@@ -2,28 +2,21 @@ import { Module, forwardRef } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { Notification } from './entities/notification.entity';
|
||||
import { NotificationQueueService } from './services/notification-queue.service';
|
||||
import { PushNotificationService } from './services/push-notification.service';
|
||||
import { SmsProcessor } from './processors/sms.processor';
|
||||
import { PushProcessor } from './processors/push.processor';
|
||||
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NotificationQueueNameEnum } from './constants/queue';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { WsAdminAuthGuard } from './guards/ws-admin-auth.guard';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { SmsService } from './services/sms.service';
|
||||
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AuthModule),
|
||||
JwtModule,
|
||||
MikroOrmModule.forFeature([Notification, Restaurant]),
|
||||
BullModule.registerQueue(
|
||||
BullModule.registerQueue(
|
||||
{ name: NotificationQueueNameEnum.SMS },
|
||||
{ name: NotificationQueueNameEnum.PUSH },
|
||||
{ name: NotificationQueueNameEnum.IN_APP },
|
||||
@@ -40,18 +33,14 @@ import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
}),
|
||||
AdminModule,
|
||||
UtilsModule,
|
||||
forwardRef(() => RestaurantsModule),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
NotificationQueueService,
|
||||
PushNotificationService,
|
||||
PushProcessor,
|
||||
SmsProcessor,
|
||||
WsAdminAuthGuard,
|
||||
SmsService,
|
||||
],
|
||||
exports: [
|
||||
NotificationQueueService, PushNotificationService, SmsService],
|
||||
NotificationQueueService, SmsService],
|
||||
})
|
||||
export class NotificationsModule { }
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository, EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Notification } from '../entities/notification.entity';
|
||||
import { PushNotificationQueueJob } from '../interfaces/jobs-queue.interface';
|
||||
import { NotificationQueueNameEnum } from '../constants/queue';
|
||||
import { PushNotificationService } from '../services/push-notification.service';
|
||||
|
||||
@Processor(NotificationQueueNameEnum.PUSH)
|
||||
export class PushProcessor extends WorkerHost {
|
||||
private readonly logger = new Logger(PushProcessor.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Notification)
|
||||
private readonly notificationRepository: EntityRepository<Notification>,
|
||||
private readonly em: EntityManager,
|
||||
private readonly pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<PushNotificationQueueJob>) {
|
||||
const { title, content, action, pushToken, pushTokens } = job.data;
|
||||
|
||||
this.logger.log(`Processing push notification job: ${job.id} - Title: ${title},`);
|
||||
|
||||
try {
|
||||
// Get push token from job data
|
||||
const pushToken = job.data.pushToken;
|
||||
const pushTokens = job.data.pushTokens;
|
||||
|
||||
if (!pushToken && (!pushTokens || pushTokens.length === 0)) {
|
||||
this.logger.warn(`Push token(s) not provided for notification `);
|
||||
throw new Error('Push token or pushTokens array is required for push notifications');
|
||||
}
|
||||
|
||||
// Send push notification
|
||||
// Convert NotificationTitle enum to a readable string for the notification title
|
||||
const notificationTitle = String(title)
|
||||
.replace(/\./g, ' ')
|
||||
.replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
let pushResult;
|
||||
if (pushTokens && pushTokens.length > 0) {
|
||||
// Send to multiple tokens
|
||||
pushResult = await this.pushNotificationService.sendToMultipleTokens({
|
||||
title: notificationTitle,
|
||||
body: content,
|
||||
tokens: pushTokens,
|
||||
data: {
|
||||
action,
|
||||
},
|
||||
});
|
||||
} else if (pushToken) {
|
||||
// Send to single token
|
||||
pushResult = await this.pushNotificationService.sendToToken({
|
||||
title: notificationTitle,
|
||||
body: content,
|
||||
token: pushToken,
|
||||
data: {
|
||||
action,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error('Push token or pushTokens array is required');
|
||||
}
|
||||
|
||||
if (!pushResult.success) {
|
||||
throw new Error(pushResult.error || 'Failed to send push notification');
|
||||
}
|
||||
|
||||
this.logger.log(`Push notification sent successfully`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
providerResponse: { messageId: pushResult.messageId },
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing push notification job ${job.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@OnWorkerEvent('completed')
|
||||
onCompleted(job: Job) {
|
||||
this.logger.log(`Notification job ${job.id} completed`);
|
||||
}
|
||||
|
||||
@OnWorkerEvent('failed')
|
||||
onFailed(job: Job, error: Error) {
|
||||
this.logger.error(`Notification job ${job.id} failed:`, error);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
export interface PushNotificationPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, any>;
|
||||
token?: string;
|
||||
tokens?: string[];
|
||||
}
|
||||
|
||||
export interface PushNotificationResponse {
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PushNotificationService {
|
||||
private readonly logger = new Logger(PushNotificationService.name);
|
||||
private firebaseApp: admin.app.App | null = null;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.initializeFirebase();
|
||||
}
|
||||
|
||||
private initializeFirebase() {
|
||||
try {
|
||||
const firebaseConfig = this.configService.get<string>('FIREBASE_SERVICE_ACCOUNT');
|
||||
|
||||
if (!firebaseConfig) {
|
||||
this.logger.warn('Firebase service account not configured. Push notifications will be disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the service account JSON
|
||||
const serviceAccount = JSON.parse(firebaseConfig);
|
||||
|
||||
if (!this.firebaseApp) {
|
||||
this.firebaseApp = admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
});
|
||||
this.logger.log('Firebase initialized successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Firebase:', error);
|
||||
this.logger.warn('Push notifications will be disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async sendToToken(payload: PushNotificationPayload): Promise<PushNotificationResponse> {
|
||||
if (!this.firebaseApp || !payload.token) {
|
||||
throw new HttpException(
|
||||
'Push notification service not configured or token missing',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const message: admin.messaging.Message = {
|
||||
notification: {
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
},
|
||||
data: this.convertDataToString(payload.data || {}),
|
||||
token: payload.token,
|
||||
android: {
|
||||
priority: 'high',
|
||||
},
|
||||
apns: {
|
||||
headers: {
|
||||
'apns-priority': '10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await admin.messaging().send(message);
|
||||
this.logger.log(`Push notification sent successfully: ${response}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageId: response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send push notification: ${error}`);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async sendToMultipleTokens(payload: PushNotificationPayload): Promise<PushNotificationResponse> {
|
||||
if (!this.firebaseApp || !payload.tokens || payload.tokens.length === 0) {
|
||||
throw new HttpException(
|
||||
'Push notification service not configured or tokens missing',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const message: admin.messaging.MulticastMessage = {
|
||||
notification: {
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
},
|
||||
data: this.convertDataToString(payload.data || {}),
|
||||
tokens: payload.tokens,
|
||||
android: {
|
||||
priority: 'high',
|
||||
},
|
||||
apns: {
|
||||
headers: {
|
||||
'apns-priority': '10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await admin.messaging().sendEachForMulticast(message);
|
||||
this.logger.log(`Push notifications sent: ${response.successCount} successful, ${response.failureCount} failed`);
|
||||
|
||||
return {
|
||||
success: response.successCount > 0,
|
||||
messageId: response.responses[0]?.messageId,
|
||||
error: response.failureCount > 0 ? `${response.failureCount} notifications failed` : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send push notifications: ${error}`);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private convertDataToString(data: Record<string, any>): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return this.firebaseApp !== null;
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, UseGuards, Delete, Query } from '@nestjs/common';
|
||||
import { RestaurantsService } from '../providers/restaurants.service';
|
||||
import { CreateRestaurantDto } from '../dto/create-restaurant.dto';
|
||||
import { UpdateRestaurantDto } from '../dto/update-restaurant.dto';
|
||||
import { UpgradeSubscriptionDto } from '../dto/upgrade-subscription.dto';
|
||||
import { FindRestaurantsDto } from '../dto/find-restaurants.dto';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiNotFoundResponse,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiTags,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
|
||||
@ApiTags('restaurants')
|
||||
@Controller()
|
||||
export class RestaurantsController {
|
||||
constructor(private readonly restaurantsService: RestaurantsService) { }
|
||||
|
||||
@Get('public/restaurants/:slug')
|
||||
@ApiOperation({ summary: 'Get restaurant specification by slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant slug' })
|
||||
@ApiNotFoundResponse({ description: 'Restaurant not found' })
|
||||
findBySlug(@Param('slug') slug: string) {
|
||||
return this.restaurantsService.getRestaurantSpecification(slug);
|
||||
}
|
||||
|
||||
/** Admin Endpoints */
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.UPDATE_RESTAURANT)
|
||||
@ApiOperation({ summary: 'Get restaurant by ID from request' })
|
||||
@Get('admin/restaurants/my-restaurant')
|
||||
async findOne(@RestId() restId: string) {
|
||||
return await this.restaurantsService.findOne(restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.UPDATE_RESTAURANT)
|
||||
@ApiOperation({ summary: 'Update a restaurant' })
|
||||
@ApiBody({ type: UpdateRestaurantDto })
|
||||
@Patch('admin/restaurants/my-restaurant')
|
||||
updateMyRestaurant(@RestId() restId: string, @Body() updateRestaurantDto: UpdateRestaurantDto) {
|
||||
return this.restaurantsService.update(restId, updateRestaurantDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.UPDATE_RESTAURANT)
|
||||
@ApiOperation({ summary: 'Delete a restaurant' })
|
||||
@Delete('admin/restaurants/:id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.restaurantsService.remove(id);
|
||||
}
|
||||
|
||||
/** Super Admin Endpoints */
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get all restaurants with pagination and filters' })
|
||||
@Get('super-admin/restaurants')
|
||||
findAll(@Query() dto: FindRestaurantsDto) {
|
||||
return this.restaurantsService.findAll(dto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create a new restaurant' })
|
||||
@Post('super-admin/restaurants')
|
||||
createRestaurant(@Body() createRestaurantDto: CreateRestaurantDto) {
|
||||
return this.restaurantsService.setupRestuarant(createRestaurantDto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get restaurant by subscription ID' })
|
||||
@ApiParam({ name: 'subscriptionId', required: true, description: 'Subscription ID' })
|
||||
@Get('super-admin/restaurants/subscription/:subscriptionId')
|
||||
findOneBySubscriptionId(@Param('subscriptionId') subscriptionId: string) {
|
||||
return this.restaurantsService.findOneBySubscriptionId(subscriptionId);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get restaurant by ID' })
|
||||
@ApiParam({ name: 'id', required: true, description: 'Restaurant ID' })
|
||||
@Get('super-admin/restaurants/:id')
|
||||
findOneById(@Param('id') id: string) {
|
||||
return this.restaurantsService.findOne(id);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update a restaurant by ID' })
|
||||
@ApiParam({ name: 'id', required: true, description: 'Restaurant ID' })
|
||||
@ApiBody({ type: UpdateRestaurantDto })
|
||||
@Patch('super-admin/restaurants/:id')
|
||||
updateRestaurant(@Param('id') id: string, @Body() updateRestaurantDto: UpdateRestaurantDto) {
|
||||
return this.restaurantsService.update(id, updateRestaurantDto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Upgrade subscription for a restaurant' })
|
||||
@ApiParam({ name: 'subscriptionId', required: true, description: 'Subscription ID' })
|
||||
@ApiBody({ type: UpgradeSubscriptionDto })
|
||||
@Patch('super-admin/restaurants/subscription/:subscriptionId/upgrade')
|
||||
upgradeSubscription(
|
||||
@Param('subscriptionId') subscriptionId: string,
|
||||
@Body() upgradeSubscriptionDto: UpgradeSubscriptionDto
|
||||
) {
|
||||
return this.restaurantsService.upgradeSubscription(subscriptionId, upgradeSubscriptionDto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Hard delete a restaurant and all related data' })
|
||||
@ApiParam({ name: 'id', required: true, description: 'Restaurant ID' })
|
||||
@Delete('super-admin/restaurants/:id/hard-delete')
|
||||
hardDeleteRestaurant(@Param('id') id: string) {
|
||||
return this.restaurantsService.hardDeleteRestaurant(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query, ValidationPipe } from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { ScheduleService } from '../providers/schedule.service';
|
||||
import { CreateScheduleDto } from '../dto/create-schedule.dto';
|
||||
import { UpdateScheduleDto } from '../dto/update-schedule.dto';
|
||||
import { FindSchedulesDto } from '../dto/find-schedules.dto';
|
||||
import { Schedule } from '../entities/schedule.entity';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
|
||||
@ApiTags('schedules')
|
||||
@Controller()
|
||||
export class ScheduleController {
|
||||
constructor(private readonly scheduleService: ScheduleService) { }
|
||||
|
||||
@Get('public/schedules/restaurant/:slug')
|
||||
@ApiOperation({ summary: 'Get schedule of a restaurant by slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', description: 'Restaurant Slug' })
|
||||
@ApiOkResponse({ description: 'Today schedule of the restaurant', type: Schedule })
|
||||
async getTodaySchedules(@Param('slug') slug: string): Promise<Schedule[]> {
|
||||
return this.scheduleService.findScheduleBySlug(slug);
|
||||
}
|
||||
|
||||
/******************** Admin Routes **********************/
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_SCHEDULES)
|
||||
@Post('admin/schedules')
|
||||
@ApiOperation({ summary: 'Create a new schedule' })
|
||||
@ApiBody({ type: CreateScheduleDto })
|
||||
@ApiCreatedResponse({ description: 'Schedule created successfully', type: Schedule })
|
||||
create(@Body() createScheduleDto: CreateScheduleDto, @RestId() restId: string) {
|
||||
return this.scheduleService.create(restId, createScheduleDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_SCHEDULES)
|
||||
@Get('admin/schedules')
|
||||
@ApiOperation({ summary: 'Get all schedules' })
|
||||
@ApiQuery({
|
||||
name: 'weekDay',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Filter by week days (0=Sunday, 6=Saturday).',
|
||||
})
|
||||
@ApiOkResponse({ description: 'List of schedules', type: [Schedule] })
|
||||
findAll(
|
||||
@Query(new ValidationPipe({ transform: true, whitelist: true })) query: FindSchedulesDto,
|
||||
@RestId() restId: string,
|
||||
) {
|
||||
return this.scheduleService.findAll(restId, query);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_SCHEDULES)
|
||||
@Get('admin/schedules/:id')
|
||||
@ApiOperation({ summary: 'Get a schedule by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Schedule ID' })
|
||||
@ApiOkResponse({ description: 'Schedule details', type: Schedule })
|
||||
@ApiNotFoundResponse({ description: 'Schedule not found' })
|
||||
findOne(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.scheduleService.findOne(id, restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_SCHEDULES)
|
||||
@Patch('admin/schedules/:id')
|
||||
@ApiOperation({ summary: 'Update a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'Schedule ID' })
|
||||
@ApiBody({ type: UpdateScheduleDto })
|
||||
@ApiOkResponse({ description: 'Schedule updated successfully', type: Schedule })
|
||||
@ApiNotFoundResponse({ description: 'Schedule not found' })
|
||||
update(@Param('id') id: string, @Body() updateScheduleDto: UpdateScheduleDto, @RestId() restId: string) {
|
||||
return this.scheduleService.update(id, restId, updateScheduleDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_SCHEDULES)
|
||||
@Delete('admin/schedules/:id')
|
||||
@ApiOperation({ summary: 'Delete a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'Schedule ID' })
|
||||
@ApiOkResponse({ description: 'Schedule deleted successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Schedule not found' })
|
||||
remove(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.scheduleService.remove(id, restId);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RestaurantCrone {
|
||||
private readonly logger = new Logger(RestaurantCrone.name);
|
||||
|
||||
constructor(private readonly em: EntityManager) { }
|
||||
|
||||
// run every day at 03:00 (3:00 AM)
|
||||
@Cron('0 3 * * *', {
|
||||
name: 'deactivateExpiredSubscriptions',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
async handleCron() {
|
||||
try {
|
||||
this.logger.debug('Starting daily subscription expiration check');
|
||||
|
||||
// Get current date
|
||||
const now = new Date();
|
||||
|
||||
// Find restaurants where subscription has expired and they're still active
|
||||
const expiredRestaurants = await this.em.find(Restaurant, {
|
||||
subscriptionEndDate: { $lt: now },
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
if (!expiredRestaurants || expiredRestaurants.length === 0) {
|
||||
this.logger.debug('No restaurants with expired subscriptions found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${expiredRestaurants.length} restaurants with expired subscriptions`);
|
||||
|
||||
// Deactivate expired restaurants
|
||||
for (const restaurant of expiredRestaurants) {
|
||||
restaurant.isActive = false;
|
||||
}
|
||||
|
||||
// Persist all changes
|
||||
await this.em.persistAndFlush(expiredRestaurants);
|
||||
|
||||
this.logger.log(`Successfully deactivated ${expiredRestaurants.length} restaurants with expired subscriptions`);
|
||||
} catch (err) {
|
||||
this.logger.error(`RestaurantCrone failed: ${err?.message}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsNumber, IsDate, IsEnum, IsBoolean } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
|
||||
export class CreateRestaurantDto {
|
||||
@ApiProperty({ example: 'کافه ژیوان', description: 'نام رستوران' })
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'zhivan', description: 'شناسه (slug) رستوران، لاتین و بدون فاصله (اختیاری - در صورت عدم ارسال، از نام رستوران تولید میشود)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
slug?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'zhivan.dmenu.ir', description: 'دامنه اختصاصی رستوران' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
domain?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 2020, description: 'سال تأسیس' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
establishedYear: number;
|
||||
|
||||
@ApiPropertyOptional({ example: '09123456789', description: 'شماره تلفن' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ example: 'base', enum: PlanEnum, description: 'پلن رستوران' })
|
||||
@IsEnum(PlanEnum)
|
||||
plan!: PlanEnum;
|
||||
|
||||
@ApiProperty({ example: 'sub_1234567890', description: 'شناسه اشتراک' })
|
||||
@IsString()
|
||||
subscriptionId!: string;
|
||||
|
||||
@ApiProperty({ example: '2025-12-26', description: 'تاریخ انقضای اشتراک' })
|
||||
@Type(() => Date)
|
||||
@IsDate()
|
||||
subscriptionEndDate!: Date;
|
||||
|
||||
@ApiProperty({ example: '2025-12-26', description: 'تاریخ شروع اشتراک' })
|
||||
@Type(() => Date)
|
||||
@IsDate()
|
||||
subscriptionStartDate!: Date;
|
||||
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { IsNumber, IsBoolean, IsOptional, IsString, Min, Max, Matches } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateScheduleDto {
|
||||
@ApiProperty({ example: 1, description: 'روز هفته (۰=یکشنبه، ۶=شنبه)' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(6)
|
||||
@Type(() => Number)
|
||||
weekDay!: number;
|
||||
|
||||
@ApiProperty({ example: '08:00', description: 'زمان باز شدن (HH:MM, از ۰۰:۰۰ تا ۲۳:۵۹)' })
|
||||
@IsString()
|
||||
@Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, {
|
||||
message: 'openTime باید در فرمت HH:MM باشد (00:00 تا 23:59)',
|
||||
})
|
||||
openTime!: string;
|
||||
|
||||
@ApiProperty({ example: '23:00', description: 'زمان بستن (HH:MM, از ۰۰:۰۰ تا ۲۳:۵۹)' })
|
||||
@IsString()
|
||||
@Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, {
|
||||
message: 'closeTime باید در فرمت HH:MM باشد (00:00 تا 23:59)',
|
||||
})
|
||||
closeTime!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true, description: 'آیا این برنامه فعال است؟' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsNumber,
|
||||
Min,
|
||||
IsIn,
|
||||
IsEnum,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindRestaurantsDto {
|
||||
@ApiPropertyOptional({ default: 1, minimum: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
page: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ default: 10, minimum: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
limit: number = 10;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search by name, slug, domain, or address',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter by active status',
|
||||
type: Boolean,
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
return value;
|
||||
})
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: PlanEnum,
|
||||
description: 'Filter by plan type',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(PlanEnum)
|
||||
plan?: PlanEnum;
|
||||
|
||||
@ApiPropertyOptional({ default: 'createdAt' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderBy: string = 'createdAt';
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: sortOrderOptions,
|
||||
default: 'desc',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
order: SortOrder = 'desc';
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { IsOptional, IsNumber, Min, Max } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FindSchedulesDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(6)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter by week days (0=Sunday, 6=Saturday)',
|
||||
type: Number,
|
||||
})
|
||||
weekDay?: number;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class RestaurantSpecificationDto {
|
||||
@ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000', description: 'Restaurant ID' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: 'کافه ژیوان', description: 'Restaurant name' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: 'zhivan', description: 'Restaurant slug' })
|
||||
slug!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'https://example.com/logo.png', description: 'Logo URL' })
|
||||
logo?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'تهران، خیابان ولیعصر پلاک ۱۲۳', description: 'Address' })
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '#ff6600', description: 'Menu color (hex)' })
|
||||
menuColor?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 35.6892, description: 'Latitude' })
|
||||
latitude?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 51.389, description: 'Longitude' })
|
||||
longitude?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[51.389, 35.6892],
|
||||
[51.39, 35.6892],
|
||||
[51.39, 35.6902],
|
||||
[51.389, 35.6902],
|
||||
[51.389, 35.6892],
|
||||
],
|
||||
],
|
||||
},
|
||||
description: 'Service area as GeoJSON Polygon',
|
||||
})
|
||||
serviceArea?: {
|
||||
type: 'Polygon';
|
||||
coordinates: number[][][];
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({ example: 2020, description: 'Established year' })
|
||||
establishedYear?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: '+989123456789', description: 'Phone number' })
|
||||
phoneNumber?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '09123456789', description: 'Phone' })
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'https://instagram.com/cafemorteza', description: 'Instagram URL' })
|
||||
instagram?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'https://t.me/cafemorteza', description: 'Telegram URL' })
|
||||
telegram?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'https://wa.me/989123456789', description: 'WhatsApp URL' })
|
||||
whatsapp?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'محل دنج با بهترین قهوه شهر.', description: 'Description' })
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'بهترین کافه تهران', description: 'SEO title' })
|
||||
seoTitle?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'کافه مرتضی ارائهدهنده قهوه با کیفیت بالا.', description: 'SEO description' })
|
||||
seoDescription?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: ['قهوه', 'صبحانه', 'کافه'], description: 'Tag names', type: [String] })
|
||||
tagNames?: string[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
|
||||
description: 'Image URLs',
|
||||
type: [String],
|
||||
})
|
||||
images?: string[];
|
||||
|
||||
@ApiPropertyOptional({ example: 0.09, description: 'VAT rate (e.g., 0.09 for 9%)' })
|
||||
vat?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: { registerScore: 100 }, description: 'Score' })
|
||||
score?: {
|
||||
registerScore?: number;
|
||||
};
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateRestaurantDto } from './create-restaurant.dto';
|
||||
|
||||
export class UpdateRestaurantDto extends PartialType(CreateRestaurantDto) {}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateScheduleDto } from './create-schedule.dto';
|
||||
|
||||
export class UpdateScheduleDto extends PartialType(CreateScheduleDto) {}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsDate, IsEnum } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
|
||||
export class UpgradeSubscriptionDto {
|
||||
@ApiProperty({ example: 'premium', enum: PlanEnum, description: 'New plan for the subscription' })
|
||||
@IsEnum(PlanEnum)
|
||||
newPlan!: PlanEnum;
|
||||
|
||||
@ApiProperty({ example: '2025-12-31', description: 'New subscription end date' })
|
||||
@Type(() => Date)
|
||||
@IsDate()
|
||||
subscriptionEndDate!: Date;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Collection, Entity, Enum, Index, OneToMany, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
|
||||
@Entity({ tableName: 'restaurants' })
|
||||
@Index({ properties: ['isActive'] })
|
||||
@Index({ properties: ['slug', 'isActive'] })
|
||||
export class Restaurant extends BaseEntity {
|
||||
// --- اطلاعات پایه ---
|
||||
@Property()
|
||||
name!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
slug!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
address?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
menuColor?: string;
|
||||
|
||||
// --- مختصات جغرافیایی ---
|
||||
@Property({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||
latitude?: number;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||
longitude?: number;
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
serviceArea?: {
|
||||
type: 'Polygon';
|
||||
coordinates: number[][][];
|
||||
};
|
||||
|
||||
// --- وضعیتها ---
|
||||
@Property({ default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@Property({ nullable: true })
|
||||
establishedYear?: number;
|
||||
|
||||
@Property({ nullable: true })
|
||||
phone?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
instagram?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
telegram?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
whatsapp?: string;
|
||||
|
||||
// --- توضیحات ---
|
||||
@Property({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
// --- سئو ---
|
||||
@Property({ nullable: true })
|
||||
seoTitle?: string;
|
||||
|
||||
@Property({ nullable: true, type: 'text' })
|
||||
seoDescription?: string;
|
||||
|
||||
@Property({ nullable: true, type: 'json' })
|
||||
tagNames?: string[];
|
||||
|
||||
// --- تصاویر ---
|
||||
@Property({ nullable: true, type: 'json' })
|
||||
images?: string[];
|
||||
|
||||
// --- مالیات یا VAT ---
|
||||
@Property({ type: 'decimal', default: 0 })
|
||||
vat?: number = 0;
|
||||
|
||||
@Property()
|
||||
domain!: string;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Enum(() => PlanEnum)
|
||||
plan: PlanEnum = PlanEnum.Base;
|
||||
|
||||
@Property({unique: true,nullable: true})
|
||||
subscriptionId?: string;
|
||||
|
||||
@Property({nullable: true})
|
||||
subscriptionEndDate?: Date;
|
||||
|
||||
@Property({nullable: true})
|
||||
subscriptionStartDate?: Date;
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Entity, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
@Entity({ tableName: 'schedules' })
|
||||
export class Schedule extends BaseEntity {
|
||||
@Property({ type: 'int' })
|
||||
weekDay!: number;
|
||||
|
||||
@Property({ type: 'time' })
|
||||
openTime!: string;
|
||||
|
||||
@Property({ type: 'time' })
|
||||
closeTime!: string;
|
||||
|
||||
@Property({ default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@Property()
|
||||
restId!: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum PlanEnum {
|
||||
Base = 'base',
|
||||
Premium = 'premium',
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateRestaurantDto } from '../dto/create-restaurant.dto';
|
||||
import { UpdateRestaurantDto } from '../dto/update-restaurant.dto';
|
||||
import { UpgradeSubscriptionDto } from '../dto/upgrade-subscription.dto';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RestRepository } from '../repositories/rest.repository';
|
||||
import { RestMessage } from 'src/common/enums/message.enum';
|
||||
import { FindRestaurantsDto } from '../dto/find-restaurants.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
import { Admin } from '../../admin/entities/admin.entity';
|
||||
import { AdminRole } from '../../admin/entities/adminRole.entity';
|
||||
import { normalizePhone } from 'src/modules/utils/phone.util';
|
||||
import slugify from 'slugify';
|
||||
import { Category } from '../../foods/entities/category.entity';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
import { Schedule } from '../entities/schedule.entity';
|
||||
|
||||
import { Notification } from '../../notifications/entities/notification.entity';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class RestaurantsService {
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly restRepository: RestRepository,
|
||||
) { }
|
||||
|
||||
async setupRestuarant(dto: CreateRestaurantDto): Promise<Restaurant> {
|
||||
return await this.em.transactional(async (em) => {
|
||||
// Generate or validate slug
|
||||
const slug = slugify(dto.slug ?? dto.name, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'fa'
|
||||
});
|
||||
|
||||
|
||||
const validateSubscriptionId = await em.findOne(Restaurant, { subscriptionId: dto.subscriptionId })
|
||||
if (validateSubscriptionId) {
|
||||
throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId)
|
||||
}
|
||||
|
||||
// Create restaurant
|
||||
const restaurant = em.create(Restaurant, {
|
||||
name: dto.name,
|
||||
slug,
|
||||
establishedYear: dto.establishedYear,
|
||||
phone: dto.phone,
|
||||
isActive: true,
|
||||
plan: dto.plan,
|
||||
domain: `https://dmenu.danakcorp.com/${slug}`,
|
||||
subscriptionId: dto.subscriptionId,
|
||||
subscriptionEndDate: dto.subscriptionEndDate,
|
||||
subscriptionStartDate: dto.subscriptionStartDate,
|
||||
});
|
||||
|
||||
// Find the appropriate role based on plan
|
||||
const roleName = dto.plan === PlanEnum.Base ? 'مدیر (پلن پایه)' : 'مدیر ( پلن ویژه)';
|
||||
|
||||
|
||||
const normalizedPhone = normalizePhone(dto.phone);
|
||||
let admin = await em.findOne(Admin, { phone: normalizedPhone });
|
||||
if (!admin) {
|
||||
admin = em.create(Admin, {
|
||||
phone: normalizedPhone,
|
||||
firstName: '[نام]',
|
||||
lastName: '[نام خانوادگی]',
|
||||
});
|
||||
}
|
||||
|
||||
// Create admin role relationship
|
||||
const adminRole = em.create(AdminRole, {
|
||||
admin,
|
||||
restaurant,
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Persist all entities
|
||||
em.persist([restaurant, admin, adminRole]);
|
||||
await em.flush();
|
||||
|
||||
return restaurant;
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(dto: FindRestaurantsDto): Promise<PaginatedResult<Restaurant>> {
|
||||
return this.restRepository.findAllPaginated({
|
||||
page: dto.page,
|
||||
limit: dto.limit,
|
||||
search: dto.search,
|
||||
isActive: dto.isActive,
|
||||
plan: dto.plan,
|
||||
orderBy: dto.orderBy,
|
||||
order: dto.order,
|
||||
});
|
||||
}
|
||||
|
||||
async findBySlug(slug: string): Promise<Restaurant> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { slug });
|
||||
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with slug "${slug}" not found`);
|
||||
}
|
||||
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Restaurant> {
|
||||
const restaurant = await this.restRepository.findOne({ id });
|
||||
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async findOneBySubscriptionId(subscriptionId: string): Promise<Restaurant> {
|
||||
console.log('subscriptionId', subscriptionId)
|
||||
const restaurant = await this.restRepository.findOne({ subscriptionId });
|
||||
console.log('restaurant', restaurant)
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async getRestaurantSpecification(slug: string) {
|
||||
const restaurant = await this.findBySlug(slug);
|
||||
return restaurant;
|
||||
}
|
||||
// TODO : it must be done inside transaction
|
||||
async update(id: string, dto: UpdateRestaurantDto): Promise<Restaurant> {
|
||||
|
||||
const restaurant = await this.restRepository.findOne({ id: id });
|
||||
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (dto.plan && dto.plan !== restaurant.plan) {
|
||||
const isPremium = dto.plan === PlanEnum.Premium
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
this.restRepository.assign(restaurant, dto);
|
||||
|
||||
await this.em.persistAndFlush(restaurant);
|
||||
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
// Soft delete the restaurant by setting isActive to false
|
||||
const restaurant = await this.restRepository.findOne({ id });
|
||||
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
restaurant.isActive = false;
|
||||
await this.em.persistAndFlush(restaurant);
|
||||
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async upgradeSubscription(subscriptionId: string, dto: UpgradeSubscriptionDto): Promise<Restaurant> {
|
||||
const restaurant = await this.restRepository.findOne({ subscriptionId });
|
||||
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const isActive = new Date(dto.subscriptionEndDate) > new Date()
|
||||
|
||||
await this.update(restaurant.id, {
|
||||
plan: dto.newPlan,
|
||||
subscriptionEndDate: dto.subscriptionEndDate,
|
||||
isActive
|
||||
})
|
||||
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async hardDeleteRestaurant(id: string): Promise<void> {
|
||||
return await this.em.transactional(async (em) => {
|
||||
// Find the restaurant first
|
||||
const restaurant = await em.findOne(Restaurant, { id });
|
||||
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Delete foods (will cascade to reviews, favorites, and inventory due to cascade settings)
|
||||
await em.nativeDelete(Food, { restaurant: id });
|
||||
|
||||
// Delete categories (foods should be deleted by cascade, but delete explicitly to be safe)
|
||||
await em.nativeDelete(Category, { restaurant: id });
|
||||
|
||||
|
||||
|
||||
// Delete schedules
|
||||
await em.nativeDelete(Schedule, { restId: id });
|
||||
|
||||
// Delete notification preferences
|
||||
|
||||
// Delete notifications
|
||||
await em.nativeDelete(Notification, { restaurant: id });
|
||||
|
||||
// Delete SMS logs
|
||||
|
||||
// Delete admin roles for this restaurant
|
||||
await em.nativeDelete(AdminRole, { restaurant: id });
|
||||
|
||||
|
||||
// Delete wallet transactions
|
||||
|
||||
// Finally, delete the restaurant itself
|
||||
await em.nativeDelete(Restaurant, { id });
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { EntityManager, RequiredEntityData } from '@mikro-orm/postgresql';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { Schedule } from '../entities/schedule.entity';
|
||||
import { ScheduleRepository } from '../repositories/schedule.repository';
|
||||
import { CreateScheduleDto } from '../dto/create-schedule.dto';
|
||||
import { UpdateScheduleDto } from '../dto/update-schedule.dto';
|
||||
import { FindSchedulesDto } from '../dto/find-schedules.dto';
|
||||
import { RestRepository } from '../repositories/rest.repository';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
import { RestMessage } from 'src/common/enums/message.enum';
|
||||
// import { RestMessage } from 'src/common/enums/message.enum';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
constructor(
|
||||
private readonly scheduleRepository: ScheduleRepository,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async create(restId: string, createScheduleDto: CreateScheduleDto): Promise<Schedule> {
|
||||
const rest = await this.restRepository.findOne({ id: restId });
|
||||
if (!rest) {
|
||||
throw new NotFoundException(`رستوران با شناسه ${restId} یافت نشد.`);
|
||||
}
|
||||
|
||||
const data: RequiredEntityData<Schedule> = {
|
||||
weekDay: createScheduleDto.weekDay,
|
||||
openTime: createScheduleDto.openTime,
|
||||
closeTime: createScheduleDto.closeTime,
|
||||
isActive: createScheduleDto.isActive ?? true,
|
||||
restId,
|
||||
};
|
||||
|
||||
const schedule = this.scheduleRepository.create(data);
|
||||
await this.em.persistAndFlush(schedule);
|
||||
return schedule;
|
||||
}
|
||||
|
||||
async findAll(restId?: string, query?: FindSchedulesDto): Promise<Schedule[]> {
|
||||
const where: FilterQuery<Schedule> = restId ? { restId } : {};
|
||||
|
||||
if (query?.weekDay !== undefined) {
|
||||
where.weekDay = query.weekDay;
|
||||
}
|
||||
|
||||
return this.scheduleRepository.find(where, { orderBy: { weekDay: 'asc' } });
|
||||
}
|
||||
|
||||
async findTodaySchedules(slug: string): Promise<Schedule | null> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { slug });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const today = new Date();
|
||||
const weekDay = today.getDay(); // 0 = Sunday, 6 = Saturday
|
||||
|
||||
const where: FilterQuery<Schedule> = {
|
||||
restId: restaurant.id.toString(),
|
||||
weekDay,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
return this.scheduleRepository.findOne(where);
|
||||
}
|
||||
|
||||
async findScheduleBySlug(slug: string): Promise<Schedule[]> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { slug });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const where: FilterQuery<Schedule> = {
|
||||
restId: restaurant.id.toString(),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
return this.scheduleRepository.find(where);
|
||||
}
|
||||
|
||||
async findOne(id: string, restId: string): Promise<Schedule> {
|
||||
const schedule = await this.scheduleRepository.findOne({ id, restId });
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`برنامه با شناسه ${id} یافت نشد.`);
|
||||
}
|
||||
return schedule;
|
||||
}
|
||||
|
||||
async update(id: string, restId: string, updateScheduleDto: UpdateScheduleDto): Promise<Schedule> {
|
||||
const schedule = await this.scheduleRepository.findOne({ id, restId });
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`برنامه با شناسه ${id} یافت نشد.`);
|
||||
}
|
||||
|
||||
const updateData: Partial<Schedule> = {};
|
||||
if (updateScheduleDto.weekDay !== undefined) {
|
||||
updateData.weekDay = updateScheduleDto.weekDay;
|
||||
}
|
||||
if (updateScheduleDto.openTime !== undefined) {
|
||||
updateData.openTime = updateScheduleDto.openTime;
|
||||
}
|
||||
if (updateScheduleDto.closeTime !== undefined) {
|
||||
updateData.closeTime = updateScheduleDto.closeTime;
|
||||
}
|
||||
if (updateScheduleDto.isActive !== undefined) {
|
||||
updateData.isActive = updateScheduleDto.isActive;
|
||||
}
|
||||
|
||||
this.em.assign(schedule, updateData);
|
||||
await this.em.persistAndFlush(schedule);
|
||||
return schedule;
|
||||
}
|
||||
|
||||
async remove(id: string, restId: string): Promise<void> {
|
||||
const schedule = await this.scheduleRepository.findOne({ id, restId });
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`برنامه با شناسه ${id} یافت نشد.`);
|
||||
}
|
||||
await this.em.removeAndFlush(schedule);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
|
||||
type FindRestaurantsOpts = {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
isActive?: boolean;
|
||||
plan?: PlanEnum;
|
||||
orderBy?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RestRepository extends EntityRepository<Restaurant> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Restaurant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find restaurants with pagination and optional filters.
|
||||
* Supports: search (name/slug/domain/address), isActive, plan, ordering.
|
||||
*/
|
||||
async findAllPaginated(opts: FindRestaurantsOpts = {}): Promise<PaginatedResult<Restaurant>> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
search,
|
||||
isActive,
|
||||
plan,
|
||||
orderBy = 'createdAt',
|
||||
order = 'desc',
|
||||
} = opts;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: FilterQuery<Restaurant> = {};
|
||||
|
||||
if (typeof isActive === 'boolean') {
|
||||
where.isActive = isActive;
|
||||
}
|
||||
|
||||
if (plan) {
|
||||
where.plan = plan;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const pattern = `%${search}%`;
|
||||
where.$or = [
|
||||
{ name: { $ilike: pattern } },
|
||||
{ slug: { $ilike: pattern } },
|
||||
{ domain: { $ilike: pattern } },
|
||||
{ address: { $ilike: pattern } },
|
||||
];
|
||||
}
|
||||
|
||||
const [data, total] = await this.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Schedule } from '../entities/schedule.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleRepository extends EntityRepository<Schedule> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Schedule);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { RestaurantsService } from './providers/restaurants.service';
|
||||
import { RestaurantsController } from './controllers/restaurants.controller';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Restaurant } from './entities/restaurant.entity';
|
||||
import { RestRepository } from './repositories/rest.repository';
|
||||
import { ScheduleRepository } from './repositories/schedule.repository';
|
||||
import { Schedule } from './entities/schedule.entity';
|
||||
import { ScheduleService } from './providers/schedule.service';
|
||||
import { ScheduleController } from './controllers/schedule.controller';
|
||||
import { RestaurantCrone } from './crone/restaurant.crone';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
controllers: [RestaurantsController, ScheduleController],
|
||||
providers: [RestaurantsService, RestRepository, ScheduleRepository, ScheduleService, RestaurantCrone],
|
||||
imports: [MikroOrmModule.forFeature([Restaurant, Schedule]), JwtModule, forwardRef(() => AuthModule)],
|
||||
exports: [RestRepository, ScheduleRepository, ScheduleService],
|
||||
})
|
||||
export class RestaurantsModule {}
|
||||
@@ -1,27 +1,23 @@
|
||||
import type { EntityManager } from '@mikro-orm/core';
|
||||
import { Seeder } from '@mikro-orm/seeder';
|
||||
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
||||
import { RestaurantsSeeder } from './restaurants.seeder';
|
||||
import { FoodsSeeder } from './foods.seeder';
|
||||
import { AdminsSeeder } from './admins.seeder';
|
||||
|
||||
|
||||
export class DatabaseSeeder extends Seeder {
|
||||
async run(em: EntityManager) {
|
||||
const TOTAL_STEPS = 15;
|
||||
console.info('[Seeder] Starting database seed');
|
||||
// 1. Create Permissions
|
||||
|
||||
|
||||
|
||||
// 3. Create Restaurants
|
||||
console.info(`[Seeder] Step 3/${TOTAL_STEPS}: Creating restaurants`);
|
||||
const restaurantsSeeder = new RestaurantsSeeder();
|
||||
const restaurantsMap = await restaurantsSeeder.run(em);
|
||||
console.info(`[Seeder] Step 3/${TOTAL_STEPS}: Done`);
|
||||
// console.info(`[Seeder] Step 3/${TOTAL_STEPS}: Creating restaurants`);
|
||||
// const restaurantsSeeder = new RestaurantsSeeder();
|
||||
// const restaurantsMap = await restaurantsSeeder.run(em);
|
||||
// console.info(`[Seeder] Step 3/${TOTAL_STEPS}: Done`);
|
||||
|
||||
|
||||
|
||||
// 8. Create Categories
|
||||
|
||||
|
||||
|
||||
// 9. Create Foods
|
||||
// console.info(`[Seeder] Step 9/${TOTAL_STEPS}: Creating foods`);
|
||||
@@ -30,9 +26,9 @@ export class DatabaseSeeder extends Seeder {
|
||||
// console.info(`[Seeder] Step 9/${TOTAL_STEPS}: Done`);
|
||||
|
||||
// 10. Create Inventory for all Foods
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 12. Create Admins
|
||||
// console.info(`[Seeder] Step 12/${TOTAL_STEPS}: Creating admins`);
|
||||
@@ -40,8 +36,8 @@ export class DatabaseSeeder extends Seeder {
|
||||
// await adminsSeeder.run(em, restaurantsMap);
|
||||
// console.info(`[Seeder] Step 12/${TOTAL_STEPS}: Done`);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 15. Create Notifications for admins and users for all restaurants
|
||||
console.info(`[Seeder] Step 15/${TOTAL_STEPS}: (optional) creating notifications`);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { EntityManager } from '@mikro-orm/core';
|
||||
import { Admin } from '../modules/admin/entities/admin.entity';
|
||||
import { AdminRole } from '../modules/admin/entities/adminRole.entity';
|
||||
import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
||||
import { adminsData } from './data/admins.data';
|
||||
import { normalizePhone } from '../modules/utils/phone.util';
|
||||
|
||||
export class AdminsSeeder {
|
||||
async run(em: EntityManager, restaurantsMap: Map<string, Restaurant>): Promise<void> {
|
||||
for (const adminData of adminsData) {
|
||||
const existingAdmin = await em.findOne(Admin, { phone: adminData.phone });
|
||||
if (existingAdmin) continue;
|
||||
|
||||
|
||||
|
||||
const restaurant = adminData.restaurantSlug ? restaurantsMap.get(adminData.restaurantSlug) : null;
|
||||
|
||||
// For restaurant role, restaurant is required
|
||||
if (!restaurant) continue;
|
||||
|
||||
const admin = em.create(Admin, {
|
||||
phone: normalizePhone(adminData.phone),
|
||||
firstName: adminData.firstName,
|
||||
lastName: adminData.lastName,
|
||||
});
|
||||
em.persist(admin);
|
||||
|
||||
// Create AdminRole linking admin to role and restaurant
|
||||
const adminRole = em.create(AdminRole, {
|
||||
admin,
|
||||
restaurant: adminData.restaurantSlug ? restaurant : null,
|
||||
});
|
||||
em.persist(adminRole);
|
||||
admin.roles.add(adminRole);
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
export interface CategoryData {
|
||||
title: string;
|
||||
restaurantSlug: string;
|
||||
}
|
||||
|
||||
export const categoriesData: CategoryData[] = [ { title: 'پاستا', restaurantSlug: 'zhivan' },
|
||||
{ title: 'پیتزا ایتالیایی', restaurantSlug: 'zhivan' },
|
||||
{ title: 'پیتزا امریکایی', restaurantSlug: 'zhivan' },
|
||||
{ title: 'استیک', restaurantSlug: 'zhivan' },
|
||||
{ title: 'غذاهای گریل و فرنگی', restaurantSlug: 'zhivan' },
|
||||
{ title: 'ساندویچ و برگر', restaurantSlug: 'zhivan' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'zhivan' },
|
||||
{ title: 'سالاد', restaurantSlug: 'zhivan' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'zhivan' },
|
||||
{ title: 'نوشیدنی ها', restaurantSlug: 'zhivan' },
|
||||
{ title: 'قهوه ', restaurantSlug: 'zhivan' },
|
||||
{ title: 'موهیتو بار', restaurantSlug: 'zhivan' },
|
||||
{ title: 'اسموتی و شیک ها ', restaurantSlug: 'zhivan' },
|
||||
{ title: 'کیک و دسر', restaurantSlug: 'zhivan' },
|
||||
{ title: 'چای و دمنوش ', restaurantSlug: 'zhivan' },
|
||||
{ title: 'آیس کافی', restaurantSlug: 'zhivan' },
|
||||
{ title: 'ایرانی', restaurantSlug: 'zhivan' },
|
||||
{ title: 'شربت و عرقیجات', restaurantSlug: 'zhivan' },
|
||||
{ title: 'ماکتیل', restaurantSlug: 'zhivan' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'sepanta' },
|
||||
{ title: 'شیک ها', restaurantSlug: 'sepanta' },
|
||||
{ title: 'کیک ها', restaurantSlug: 'sepanta' },
|
||||
{ title: 'شربت ها ', restaurantSlug: 'sepanta' },
|
||||
{ title: 'دمنوش ها', restaurantSlug: 'sepanta' },
|
||||
{ title: 'صبحانه', restaurantSlug: 'ocafe' },
|
||||
{ title: 'سیب زمینی ها', restaurantSlug: 'ocafe' },
|
||||
{ title: 'پیش غذا و سالادها ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'غذاهای اصلی ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'کوکی و دسرها (آیتم ها به تعداد محدود در روز موجود میباشد )', restaurantSlug: 'ocafe' },
|
||||
{ title: 'موکتل های ایرانی و ملل ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'شیک ها ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'نوشیدنی های گرم بر پایه اسپرسو ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'نوشیدنی های سرد بر پایه اسپرسو ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'نوشیدنی های گرم ملل ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'چای های ایرانی ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'دمنوش ها ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'قهوه های شنی ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'کلد برو ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'متدهای موج سوم', restaurantSlug: 'ocafe' },
|
||||
{ title: 'آپشنال ', restaurantSlug: 'ocafe' },
|
||||
{ title: 'نوشیدنی', restaurantSlug: 'ocafe' },
|
||||
{ title: 'لقمه', restaurantSlug: 'ocafe' },
|
||||
{ title: 'خوراک ها', restaurantSlug: 'easydizy' },
|
||||
{ title: 'چاشنی ها', restaurantSlug: 'easydizy' },
|
||||
{ title: 'نوشیدنی های خنک', restaurantSlug: 'easydizy' },
|
||||
{ title: 'دسر ها', restaurantSlug: 'easydizy' },
|
||||
{ title: 'پیش غذا ', restaurantSlug: 'boote' },
|
||||
{ title: 'پیتزا آمریکایی', restaurantSlug: 'boote' },
|
||||
{ title: 'پیتزا ایتالیایی ', restaurantSlug: 'boote' },
|
||||
{ title: 'پاستا ', restaurantSlug: 'boote' },
|
||||
{ title: 'ناهار بوته', restaurantSlug: 'boote' },
|
||||
{ title: 'غذای اصلی', restaurantSlug: 'boote' },
|
||||
{ title: 'ساندویچ', restaurantSlug: 'boote' },
|
||||
{ title: 'اسپرسو بار', restaurantSlug: 'boote' },
|
||||
{ title: 'دمی بار', restaurantSlug: 'boote' },
|
||||
{ title: 'هاوس آف تی', restaurantSlug: 'boote' },
|
||||
{ title: 'امریکن استایل', restaurantSlug: 'boote' },
|
||||
{ title: 'کیک و دسر', restaurantSlug: 'boote' },
|
||||
{ title: 'آیس کافی', restaurantSlug: 'boote' },
|
||||
{ title: 'ماکتل بار', restaurantSlug: 'boote' },
|
||||
{ title: 'ژلاتو میلک شیک', restaurantSlug: 'boote' },
|
||||
{ title: 'صبحانه', restaurantSlug: 'boote' },
|
||||
{ title: 'اسموتی بار', restaurantSlug: 'boote' },
|
||||
{ title: 'چاکلت بار', restaurantSlug: 'boote' },
|
||||
{ title: 'نوشیدنی های گرم ', restaurantSlug: 'life' },
|
||||
{ title: 'نوشیدنی های سرد ', restaurantSlug: 'life' },
|
||||
{ title: 'صبحانه ', restaurantSlug: 'life' },
|
||||
{ title: 'میان وعده', restaurantSlug: 'life' },
|
||||
{ title: 'بستنی', restaurantSlug: 'life' },
|
||||
{ title: 'شیک', restaurantSlug: 'life' },
|
||||
{ title: 'قلیان', restaurantSlug: 'life' },
|
||||
{ title: 'پیتزا ایتالیایی ', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'پیتزا آمریکایی ', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'پاستاها', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'استیک ها', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'برگرها', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'ایرانی', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'سالاد.', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'پیش غذا.', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'نوشیدنی ها ', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'کیک و دسر', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'اسپرسو بار', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'آیس کافه', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'میلک شیک', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'نوشیدی های سرد', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'قهوه های دمی.', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'گلاسه ها', restaurantSlug: 'foodcourt' },
|
||||
{ title: 'صبحانه ', restaurantSlug: 'foodcourt' },
|
||||
{ title: ' پیش غذا', restaurantSlug: 'banicho' },
|
||||
{ title: 'کباب ها ', restaurantSlug: 'banicho' },
|
||||
{ title: 'دمنوش', restaurantSlug: 'banicho' },
|
||||
{ title: 'قهوه های دمی', restaurantSlug: 'banicho' },
|
||||
{ title: 'جگر گوسفندی', restaurantSlug: 'banicho' },
|
||||
{ title: 'جگر گوساله', restaurantSlug: 'banicho' },
|
||||
{ title: 'بارسرد ', restaurantSlug: 'banicho' },
|
||||
{ title: 'بارگرم', restaurantSlug: 'banicho' },
|
||||
{ title: 'نوشیدنی گرم بر پایه اسپرسو', restaurantSlug: 'blacksugar' },
|
||||
{ title: ' نوشیدنی گرم ', restaurantSlug: 'blacksugar' },
|
||||
{ title: 'قهوه سرد', restaurantSlug: 'blacksugar' },
|
||||
{ title: ' قهوه دمی', restaurantSlug: 'blacksugar' },
|
||||
{ title: ' میان وعده و دسر', restaurantSlug: 'blacksugar' },
|
||||
{ title: 'پیش غذاها', restaurantSlug: 'Piano' },
|
||||
{ title: 'ساندویچ و برگر', restaurantSlug: 'Piano' },
|
||||
{ title: 'پیتزا امریکایی', restaurantSlug: 'Piano' },
|
||||
{ title: 'پیتزا ایتالیایی', restaurantSlug: 'Piano' },
|
||||
{ title: 'پاستا', restaurantSlug: 'Piano' },
|
||||
{ title: 'استیک', restaurantSlug: 'Piano' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'Piano' },
|
||||
{ title: 'سالاد', restaurantSlug: 'Piano' },
|
||||
{ title: 'بشقاب', restaurantSlug: 'Piano' },
|
||||
{ title: 'پیده', restaurantSlug: 'Piano' },
|
||||
{ title: 'خوراک', restaurantSlug: 'Piano' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'Piano' },
|
||||
{ title: 'نوشیدنی سرد', restaurantSlug: 'Piano' },
|
||||
{ title: 'دسر و کیک', restaurantSlug: 'Piano' },
|
||||
{ title: 'بستنی', restaurantSlug: 'Piano' },
|
||||
{ title: 'دمنوش گیاهی', restaurantSlug: 'Piano' },
|
||||
{ title: 'اسموتی', restaurantSlug: 'Piano' },
|
||||
{ title: 'شیک', restaurantSlug: 'Piano' },
|
||||
{ title: 'ماکتل ها', restaurantSlug: 'Piano' },
|
||||
{ title: 'شربت ها', restaurantSlug: 'Piano' },
|
||||
{ title: 'غذا', restaurantSlug: 'saadat' },
|
||||
{ title: 'فرنگی', restaurantSlug: 'saadat' },
|
||||
{ title: 'سالاد ها', restaurantSlug: 'saadat' },
|
||||
{ title: 'نوشیدنی', restaurantSlug: 'saadat' },
|
||||
{ title: 'قلیان', restaurantSlug: 'saadat' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'saadat' },
|
||||
{ title: 'آیس کافی', restaurantSlug: 'saadat' },
|
||||
{ title: 'چای', restaurantSlug: 'saadat' },
|
||||
{ title: 'قهوه نسل ۳', restaurantSlug: 'saadat' },
|
||||
{ title: 'ماکتل ها', restaurantSlug: 'saadat' },
|
||||
{ title: 'شیک ها', restaurantSlug: 'saadat' },
|
||||
{ title: 'پیش غذاها', restaurantSlug: 'saadat' },
|
||||
{ title: 'خوراک مخصوص هفت خوان', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: 'خوراک', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: 'نوشیدنی', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: 'چای و نوشیدنی گرم', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: 'قلیان', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: 'دسر', restaurantSlug: 'Haaft khaan' },
|
||||
{ title: ' نوشیدنی های سرد', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'پیتزا ', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'قهوه و نوشیدنی گرم', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'پیش غذا و سالاد ', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'کیک', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'قهوه های دمی ', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'پاستا', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'برگر', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'قلیان', restaurantSlug: '1972Cafe' },
|
||||
{ title: 'پیتزاها', restaurantSlug: 'lounge1' },
|
||||
{ title: 'پیتزا', restaurantSlug: 'Honar' },
|
||||
{ title: 'برگر', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'پاستا', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'پنینی ', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'هات داگ ', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'سیب زمینی', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'سالاد', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'نوشیدنی', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'اسپرسو بار', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'دمی', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: ' ایس کافی', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'چای دمنوش', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'کیک', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'شیک', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'نوشیدنی سرد', restaurantSlug: 'cafe_sepanj' },
|
||||
{ title: 'سیروپ', restaurantSlug: 'monikh' },
|
||||
{ title: 'کیک', restaurantSlug: 'monikh' },
|
||||
{ title: 'ساندویچ سرد', restaurantSlug: 'monikh' },
|
||||
{ title: 'بر پایه اسپرسو', restaurantSlug: 'monikh' },
|
||||
{ title: 'ماچا', restaurantSlug: 'monikh' },
|
||||
{ title: 'بدون اسپرسو', restaurantSlug: 'monikh' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'monikh' },
|
||||
{ title: 'شیک و اسموتی', restaurantSlug: 'monikh' },
|
||||
{ title: 'نوشیدنی های گرم (بر پایه اسپرسو)', restaurantSlug: 'o-cafe' },
|
||||
{ title: 'نوشیدنی های سرد ', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' نوشیدنی های گرم ملل', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' قهوه های شنی', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' شیک ها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' کوکی ها و دسرها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' کلد برو', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' موج سوم', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' چای های ایرانی', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' دمنوش ها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' ماکتل ها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' غذاهای اصلی', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' پیش غذاها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' سیب زمینی ها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' صبحانه ها', restaurantSlug: 'o-cafe' },
|
||||
{ title: ' آپشنال', restaurantSlug: 'o-cafe' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'برگرها', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'دبل برگرها', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'تریپل برگرها', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'پیتزا ها', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'نوشیدنی ها', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'ساندویچ', restaurantSlug: 'yummyburger' },
|
||||
{ title: 'سالاد و پیش غذا', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'آش بار مهتاب', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'غذای اصلی', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'شربت بار مهتاب', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'برگر', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'کافی بار', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'نوشیدنی گرم شکلاتی', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'دسر و کیک', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'شیک', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'دمنوش بار', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'ماکتل بار', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'اسموتی', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'نوشیدنی سرد', restaurantSlug: 'mahtabgarden' },
|
||||
{ title: 'نوشیدنی های بر پایه اسپرسو', restaurantSlug: 'dream' },
|
||||
{ title: 'قهوه های دمی ', restaurantSlug: 'dream' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'dream' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'dream' },
|
||||
{ title: 'قهوه های سرد', restaurantSlug: 'dream' },
|
||||
{ title: 'شیک ها', restaurantSlug: 'dream' },
|
||||
{ title: 'موکتل و آب میوه ها', restaurantSlug: 'dream' },
|
||||
{ title: 'شربت ها', restaurantSlug: 'dream' },
|
||||
{ title: 'فانتزی', restaurantSlug: 'dream' },
|
||||
{ title: 'افزودنی', restaurantSlug: 'dream' },
|
||||
{ title: 'بستنی ', restaurantSlug: 'dream' },
|
||||
{ title: 'دسر', restaurantSlug: 'dream' },
|
||||
{ title: 'صبحانه', restaurantSlug: 'dream' },
|
||||
{ title: 'پیتزا ایتالیایی', restaurantSlug: 'dream' },
|
||||
{ title: 'برگر', restaurantSlug: 'dream' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'dream' },
|
||||
{ title: 'پاستا', restaurantSlug: 'dream' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'dream' },
|
||||
{ title: 'خوراک', restaurantSlug: 'dream' },
|
||||
{ title: 'پیده', restaurantSlug: 'dream' },
|
||||
{ title: 'شیرینی', restaurantSlug: 'dream' },
|
||||
{ title: 'استیک ها', restaurantSlug: 'dream' },
|
||||
{ title: 'سالاد', restaurantSlug: 'dream' },
|
||||
{ title: 'پیتزا آمریکایی', restaurantSlug: 'dream' },
|
||||
{ title: 'غذای ایرانی', restaurantSlug: 'dream' },
|
||||
{ title: 'باربیکیو', restaurantSlug: 'dream' },
|
||||
{ title: 'پیش غذاها', restaurantSlug: 'lanka' },
|
||||
{ title: 'ایران زمین', restaurantSlug: 'lanka' },
|
||||
{ title: 'پیتزا ایتالیایی', restaurantSlug: 'lanka' },
|
||||
{ title: 'برگرها', restaurantSlug: 'lanka' },
|
||||
{ title: 'استیک ها', restaurantSlug: 'lanka' },
|
||||
{ title: 'پاستا', restaurantSlug: 'lanka' },
|
||||
{ title: 'سالاد', restaurantSlug: 'lanka' },
|
||||
{ title: 'دریایی ها', restaurantSlug: 'lanka' },
|
||||
{ title: 'شات سس ها', restaurantSlug: 'lanka' },
|
||||
{ title: 'اسپرسو بار', restaurantSlug: 'lanka' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'lanka' },
|
||||
{ title: 'آیس کافی', restaurantSlug: 'lanka' },
|
||||
{ title: 'میلک شیک', restaurantSlug: 'lanka' },
|
||||
{ title: 'نوشیدنی سرد', restaurantSlug: 'lanka' },
|
||||
{ title: 'کیک و دسر ', restaurantSlug: 'lanka' },
|
||||
{ title: 'سوخاری ها', restaurantSlug: 'lanka' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'lanka' },
|
||||
{ title: 'قهوه دمی', restaurantSlug: 'lanka' },
|
||||
{ title: 'اسموتی ', restaurantSlug: 'lanka' },
|
||||
{ title: 'vip drink', restaurantSlug: 'lanka' },
|
||||
{ title: 'اسپرسو', restaurantSlug: 'LIAN' },
|
||||
{ title: 'اسپرسو با شیر', restaurantSlug: 'LIAN' },
|
||||
{ title: 'بار گرم', restaurantSlug: 'LIAN' },
|
||||
{ title: 'آیس کافی', restaurantSlug: 'LIAN' },
|
||||
{ title: 'آیس', restaurantSlug: 'LIAN' },
|
||||
{ title: 'ماکتیل', restaurantSlug: 'LIAN' },
|
||||
{ title: 'شیک', restaurantSlug: 'LIAN' },
|
||||
{ title: 'دمی', restaurantSlug: 'LIAN' },
|
||||
{ title: 'اسموتی', restaurantSlug: 'LIAN' },
|
||||
{ title: 'میان وعده', restaurantSlug: 'LIAN' },
|
||||
{ title: 'کیک ها', restaurantSlug: 'LIAN' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'LIAN' },
|
||||
{ title: 'بار گرم', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'چای ها', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'دمنوش ها', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'شیک ها', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'بارسرد', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'برگرها ', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'هات داگ ها', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'نوستالژی', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'سیب زمینی', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'پاستا', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'سالاد ', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'اسپرسو بار', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'نوشیدنی سرد بر پایه قهوه', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'ساندویچ ', restaurantSlug: 'milco-pilco' },
|
||||
{ title: 'نوشیدنی های گرم بر پایه قهوه', restaurantSlug: 'lama' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'lama' },
|
||||
{ title: 'دمنوش ها', restaurantSlug: 'lama' },
|
||||
{ title: 'نوشیدنی های سر بر پایه قهوه', restaurantSlug: 'lama' },
|
||||
{ title: 'ماکتیل', restaurantSlug: 'lama' },
|
||||
{ title: 'چای', restaurantSlug: 'lama' },
|
||||
{ title: 'شیک', restaurantSlug: 'lama' },
|
||||
{ title: 'پیتزا', restaurantSlug: 'lama' },
|
||||
{ title: 'برگر', restaurantSlug: 'lama' },
|
||||
{ title: 'ساندویچ', restaurantSlug: 'lama' },
|
||||
{ title: 'ساندویچ نوستالژی', restaurantSlug: 'lama' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'lama' },
|
||||
{ title: 'سیب زمینی', restaurantSlug: 'lama' },
|
||||
{ title: 'پنه', restaurantSlug: 'lama' },
|
||||
{ title: 'غذای ایرانی', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'پیتزا آمریکایی', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'پیتزا ایتالیایی', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'برگر', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'خوراک', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'پیده', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'سوخاری', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'استیک', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'پاستا', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'سالاد', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'نوشیدنی های بر پایه اسپرسو', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'قهوه های دمی', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'قهوه های سرد', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'چای و دمنوش ', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'موکتل و آب میوه ها', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'دسر', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'شیک ', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'بستنی', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'شربت', restaurantSlug: 'kaj.foodhall' },
|
||||
{ title: 'دسر', restaurantSlug: 'hamid' },
|
||||
{ title: 'شیک', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'بستنی ویژه', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دسر', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'اسموتی', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'آیس کافی', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'قهوه', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'نوشیدنی های گرم', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دمنوش', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'آیس بستنی', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'ویتامینه', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'آبمیوه طبیعی', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات اسپشیال ', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات آرت', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات فانتزی', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات جنرال', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'رست نات', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'برلینر', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات اسپال', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات لقمه ای ', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات لقمه ای', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات ناتس', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'صبحانه', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'اسنک و میان وعده', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'دونات دوبی ', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'منوی اقتصادی ', restaurantSlug: 'donat-factory' },
|
||||
{ title: 'پیتزا آمریکایی', restaurantSlug: 'narsis' },
|
||||
{ title: 'برگرها', restaurantSlug: 'narsis' },
|
||||
{ title: 'پاستاها', restaurantSlug: 'narsis' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'narsis' },
|
||||
{ title: 'نوشیدنی سرد آماده', restaurantSlug: 'narsis' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'narsis' },
|
||||
{ title: 'قهوه', restaurantSlug: 'narsis' },
|
||||
{ title: 'نوشیدنی های سرد', restaurantSlug: 'narsis' },
|
||||
{ title: 'نوشیدنی های سرد بر پایه قهوه', restaurantSlug: 'narsis' },
|
||||
{ title: 'نوشیدنی های طبیعی با بستنی', restaurantSlug: 'narsis' },
|
||||
{ title: 'ﻧﻮﺷﯿﺪﻧﯽ ﻫﺎ ﺑﺮ ﭘﺎﯾﻪ ﺷﯿﺮ', restaurantSlug: 'narsis' },
|
||||
{ title: 'شیک ﻫﺎ و آﯾﺲ پک ها', restaurantSlug: 'narsis' },
|
||||
{ title: 'آﺑﻤﯿﻮه ﻫﺎی ﻃﺒﯿﻌﯽ', restaurantSlug: 'narsis' },
|
||||
{ title: 'آﺑﻤﯿﻮه ﻫﺎی ترکیبی', restaurantSlug: 'narsis' },
|
||||
{ title: 'افزودنی ها', restaurantSlug: 'narsis' },
|
||||
{ title: 'کیک ها ', restaurantSlug: 'narsis' },
|
||||
{ title: 'خوراک ها', restaurantSlug: 'narsis' },
|
||||
{ title: 'غذاهای چلویی', restaurantSlug: 'narsis' },
|
||||
{ title: 'قهوه گرم', restaurantSlug: 'Theory' },
|
||||
{ title: 'قهوه سرد', restaurantSlug: 'Theory' },
|
||||
{ title: 'قهوه دمی', restaurantSlug: 'Theory' },
|
||||
{ title: 'ماکتل', restaurantSlug: 'Theory' },
|
||||
{ title: 'اسموتی', restaurantSlug: 'Theory' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'Theory' },
|
||||
{ title: 'ساندویچ گرم', restaurantSlug: 'Theory' },
|
||||
{ title: 'ساندویچ سرد', restaurantSlug: 'Theory' },
|
||||
{ title: 'سالاد و پیش غذا', restaurantSlug: 'Theory' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'Theory' },
|
||||
{ title: 'دسر', restaurantSlug: 'Theory' },
|
||||
{ title: 'صبحانه', restaurantSlug: 'Theory' },
|
||||
{ title: 'شیک', restaurantSlug: 'Theory' },
|
||||
{ title: 'اضافه بار', restaurantSlug: 'Theory' },
|
||||
{ title: 'نوشیدنی گرم بر پایه اسپرسو', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'نوشیدنی سرد بر پایه اسپرسو', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'دمنوش و چای', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'شیک', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'ماکتل', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'اسموتی', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'کیک و دسر', restaurantSlug: 'ahr_cofe' },
|
||||
{ title: 'اسپرسو بار', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'بارسرد بر پایه اسپرسو', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'صبحانه', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'دمنوش ها', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'بارسرد', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'شیک', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'غذا اصلی', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'بار گرم بدون کافئین ', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'کیک', restaurantSlug: 'cafe_meat' },
|
||||
{ title: 'قهوه ها', restaurantSlug: 'Moon' },
|
||||
{ title: 'پیستری', restaurantSlug: 'Moon' },
|
||||
{ title: 'چای و دمنوش', restaurantSlug: 'Moon' },
|
||||
{ title: 'نوشیدنی سرد', restaurantSlug: 'Moon' },
|
||||
{ title: 'ماکتل', restaurantSlug: 'Moon' },
|
||||
{ title: 'بستنی', restaurantSlug: 'Moon' },
|
||||
{ title: 'نوشیدنی گرم', restaurantSlug: 'Moon' },
|
||||
{ title: 'ایتم های بر پایه شیر', restaurantSlug: 'Moon' },
|
||||
{ title: 'بیکری', restaurantSlug: 'Moon' },
|
||||
{ title: 'سانویچ سرد', restaurantSlug: 'Moon' },
|
||||
{ title: 'سالاد', restaurantSlug: 'Moon' },
|
||||
{ title: 'اب', restaurantSlug: 'Moon' },
|
||||
{ title: 'پیتزا', restaurantSlug: 'Moon' },
|
||||
{ title: 'پاستاها', restaurantSlug: 'passata' },
|
||||
{ title: 'Menu star (Only for special days)', restaurantSlug: 'passata' },
|
||||
{ title: 'تاپینگ بار', restaurantSlug: 'passata' },
|
||||
{ title: 'چیاباتا(سندویچ)', restaurantSlug: 'passata' },
|
||||
{ title: 'نوشیدنی', restaurantSlug: 'passata' },
|
||||
{ title: 'آش ها', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'غذاهای اصلی', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'غذاهای ویژه در طول هفته', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'صبحانه ', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'نوشیدنی ها', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'پیش غذا', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'دسرها', restaurantSlug: 'kolbe-setareh' },
|
||||
{ title: 'سرویس اضافه', restaurantSlug: 'kolbe-setareh' },
|
||||
];
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
import { Permission, PermissionTitles } from '../../common/enums/permission.enum';
|
||||
|
||||
|
||||
|
||||
export const getPermissionsData = () => {
|
||||
return Object.values(Permission).map(name => ({
|
||||
name,
|
||||
title: PermissionTitles[name],
|
||||
}));
|
||||
};
|
||||
@@ -1,919 +0,0 @@
|
||||
import { PlanEnum } from 'src/modules/restaurants/interface/plan.interface';
|
||||
|
||||
export interface RestaurantData {
|
||||
name: string;
|
||||
slug: string;
|
||||
domain: string;
|
||||
isActive: boolean;
|
||||
phone: string;
|
||||
logo?: string;
|
||||
score: {
|
||||
purchaseAmount: string;
|
||||
purchaseScore: string;
|
||||
scoreAmount: string;
|
||||
scoreCredit: string;
|
||||
birthdayScore: string;
|
||||
registerScore: string;
|
||||
marriageDateScore: string;
|
||||
referrerScore: string;
|
||||
};
|
||||
plan: PlanEnum;
|
||||
subscriptionId: string;
|
||||
}
|
||||
|
||||
export const restaurantsData: RestaurantData[] = [
|
||||
{
|
||||
name: 'ژیوان',
|
||||
slug: 'zhivan',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/zhivan',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1638870352717_61a3661f37a0d33354a6d210.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_zhivan_001',
|
||||
},
|
||||
{
|
||||
name: 'سپنتا',
|
||||
slug: 'sepanta',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/sepanta',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1641199914860_61d2b72b2e8bd97126a70b5f.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_sepanta_001',
|
||||
},
|
||||
{
|
||||
name: 'اکافه',
|
||||
slug: 'ocafe',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/ocafe',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1643274757715_61e509141601c7c7d9141989.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_ocafe_001',
|
||||
},
|
||||
{
|
||||
name: 'رستوران ایزی دیزی',
|
||||
slug: 'easydizy',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/easydizy',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1644324825827_6202562b1ee4b0270db4ae58.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_easydizy_001',
|
||||
},
|
||||
{
|
||||
name: 'کافه رستوران بوته',
|
||||
slug: 'boote',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/boote',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1644655737806_62076fa71ee4b0270db4c32d.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Premium,
|
||||
subscriptionId: 'sub_seed_boote_001',
|
||||
},
|
||||
{
|
||||
name: 'کافه لایف',
|
||||
slug: 'life',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/life',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1644838482386_6207b1211ee4b0270db4c4ae.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_life_001',
|
||||
},
|
||||
{
|
||||
name: 'ninja park',
|
||||
slug: 'foodcourt',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/foodcourt',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1725286063770_6210b063adf126141b26173d.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_foodcourt_001',
|
||||
},
|
||||
{
|
||||
name: 'هیزم',
|
||||
slug: 'banicho',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/banicho',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1699722686770_622dccbda03b80e0a44f1d3a.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_banicho_001',
|
||||
},
|
||||
{
|
||||
name: 'بلک شوگر',
|
||||
slug: 'blacksugar',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/blacksugar',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1647809564910_62378f6bc1bb048b321cd949.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_blacksugar_001',
|
||||
},
|
||||
{
|
||||
name: 'پیانو',
|
||||
slug: 'Piano',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Piano',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1652476386948_627e3d972353df2fe6512c8f.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Piano_001',
|
||||
},
|
||||
{
|
||||
name: 'سعادت',
|
||||
slug: 'saadat',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/saadat',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1655296406771_62a9cff7128ed6fd013332dc.png',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_saadat_001',
|
||||
},
|
||||
{
|
||||
name: 'هفت خوان',
|
||||
slug: 'Haftkhan',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Haftkhan',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Haftkhan_001',
|
||||
},
|
||||
{
|
||||
name: 'سفره خانه و باغ رستوران هفت خوان',
|
||||
slug: 'Haaft khaan',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1657711159036_62cea8d47400250986c70a07.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Haaft khaan',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Haaft khaan_001',
|
||||
},
|
||||
{
|
||||
name: '1972',
|
||||
slug: '1972Cafe',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1658666648415_62dd3bf3faacb066e1206279.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/1972Cafe',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_1972Cafe_001',
|
||||
},
|
||||
{
|
||||
name: 'لانژ',
|
||||
slug: 'lounge1',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1659109491707_62e3ff5e96be484852cba3c3.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/lounge1',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_lounge1_001',
|
||||
},
|
||||
{
|
||||
name: 'کافه رستوران هنر',
|
||||
slug: 'Honar',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1662296508840_63131c3d96be484852d0a9ed.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Honar',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Honar_001',
|
||||
},
|
||||
{
|
||||
name: 'خانه وانیلی',
|
||||
slug: 'Vanilla',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1662977478952_631f0484e410c5322752c409.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Vanilla',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Vanilla_001',
|
||||
},
|
||||
{
|
||||
name: '1972',
|
||||
slug: '1972',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1672070842973_63a9c15f82d7fc8d726b8b2f.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/1972',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_1972_001',
|
||||
},
|
||||
{
|
||||
name: 'نارسیس',
|
||||
slug: 'Narcis',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Narcis',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Narcis_001',
|
||||
},
|
||||
{
|
||||
name: 'کافه سپنج',
|
||||
slug: 'cafe_sepanj',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1683113201780_6450aa9a98ff3c7414414230.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/cafe_sepanj',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_cafe_sepanj_001',
|
||||
},
|
||||
{
|
||||
name: 'مونیخ',
|
||||
slug: 'monikh',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1737820250944_6450d97a98ff3c7414414999.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/monikh',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_monikh_001',
|
||||
},
|
||||
{
|
||||
name: 'اُ کافه',
|
||||
slug: 'o-cafe',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1684406222541_6465ff4d98ff3c741442d56a.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/o-cafe',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_o-cafe_001',
|
||||
},
|
||||
{
|
||||
name: 'یامی برگر',
|
||||
slug: 'yummyburger',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1689594902563_64b3d17d98ff3c741448cccd.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/yummyburger',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_yummyburger_001',
|
||||
},
|
||||
{
|
||||
name: 'کافه رستوران مهتاب',
|
||||
slug: 'mahtabgarden',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1689965802684_64bad3b698ff3c741449521e.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/mahtabgarden',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_mahtabgarden_001',
|
||||
},
|
||||
{
|
||||
name: 'dream',
|
||||
slug: 'dream',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1692001432499_64d9e2b979f54f6403d7e0be.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/dream',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_dream_001',
|
||||
},
|
||||
{
|
||||
name: 'لانکا',
|
||||
slug: 'lanka',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1695032652234_650823be59383e8d7550ccb5.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/lanka',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_lanka_001',
|
||||
},
|
||||
{
|
||||
name: '123',
|
||||
slug: '123',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1696240982921_651a7ffe59383e8d7551eebf.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/123',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_123_001',
|
||||
},
|
||||
{
|
||||
name: 'لیان',
|
||||
slug: 'LIAN',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1701608905866_651ad03159383e8d7551f3be.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/LIAN',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_LIAN_001',
|
||||
},
|
||||
{
|
||||
name: 'کافه رستوران لانکا',
|
||||
slug: 'Lanka',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Lanka',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Lanka_001',
|
||||
},
|
||||
{
|
||||
name: 'میلکو پلاس',
|
||||
slug: 'milco-pilco',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1710166978715_65ef0b1d3ad722005756bb0f.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/milco-pilco',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_milco-pilco_001',
|
||||
},
|
||||
{
|
||||
name: 'لاما',
|
||||
slug: 'lama',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1710333176537_65ef1a093ad722005756bc8e.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/lama',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_lama_001',
|
||||
},
|
||||
{
|
||||
name: 'کاج',
|
||||
slug: 'kaj.foodhall',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1720963586174_6693d0cc136cc10070469510.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/kaj.foodhall',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_kaj.foodhall_001',
|
||||
},
|
||||
{
|
||||
name: 'حمید',
|
||||
slug: 'hamid',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1729929522774_671c9c58c3c0100063e4d1a9.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/hamid',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_hamid_001',
|
||||
},
|
||||
{
|
||||
name: 'دونات فکتوری',
|
||||
slug: 'donat-factory',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1731152090639_672f3c9bc3c0100063e6a526.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/donat-factory',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_donat-factory_001',
|
||||
},
|
||||
{
|
||||
name: 'نارسیس',
|
||||
slug: 'narsis',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1731314427492_6731c2b9c3c0100063e6e2ae.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/narsis',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_narsis_001',
|
||||
},
|
||||
{
|
||||
name: 'تئوری',
|
||||
slug: 'Theory',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1736665730887_677ab9f9a4463c0057e3ac5d.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Theory',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Theory_001',
|
||||
},
|
||||
{
|
||||
name: 'testmenu',
|
||||
slug: 'testmenu',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/testmenu',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_testmenu_001',
|
||||
},
|
||||
{
|
||||
name: 'ahr_cafe',
|
||||
slug: 'ahr_cofe',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1738074249830_6798e6aabd2ce20057144cfb.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/ahr_cofe',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_ahr_cofe_001',
|
||||
},
|
||||
{
|
||||
name: 'cafe_meat',
|
||||
slug: 'cafe_meat',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1739868881926_679b7fe7bd2ce200571498a8.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/cafe_meat',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_cafe_meat_001',
|
||||
},
|
||||
{
|
||||
name: 'پیتزا خانواده',
|
||||
slug: 'KFP',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1740470240551_67bd773f536282006220de43.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/KFP',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_KFP_001',
|
||||
},
|
||||
{
|
||||
name: 'پیتزا خانواده ',
|
||||
slug: 'KFP ',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/KFP ',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_KFP _001',
|
||||
},
|
||||
{
|
||||
name: 'Themoon',
|
||||
slug: 'Moon',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1743347876393_67e93f9e536282006224c40d.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/Moon',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_Moon_001',
|
||||
},
|
||||
{
|
||||
name: 'زپارتی',
|
||||
slug: 'zparty',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/zparty',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_zparty_001',
|
||||
},
|
||||
{
|
||||
name: 'پاساتا پلاس',
|
||||
slug: 'passata',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1749534426675_6847c5f3f114460057d617bf.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/passata',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_passata_001',
|
||||
},
|
||||
{
|
||||
name: 'کلبه ستاره',
|
||||
slug: 'kolbe-setareh',
|
||||
logo: 'https://dmenu.danakcorp.com/uploads/images/store/store_1751196871890_686100dd9086300011bdf7d0.png',
|
||||
domain: 'https://dmenu-plus-front.dev.danakcorp.com/kolbe-setareh',
|
||||
isActive: true,
|
||||
phone: '',
|
||||
score: {
|
||||
purchaseAmount: '100000',
|
||||
purchaseScore: '0',
|
||||
scoreAmount: '0',
|
||||
scoreCredit: '0',
|
||||
birthdayScore: '0',
|
||||
registerScore: '0',
|
||||
marriageDateScore: '0',
|
||||
referrerScore: '0',
|
||||
},
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_kolbe-setareh_001',
|
||||
},
|
||||
];
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { EntityManager } from '@mikro-orm/core';
|
||||
import { Food } from '../modules/foods/entities/food.entity';
|
||||
import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
||||
import type { Category } from '../modules/foods/entities/category.entity';
|
||||
import { MealType } from '../modules/foods/interface/food.interface';
|
||||
import { foodsData } from './data/foods.data';
|
||||
|
||||
export class FoodsSeeder {
|
||||
async run(
|
||||
em: EntityManager,
|
||||
restaurantsMap: Map<string, Restaurant>,
|
||||
categoriesMap: Map<string, Category>,
|
||||
): Promise<void> {
|
||||
for (const foodData of foodsData) {
|
||||
const restaurant = restaurantsMap.get(foodData.restaurantSlug);
|
||||
const key = `${foodData.restaurantSlug}-${foodData.categoryTitle}`;
|
||||
|
||||
const normalize = (s: string) => (s || '').replace(/\s+/g, ' ').trim();
|
||||
|
||||
let category = categoriesMap.get(key);
|
||||
|
||||
// Fallback: try to find category by normalized title if exact key not found
|
||||
if (!category) {
|
||||
for (const [k, v] of categoriesMap.entries()) {
|
||||
const parts = k.split('-');
|
||||
const kSlug = parts[0];
|
||||
const kTitle = parts.slice(1).join('-');
|
||||
if (kSlug === foodData.restaurantSlug && normalize(kTitle) === normalize(foodData.categoryTitle)) {
|
||||
category = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!restaurant || !category) {
|
||||
console.warn(
|
||||
`Skipping food \"${foodData.title}\" — missing ${!restaurant ? 'restaurant' : 'category'} for key ${key}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await em.findOne(Food, {
|
||||
title: foodData.title,
|
||||
restaurant,
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const food = em.create(Food, {
|
||||
title: foodData.title,
|
||||
desc: foodData.desc,
|
||||
content: foodData.content,
|
||||
price: foodData.price,
|
||||
restaurant,
|
||||
category,
|
||||
isActive: foodData.isActive,
|
||||
// new fields on Food entity
|
||||
weekDays: foodData.weekDays ?? [0, 1, 2, 3, 4, 5, 6],
|
||||
mealTypes: foodData.mealTypes ?? [MealType.BREAKFAST, MealType.LUNCH, MealType.DINNER, MealType.SNACK],
|
||||
discount: foodData.discount ?? 0,
|
||||
score: foodData.score ?? 0,
|
||||
inPlaceServe: true,
|
||||
pickupServe: true,
|
||||
images: foodData.images,
|
||||
isSpecialOffer: foodData.isSpecialOffer ?? false,
|
||||
});
|
||||
|
||||
em.persist(food);
|
||||
}
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { EntityManager } from '@mikro-orm/core';
|
||||
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
||||
import { restaurantsData } from './data/restaurants.data';
|
||||
|
||||
export class RestaurantsSeeder {
|
||||
async run(em: EntityManager): Promise<Map<string, Restaurant>> {
|
||||
const restaurantsMap = new Map<string, Restaurant>();
|
||||
|
||||
for (const restaurantData of restaurantsData) {
|
||||
let restaurant = await em.findOne(Restaurant, { slug: restaurantData.slug });
|
||||
if (!restaurant) {
|
||||
restaurant = em.create(Restaurant, {
|
||||
...restaurantData,
|
||||
subscriptionStartDate: new Date('2026-01-01'),
|
||||
subscriptionEndDate: new Date('2026-01-07'),
|
||||
isActive: true,
|
||||
});
|
||||
em.persist(restaurant);
|
||||
}
|
||||
restaurantsMap.set(restaurantData.slug, restaurant);
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
return restaurantsMap;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user