diff --git a/src/app.module.ts b/src/app.module.ts index c052616..6808bc1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,7 +2,6 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Module } from '@nestjs/common'; import dataBaseConfig from './config/mikro-orm.config'; import { ConfigModule } from '@nestjs/config'; -import { UserModule } from './modules/users/user.module'; import { UtilsModule } from './modules/utils/utils.module'; import { AuthModule } from './modules/auth/auth.module'; import { UploaderModule } from './modules/uploader/uploader.module'; @@ -24,7 +23,6 @@ import { CatalogueModule } from './modules/catalogue/catalogue.module'; ConfigModule.forRoot({ isGlobal: true, cache: true }), CacheModule.registerAsync(cacheConfig()), MikroOrmModule.forRootAsync(dataBaseConfig), - UserModule, UtilsModule, AuthModule, UploaderModule, diff --git a/src/core/interceptors/response.interceptor.ts b/src/core/interceptors/response.interceptor.ts index 1ae8154..cbeabdb 100755 --- a/src/core/interceptors/response.interceptor.ts +++ b/src/core/interceptors/response.interceptor.ts @@ -21,11 +21,9 @@ export class ResponseInterceptor implements NestInterceptor { return next.handle().pipe( map(data => { - console.log('data',data) - // If data is already wrapped or doesn't need wrapping, return as-is + // If data is already wrapped or doesn't need wrapping, return as-is if (data && typeof data === 'object' && 'data' in data && 'success' in data) { - console.log('data', data) - + return { data: data, ...('nextCursor' in (data as any) && (data as any).nextCursor != null diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index ebb6d27..65cf666 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -2,23 +2,20 @@ import { Module, forwardRef } from '@nestjs/common'; import { AuthService } from './services/auth.service'; import { AuthController } from './controllers/auth.controller'; import { UtilsModule } from '../utils/utils.module'; -import { UserModule } from '../users/user.module'; -import { JwtModule } from '@nestjs/jwt'; + import { JwtModule } from '@nestjs/jwt'; 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 { User } from '../users/entities/user.entity'; -import { AdminAuthGuard } from './guards/adminAuth.guard'; + import { AdminAuthGuard } from './guards/adminAuth.guard'; import { RefreshToken } from './entities/refresh-token.entity'; import { NotificationsModule } from '../notifications/notifications.module'; @Module({ imports: [ UtilsModule, - forwardRef(() => UserModule), - JwtModule.registerAsync({ + JwtModule.registerAsync({ useFactory: (configService: ConfigService) => { const expiresIn = configService.getOrThrow('JWT_EXPIRATION_TIME'); return { @@ -34,7 +31,7 @@ import { NotificationsModule } from '../notifications/notifications.module'; }), forwardRef(() => AdminModule), forwardRef(() => RestaurantsModule), - MikroOrmModule.forFeature([User, RefreshToken]), + MikroOrmModule.forFeature([RefreshToken]), forwardRef(() => NotificationsModule), ], diff --git a/src/modules/auth/controllers/auth.controller.ts b/src/modules/auth/controllers/auth.controller.ts index 6a4b7b9..5de8439 100644 --- a/src/modules/auth/controllers/auth.controller.ts +++ b/src/modules/auth/controllers/auth.controller.ts @@ -22,13 +22,7 @@ export class AuthController { return this.authService.requestOtp(dto, false); } - @Post('public/auth/otp/verify') - @ApiOperation({ summary: 'Verify OTP code' }) - @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' }) - otpVerify(@Body() dto: VerifyOtpDto) { - return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property - } - + @RefreshTokenRateLimit() @ApiOperation({ summary: 'refresh the user access token / refresh token' }) @Post('public/auth/refresh') diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index fb18bfa..ca9e649 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -2,15 +2,13 @@ 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 { UserService } from '../../users/providers/user.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 { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { AdminLoginTransformer } from '../transformers/admin-login.transformer'; -import { UserLoginTransformer } from '../transformers/user-login.transformer'; -import { ConfigService } from '@nestjs/config'; + import { ConfigService } from '@nestjs/config'; import { normalizePhone } from '../../utils/phone.util'; @Injectable() @@ -20,8 +18,7 @@ export class AuthService { constructor( private readonly cacheService: CacheService, private readonly smsService: SmsService, - private readonly userService: UserService, - private readonly tokensService: TokensService, + private readonly tokensService: TokensService, private readonly restRepository: RestRepository, private readonly adminRepository: AdminRepository, private readonly configService: ConfigService, @@ -56,29 +53,7 @@ export class AuthService { return { code: null }; } - async verifyOtp(phone: string, slug: string, code: string) { - const normalizedPhone = normalizePhone(phone); - const key = this.userOtpKey(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); - - const user = await this.userService.findOrCreateByPhone(normalizedPhone); - - const rest = await this.restRepository.findOne({ slug }); - - if (!rest) { - throw new BadRequestException(RestMessage.NOT_FOUND); - } - - const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false, slug); - - const userResponse = UserLoginTransformer.transform(user, rest); - - return { tokens, user: userResponse }; - } + async verifyOtpAdmin(phone: string, slug: string, code: string) { const normalizedPhone = normalizePhone(phone); diff --git a/src/modules/auth/transformers/user-login.transformer.ts b/src/modules/auth/transformers/user-login.transformer.ts index 6148def..8663bcb 100644 --- a/src/modules/auth/transformers/user-login.transformer.ts +++ b/src/modules/auth/transformers/user-login.transformer.ts @@ -1,6 +1,4 @@ -import type { User } from '../../users/entities/user.entity'; -import type { Restaurant } from '../../restaurants/entities/restaurant.entity'; - + export interface UserLoginResponse { id: string; firstName: string; @@ -15,19 +13,4 @@ export interface UserLoginResponse { }; } -export class UserLoginTransformer { - static transform(user: User, restaurant: Restaurant): UserLoginResponse { - return { - id: user.id, - firstName: user.firstName, - lastName: user.lastName, - phone: user.phone, - isActive: user.isActive, - restaurant: { - id: restaurant.id, - name: restaurant.name, - slug: restaurant.slug, - }, - }; - } -} + diff --git a/src/modules/foods/controllers/food.controller.ts b/src/modules/foods/controllers/food.controller.ts index 097bd7a..765dd90 100644 --- a/src/modules/foods/controllers/food.controller.ts +++ b/src/modules/foods/controllers/food.controller.ts @@ -54,15 +54,7 @@ export class FoodController { return this.foodsService.toggleFavorite(userId, foodId); } - @Get('public/foods/favorite') - @UseGuards(AuthGuard) - @ApiHeader(API_HEADER_SLUG) - @ApiBearerAuth() - @ApiOperation({ summary: 'get my favorites' }) - getMyFavorites(@UserId() userId: string, @RestId() restId: string) { - return this.foodsService.getMyFavorites(userId, restId); - } - + /* ---------------------------------- Admin ---------------------------------- */ @UseGuards(AdminAuthGuard) @ApiBearerAuth() diff --git a/src/modules/foods/entities/favorite.entity.ts b/src/modules/foods/entities/favorite.entity.ts index 245e102..439edb0 100644 --- a/src/modules/foods/entities/favorite.entity.ts +++ b/src/modules/foods/entities/favorite.entity.ts @@ -1,13 +1,10 @@ import { Entity, ManyToOne, Unique } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { User } from '../../users/entities/user.entity'; -import { Food } from '../../foods/entities/food.entity'; + import { Food } from '../../foods/entities/food.entity'; @Entity({ tableName: 'favorites' }) -@Unique({ properties: ['user', 'food'] }) -export class Favorite extends BaseEntity { - @ManyToOne(() => User) - user: User; + export class Favorite extends BaseEntity { + @ManyToOne(() => Food) food: Food; diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index 7428334..937832b 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -82,9 +82,7 @@ export class FoodService { const food = await this.foodRepository.findOne({ id: foodId, isActive: true }, { populate: ['category',] }); if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND); let isFavorite = false; - if (userId) { - isFavorite = (await this.em.count(Favorite, { user: { id: userId }, food: { id: foodId } })) > 0; - } + return { ...food, isFavorite, @@ -230,21 +228,18 @@ export class FoodService { async toggleFavorite(userId: string, foodId: string) { - const favorite = await this.em.findOne(Favorite, { user: userId, food: foodId }); + const favorite = await this.em.findOne(Favorite, { food: foodId }); if (favorite) { return this.em.removeAndFlush(favorite); } const newFavorite = this.em.create(Favorite, { - user: userId, food: foodId, }); return this.em.persistAndFlush(newFavorite); } - async getMyFavorites(userId: string, restId: string) { - return this.em.find(Favorite, { user: userId, food: { restaurant: { id: restId } } }, { populate: ['food'] }); - } + /** * Invalidate cache for restaurant foods */ diff --git a/src/modules/notifications/controllers/notifications.controller.ts b/src/modules/notifications/controllers/notifications.controller.ts deleted file mode 100644 index 71e5335..0000000 --- a/src/modules/notifications/controllers/notifications.controller.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Controller, Get, Body, Param, UseGuards, Query, Patch, Put, Post } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiHeader } from '@nestjs/swagger'; -import { NotificationService } from '../services/notification.service'; -import { NotificationPreferenceService } from '../services/notification-preference.service'; -import { AuthGuard } from '../../auth/guards/auth.guard'; -import { UserId } from '../../../common/decorators/user-id.decorator'; -import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard'; -import { RestId } from '../../../common/decorators/rest-id.decorator'; -import { UpdatePreferenceDto } from '../dto/update-preference.dto'; -import { CreatePreferenceDto } from '../dto/create-preference.dto'; -import { AdminId } from 'src/common/decorators/admin-id.decorator'; -import { API_HEADER_SLUG } from 'src/common/constants/index'; -import { NotificationMessage } from 'src/common/enums/message.enum'; -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('notifications') -@Controller() -export class NotificationsController { - constructor( - private readonly notificationService: NotificationService, - private readonly preferenceService: NotificationPreferenceService, - ) { } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Get('public/notifications') - @ApiOperation({ summary: 'Get user restaurant notifications' }) - @ApiHeader(API_HEADER_SLUG) - @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' }) - @ApiQuery({ - name: 'cursor', - required: false, - type: String, - description: 'Cursor for pagination (ID of the last item from previous page)', - }) - @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' }) - async getUserNotifications( - @UserId() userId: string, - @RestId() restaurantId: string, - @Query('limit') limit?: number, - @Query('cursor') cursor?: string, - @Query('status') status?: 'seen' | 'unseen', - ) { - return await this.notificationService.findByUserAndRestaurant( - userId, - restaurantId, - limit ? parseInt(limit.toString(), 10) : 50, - cursor, - status, - ); - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Get('public/notifications/unseen-count') - @ApiOperation({ summary: 'Get unseen notifications count for user' }) - @ApiHeader(API_HEADER_SLUG) - async getUserUnseenCount(@UserId() userId: string, @RestId() restaurantId: string) { - const count = await this.notificationService.countUnseenByUserAndRestaurant(userId, restaurantId); - return { count }; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Put('public/notifications/:id') - @ApiOperation({ summary: 'Read a notification ' }) - @ApiParam({ name: 'id', description: 'Notification ID' }) - async readNotificationUser(@RestId() restaurantId: string, @Param('id') id: string, @UserId() userId: string) { - await this.notificationService.readNotificationAsUser(id, userId, restaurantId); - return { message: NotificationMessage.READ_SUCCESS }; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Put('public/notifications/read/all') - @ApiOperation({ summary: 'Read all notification ' }) - async readAllNotificationUser(@RestId() restaurantId: string, @UserId() userId: string) { - await this.notificationService.readAllNotifsAsUser(userId, restaurantId); - return { message: NotificationMessage.READ_SUCCESS }; - } - /* ***************** Admin Endpoints ***************** */ - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Get('admin/notifications') - @ApiOperation({ summary: 'Get Admin notifications' }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ - name: 'cursor', - required: false, - type: String, - description: 'Cursor for pagination (ID of the last item from previous page)', - }) - @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' }) - async getAdminNotifications( - @AdminId() adminId: string, - @RestId() restaurantId: string, - @Query('limit') limit?: number, - @Query('cursor') cursor?: string, - @Query('status') status?: 'seen' | 'unseen', - ) { - const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50; - return await this.notificationService.findByRestaurant(restaurantId, adminId, parsedLimit, cursor, status); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Get('admin/notifications/unseen-count') - @ApiOperation({ summary: 'Get unseen notifications count for admin' }) - async getAdminUnseenCount(@AdminId() adminId: string, @RestId() restaurantId: string) { - const count = await this.notificationService.countUnseenByRestaurant(adminId, restaurantId); - return { count }; - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Put('admin/notifications/:id') - @ApiOperation({ summary: 'Read a notification ' }) - @ApiParam({ name: 'id', description: 'Notification ID' }) - async readNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) { - await this.notificationService.readNotificationAdmin(id, adminId, restaurantId); - return { message: NotificationMessage.READ_SUCCESS }; - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Put('admin/notifications/read/all') - @ApiOperation({ summary: 'Read all notification ' }) - async readAllNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) { - await this.notificationService.readAllNotifsAsAdmin(adminId, restaurantId); - return { message: NotificationMessage.READ_SUCCESS }; - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_SETTINGS) - @Get('admin/notification-preferences') - @ApiOperation({ summary: 'Get all notification preferences for a restaurant' }) - async getPreferences(@RestId() restaurantId: string) { - return this.preferenceService.findByRestaurant(restaurantId); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_SETTINGS) - @Patch('admin/notification-preferences/:id') - @ApiOperation({ summary: 'Update notification channels' }) - @ApiParam({ name: 'id', description: 'Notification preference ID' }) - async updatePreference( - @RestId() restaurantId: string, - @Param('id') preferenceId: string, - @Body() dto: UpdatePreferenceDto, - ) { - return this.preferenceService.updatePreference(preferenceId, restaurantId, dto); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_SETTINGS) - @Post('admin/notification-preferences') - @ApiOperation({ summary: 'Create notification preference' }) - async createPreference( - @RestId() restaurantId: string, - @Body() dto: CreatePreferenceDto, - ) { - return this.preferenceService.create(restaurantId, dto); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Get('admin/notifications/sms-usage') - @ApiOperation({ summary: 'Get SMS usage for my restaurant' }) - async getRestaurantSmsUsage(@RestId() restaurantId: string) { - const smsCount = await this.notificationService.getSmsCountByRestaurantId(restaurantId); - return { smsCount }; - } - - // super admin endpoints - - @UseGuards(SuperAdminAuthGuard) - @ApiBearerAuth() - @Get('super-admin/notifications/sms-count') - @ApiOperation({ summary: 'Get SMS count for each restaurant with pagination' }) - @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)', minimum: 1 }) - @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 10)', minimum: 1 }) - async getSmsCount( - @Query('page') page?: number, - @Query('limit') limit?: number, - ) { - const parsedPage = page ? parseInt(page.toString(), 10) : 1; - const parsedLimit = limit ? parseInt(limit.toString(), 10) : 10; - return await this.notificationService.getSmsCountByRestaurant(parsedPage, parsedLimit); - } -} diff --git a/src/modules/notifications/crone/notification.crone.ts b/src/modules/notifications/crone/notification.crone.ts deleted file mode 100644 index 01052fa..0000000 --- a/src/modules/notifications/crone/notification.crone.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Notification } from '../entities/notification.entity'; - -@Injectable() -export class NotificationCrone { - private readonly logger = new Logger(NotificationCrone.name); - - constructor(private readonly em: EntityManager) {} - - // run every day at 03:00 (3:00 AM) - @Cron('0 3 * * *', { - name: 'deleteOldNotifications', - timeZone: 'UTC', - }) - async handleCron() { - try { - this.logger.debug('Starting daily notification cleanup (removing notifications older than 24 hours)'); - - // Calculate the date 24 hours ago - const twentyFourHoursAgo = new Date(); - twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24); - - // Find all notifications created more than 24 hours ago - const oldNotifications = await this.em.find(Notification, { - createdAt: { $lt: twentyFourHoursAgo }, - }); - - if (!oldNotifications || oldNotifications.length === 0) { - this.logger.debug('No old notifications found to delete'); - return; - } - - this.logger.log(`Found ${oldNotifications.length} notifications older than 24 hours to delete`); - - // Delete the old notifications - await this.em.removeAndFlush(oldNotifications); - - this.logger.log(`Successfully deleted ${oldNotifications.length} old notifications`); - } catch (err) { - this.logger.error(`NotificationCrone failed: ${err?.message}`, err); - } - } -} - diff --git a/src/modules/notifications/decorators/ws-rest-id.decorator.ts b/src/modules/notifications/decorators/ws-rest-id.decorator.ts deleted file mode 100644 index 8391a8e..0000000 --- a/src/modules/notifications/decorators/ws-rest-id.decorator.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ExecutionContext } from '@nestjs/common'; -import { createParamDecorator } from '@nestjs/common'; -import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard'; - -/** - * Extract restaurant ID from authenticated WebSocket client - * Use this decorator in WebSocket handlers to get the restId from the authenticated admin - * - * @example - * ```typescript - * @SubscribeMessage('get:notifications') - * handleGetNotifications(@WsRestId() restId: string) { - * // restId is automatically extracted from authenticated admin - * } - * ``` - */ -export const WsRestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => { - const client = ctx.switchToWs().getClient(); - const restId = client.restId; - - if (!restId) { - throw new Error('Restaurant ID not found. Ensure WsAdminAuthGuard is applied.'); - } - - return restId; -}); diff --git a/src/modules/notifications/entities/notification-preference.entity.ts b/src/modules/notifications/entities/notification-preference.entity.ts deleted file mode 100644 index e3ab3bc..0000000 --- a/src/modules/notifications/entities/notification-preference.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Entity, Property, ManyToOne, Unique } 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 { NotifChannelEnum } from '../interfaces/notification.interface'; - -@Entity({ tableName: 'notification_preferences' }) -@Unique({ properties: ['restaurant', 'title'] }) -export class NotificationPreference extends BaseEntity { - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - - @Property() - title!: NotifTitleEnum; - - @Property({ type: 'json' }) - channels: NotifChannelEnum[] = []; -} diff --git a/src/modules/notifications/entities/notification.entity.ts b/src/modules/notifications/entities/notification.entity.ts index 0f2d544..cdd1424 100644 --- a/src/modules/notifications/entities/notification.entity.ts +++ b/src/modules/notifications/entities/notification.entity.ts @@ -1,8 +1,7 @@ import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { User } from '../../users/entities/user.entity'; -import { NotifTitleEnum } from '../interfaces/notification.interface'; + import { NotifTitleEnum } from '../interfaces/notification.interface'; import { Admin } from 'src/modules/admin/entities/admin.entity'; @Entity({ tableName: 'notifications' }) @@ -10,9 +9,7 @@ export class Notification extends BaseEntity { @ManyToOne(() => Restaurant) restaurant!: Restaurant; - @ManyToOne(() => User, { nullable: true }) - user?: User; - + @ManyToOne(() => Admin, { nullable: true }) admin?: Admin; diff --git a/src/modules/notifications/entities/smsLogs.entity.ts b/src/modules/notifications/entities/smsLogs.entity.ts deleted file mode 100644 index edeb103..0000000 --- a/src/modules/notifications/entities/smsLogs.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Entity, Property, ManyToOne, PrimaryKey } from '@mikro-orm/core'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; - -@Entity({ tableName: 'sms_logs' }) -export class SmsLog { - @PrimaryKey() - id!: number; - - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - - @Property() - phone!: string; - - @Property() - createdAt: Date = new Date(); -} \ No newline at end of file diff --git a/src/modules/notifications/events/sms.events.ts b/src/modules/notifications/events/sms.events.ts deleted file mode 100644 index 303b366..0000000 --- a/src/modules/notifications/events/sms.events.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class SmsSentEvent { - constructor( - public readonly phoneNumber: string, - public readonly restaurantId: string, - ) {} -} - diff --git a/src/modules/notifications/listeners/sms.listeners.ts b/src/modules/notifications/listeners/sms.listeners.ts deleted file mode 100644 index 8c2a842..0000000 --- a/src/modules/notifications/listeners/sms.listeners.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; -import { SmsSentEvent } from '../events/sms.events'; -import { RestRepository } from '../../restaurants/repositories/rest.repository'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { SmsLog } from '../entities/smsLogs.entity'; - -@Injectable() -export class SmsListeners { - private readonly logger = new Logger(SmsListeners.name); - - constructor( - private readonly restRepository: RestRepository, - private readonly em: EntityManager, - ) { - } - - @OnEvent(SmsSentEvent.name) - async handleSmsSent(event: SmsSentEvent) { - try { - this.logger.log( - `SMS sent event received: phone ${event.phoneNumber} for restaurant: ${event.restaurantId}`, - ); - - // Get the restaurant entity - const restaurant = await this.restRepository.findOne({ id: event.restaurantId }); - - if (!restaurant) { - this.logger.warn( - `Restaurant not found for SMS log: ${event.restaurantId}`, - ); - return; - } - - // Create SMS log record - const smsLog = this.em.create(SmsLog, { - restaurant: restaurant, - phone: event.phoneNumber, - createdAt: new Date(), - }); - - await this.em.flush(); - - this.logger.log( - `SMS log created successfully: ${smsLog.id} for phone ${event.phoneNumber}`, - ); - } catch (error) { - this.logger.error( - `Failed to create SMS log for event: ${event.restaurantId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } - -} \ No newline at end of file diff --git a/src/modules/notifications/notifications.gateway.ts b/src/modules/notifications/notifications.gateway.ts deleted file mode 100644 index 2bbd80d..0000000 --- a/src/modules/notifications/notifications.gateway.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - WebSocketGateway, - WebSocketServer, - // SubscribeMessage, - OnGatewayConnection, - OnGatewayDisconnect, - // MessageBody, - // ConnectedSocket, -} from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { Logger, Inject, forwardRef } from '@nestjs/common'; -import { NotificationService } from './services/notification.service'; -import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-auth.guard'; -import { ModuleRef } from '@nestjs/core'; -import { ExecutionContext } from '@nestjs/common'; -import { IInAppNotificationPayload } from './interfaces/notification.interface'; - -@WebSocketGateway({ - cors: { - origin: true, - credentials: true, - }, - namespace: '/notifications', - transports: ['websocket', 'polling'], -}) -export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect { - @WebSocketServer() - server!: Server; - - private readonly logger = new Logger(NotificationsGateway.name); - private readonly pingIntervals = new Map(); - - constructor( - @Inject(forwardRef(() => NotificationService)) - private readonly notificationService: NotificationService, - private readonly moduleRef: ModuleRef, - ) {} - - async handleConnection(client: Socket) { - try { - await this.authenticateConnection(client); - const authenticatedClient = client as AuthenticatedSocket; - this.logger.log(`Admin connected: ${authenticatedClient.adminId}`); - this.handleJoinRoom(authenticatedClient); - - // Start ping interval for this client - // const intervalId = setInterval(() => { - // void authenticatedClient.emit('ping', { message: 'ping', timestamp: new Date().toISOString() }); - // }, 2000); - - // this.pingIntervals.set(client.id, intervalId); - } catch (error) { - this.logger.error( - `Connection authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - void client.emit('error', { message: 'Authentication failed' }); - client.disconnect(); - } - } - - handleDisconnect(client: Socket) { - this.logger.log(`Client disconnected: ${client.id}`); - this.handleLeaveRoom(client); - - // Clear ping interval for this client - // const intervalId = this.pingIntervals.get(client.id); - // if (intervalId) { - // clearInterval(intervalId); - // this.pingIntervals.delete(client.id); - // } - } - - sendInAppNotification(repipient: { adminId: string; restaurantId: string }, payload: IInAppNotificationPayload) { - const room = this.getRoom(repipient.adminId, repipient.restaurantId); - - this.logger.log(`Sending in app notification to admin: ${repipient.adminId}`); - this.server.to(room).emit('notifications', payload, async () => { - // 3. ACK (delivered) - // await this.notificationService.markAsDelivered(payload.notificationId); - }); - this.logger.log(`In app notification sent to admin: ${repipient.adminId}`); - } - - private getRoom(adminId: string, restaurantId: string) { - return `restaurant:${restaurantId}-admin:${adminId}`; - } - - handleLeaveRoom(client: AuthenticatedSocket) { - const room = this.getRoom(client.adminId!, client.restId!); - void client.leave(room); - this.logger.log(`Admin ${client.adminId} (Client ${client.id}) left room: ${room}`); - void client.emit('left', { room, message: 'Successfully Admin left room' }); - } - - handleJoinRoom(client: AuthenticatedSocket) { - const room = this.getRoom(client.adminId!, client.restId!); - void client.join(room); - this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`); - void client.emit('joined', { room, message: 'Successfully Admin joined room' }); - } - - private async authenticateConnection(client: Socket): Promise { - const guard = this.moduleRef.get(WsAdminAuthGuard); - const context = { - switchToWs: () => ({ getClient: () => client }), - getClass: () => NotificationsGateway, - getHandler: () => this.handleConnection, - } as unknown as ExecutionContext; - - const canActivate = await guard.canActivate(context); - if (!canActivate) { - throw new Error('Authentication failed'); - } - } -} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index c99a1ae..3540cc0 100644 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -3,37 +3,26 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { BullModule } from '@nestjs/bullmq'; import { JwtModule } from '@nestjs/jwt'; import { Notification } from './entities/notification.entity'; -import { NotificationPreference } from './entities/notification-preference.entity'; -import { NotificationService } from './services/notification.service'; -import { NotificationPreferenceService } from './services/notification-preference.service'; 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 { NotificationsController } from './controllers/notifications.controller'; -import { User } from '../users/entities/user.entity'; 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 { NotificationsGateway } from './notifications.gateway'; import { WsAdminAuthGuard } from './guards/ws-admin-auth.guard'; -import { InAppProcessor } from './processors/in-app.processor'; import { AdminModule } from '../admin/admin.module'; -import { UserModule } from '../users/user.module'; -import { NotificationCrone } from './crone/notification.crone'; import { SmsService } from './services/sms.service'; -import { SmsLog } from './entities/smsLogs.entity'; -import { SmsLogRepository } from './repositories/sms-log.repository'; + import { RestaurantsModule } from '../restaurants/restaurants.module'; -import { SmsListeners } from './listeners/sms.listeners'; @Module({ imports: [ forwardRef(() => AuthModule), JwtModule, - MikroOrmModule.forFeature([Notification, NotificationPreference, User, Restaurant, SmsLog]), + MikroOrmModule.forFeature([Notification, Restaurant]), BullModule.registerQueue( { name: NotificationQueueNameEnum.SMS }, { name: NotificationQueueNameEnum.PUSH }, @@ -50,27 +39,19 @@ import { SmsListeners } from './listeners/sms.listeners'; }), }), AdminModule, - forwardRef(() => UserModule), UtilsModule, forwardRef(() => RestaurantsModule), ], - controllers: [NotificationsController], + controllers: [], providers: [ - NotificationService, - NotificationPreferenceService, NotificationQueueService, PushNotificationService, PushProcessor, SmsProcessor, - InAppProcessor, - NotificationsGateway, WsAdminAuthGuard, - NotificationCrone, SmsService, - SmsLogRepository, - SmsListeners, ], - exports: [NotificationService, NotificationPreferenceService, + exports: [ NotificationQueueService, PushNotificationService, SmsService], }) export class NotificationsModule { } diff --git a/src/modules/notifications/processors/in-app.processor.ts b/src/modules/notifications/processors/in-app.processor.ts deleted file mode 100644 index 45c23df..0000000 --- a/src/modules/notifications/processors/in-app.processor.ts +++ /dev/null @@ -1,50 +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 { EntityManager } from '@mikro-orm/postgresql'; -import { Notification } from '../entities/notification.entity'; -import { InAppNotificationQueueJob } from '../interfaces/jobs-queue.interface'; -import { NotificationQueueNameEnum } from '../constants/queue'; -import { NotificationsGateway } from '../notifications.gateway'; - -@Processor(NotificationQueueNameEnum.IN_APP) -export class InAppProcessor extends WorkerHost { - private readonly logger = new Logger(InAppProcessor.name); - - constructor( - @InjectRepository(Notification) - private readonly em: EntityManager, - private readonly notificationsGateway: NotificationsGateway, - ) { - super(); - } - - async process(job: Job): Promise { - const { recipient, subject, body, notificationId } = job.data; - - this.logger.log(`Processing InApp notification - Recipient: ${JSON.stringify(recipient)}, subject: ${subject}`); - - try { - this.notificationsGateway.sendInAppNotification( - { adminId: recipient.adminId, restaurantId: recipient.restaurantId }, - { subject, body, notificationId }, - ); - - this.logger.log(`InApp notification sent successfully to ${recipient.adminId}`); - } catch (error) { - this.logger.error(`Error processing InApp 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); - } -} diff --git a/src/modules/notifications/processors/sms.processor.ts b/src/modules/notifications/processors/sms.processor.ts index 0846e2b..d6f2f5c 100644 --- a/src/modules/notifications/processors/sms.processor.ts +++ b/src/modules/notifications/processors/sms.processor.ts @@ -1,12 +1,10 @@ import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq'; import { Logger } from '@nestjs/common'; import { Job } from 'bullmq'; -import { UserRepository } from 'src/modules/users/repositories/user.repository'; -import { SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface'; + import { SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { NotificationQueueNameEnum } from '../constants/queue'; import { SmsService } from '../services/sms.service'; -import { SmsSentEvent } from '../events/sms.events'; import { EventEmitter2 } from '@nestjs/event-emitter'; @Processor(NotificationQueueNameEnum.SMS) @@ -15,8 +13,7 @@ export class SmsProcessor extends WorkerHost { constructor( private readonly adminRepository: AdminRepository, - private readonly userRepository: UserRepository, - private readonly smsService: SmsService, + private readonly smsService: SmsService, private readonly eventEmitter: EventEmitter2, ) { super(); @@ -29,14 +26,7 @@ export class SmsProcessor extends WorkerHost { try { let phone!: string; - if ('userId' in recipient) { - const user = await this.userRepository.findOne({ id: recipient.userId }); - if (!user || !user.phone) { - this.logger.warn(`User ${recipient.userId} not found or has no phone number`); - throw new Error('User phone number is required for SMS notifications'); - } - phone = user.phone; - } + if ('adminId' in recipient) { const admin = await this.adminRepository.findOne({ id: recipient.adminId }); if (!admin || !admin.phone) { @@ -58,7 +48,6 @@ export class SmsProcessor extends WorkerHost { this.logger.log(`SMS notification sent successfully to ${phone}`); - this.eventEmitter.emit(SmsSentEvent.name, new SmsSentEvent(phone, restaurantId)); return { success: true, diff --git a/src/modules/notifications/repositories/sms-log.repository.ts b/src/modules/notifications/repositories/sms-log.repository.ts deleted file mode 100644 index 6076a29..0000000 --- a/src/modules/notifications/repositories/sms-log.repository.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { SmsLog } from '../entities/smsLogs.entity'; -import { PaginatedResult } from '../../../common/interfaces/pagination.interface'; - -@Injectable() -export class SmsLogRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, SmsLog); - } - - async getSmsCountByRestaurant( - page: number = 1, - limit: number = 10, - ): Promise> { - const offset = (page - 1) * limit; - - // Get total count of unique restaurants with SMS logs - const totalResult = await this.em.execute( - ` - SELECT COUNT(DISTINCT sl.restaurant_id) as total - FROM sms_logs sl - WHERE sl.restaurant_id IS NOT NULL - `, - ); - const total = parseInt(totalResult[0]?.total || '0', 10); - - // Get paginated results with SMS counts grouped by restaurant - const results = await this.em.execute( - ` - SELECT - r.id as "restaurantId", - r.name as "restaurantName", - COUNT(sl.id)::int as "smsCount" - FROM sms_logs sl - INNER JOIN restaurants r ON sl.restaurant_id = r.id - GROUP BY r.id, r.name - ORDER BY "smsCount" DESC, r.name ASC - LIMIT ? OFFSET ? - `, - [limit, offset], - ); - - const data = results.map((row: any) => ({ - restaurantId: row.restaurantId, - restaurantName: row.restaurantName || 'Unknown', - smsCount: parseInt(row.smsCount || '0', 10), - })); - - const totalPages = Math.ceil(total / limit); - - return { - data, - meta: { - total, - page, - limit, - totalPages, - }, - }; - } - - async getSmsCountByRestaurantId(restaurantId: string): Promise { - const result = await this.em.execute( - ` - SELECT COUNT(sl.id)::int as "smsCount" - FROM sms_logs sl - WHERE sl.restaurant_id = ? - `, - [restaurantId], - ); - - return parseInt(result[0]?.smsCount || '0', 10); - } -} - diff --git a/src/modules/notifications/services/notification-preference.service.ts b/src/modules/notifications/services/notification-preference.service.ts deleted file mode 100644 index afe4477..0000000 --- a/src/modules/notifications/services/notification-preference.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { NotificationPreference } from '../entities/notification-preference.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { CreatePreferenceDto } from '../dto/create-preference.dto'; -import { UpdatePreferenceDto } from '../dto/update-preference.dto'; -import { NotifTitleEnum } from '../interfaces/notification.interface'; - -@Injectable() -export class NotificationPreferenceService { - constructor(private readonly em: EntityManager) { } - - async create(restaurantId: string, dto: CreatePreferenceDto): Promise { - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) { - throw new NotFoundException('Restaurant not found'); - } - - const preference = this.em.create(NotificationPreference, { - restaurant, - channels: dto.channels, - title: dto.title, - }); - - await this.em.persistAndFlush(preference); - return preference; - } - - async findByRestaurant(restaurantId: string): Promise { - return this.em.find(NotificationPreference, { - restaurant: { id: restaurantId }, - }); - } - - async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum): Promise { - return this.em.findOne(NotificationPreference, { - restaurant: { id: restaurantId }, - title, - }); - } - - // async updateEnabled( - // restaurantId: string, - // notificationType: string, - // enabled: boolean, - // ): Promise { - // const preference = await this.findByRestaurantAndType(restaurantId, notificationType); - // if (!preference) { - // throw new NotFoundException('Notification preference not found'); - // } - - // preference.enabled = enabled; - // await this.em.persistAndFlush(preference); - // return preference; - // } - - async updatePreference( - preferenceId: string, - restaurantId: string, - dto: UpdatePreferenceDto, - ): Promise { - const preference = await this.em.findOne(NotificationPreference, { - id: preferenceId, - restaurant: { id: restaurantId }, - }); - if (!preference) { - throw new NotFoundException('Notification preference not found'); - } - - this.em.assign(preference, dto); - await this.em.persistAndFlush(preference); - return preference; - } - - async delete(restaurantId: string, preferenceId: string): Promise { - const preference = await this.em.findOne(NotificationPreference, { - restaurant: { id: restaurantId }, - id: preferenceId, - }); - if (!preference) { - throw new NotFoundException('Notification preference not found'); - } - await this.em.removeAndFlush(preference); - } -} diff --git a/src/modules/notifications/services/notification.service.ts b/src/modules/notifications/services/notification.service.ts deleted file mode 100644 index 6c7e243..0000000 --- a/src/modules/notifications/services/notification.service.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { EntityManager, FilterQuery } from '@mikro-orm/postgresql'; -import { Notification } from '../entities/notification.entity'; -import { NotificationPreferenceService } from './notification-preference.service'; -import { NotificationQueueService } from './notification-queue.service'; -import { NotificationsGateway } from '../notifications.gateway'; -import { NotifChannelEnum, NotifRequest, NotifTitleEnum } from '../interfaces/notification.interface'; -import { SmsLog } from '../entities/smsLogs.entity'; -import { SmsLogRepository } from '../repositories/sms-log.repository'; -import { PaginatedResult } from '../../../common/interfaces/pagination.interface'; - -@Injectable() -export class NotificationService { - private readonly logger = new Logger(NotificationService.name); - - constructor( - private readonly em: EntityManager, - private readonly preferenceService: NotificationPreferenceService, - private readonly queueService: NotificationQueueService, - private readonly notificationGateway: NotificationsGateway, - private readonly smsLogRepository: SmsLogRepository, - ) { } - - async sendNotification(params: NotifRequest): Promise { - const { recipients, message, metadata, restaurantId } = params; - - // create Database notifications - const notifications = await this.createAdminBulkNotifications( - recipients.map(recipient => ({ - restaurantId, - title: message.title, - content: message.content, - adminId: 'adminId' in recipient ? recipient.adminId : null, - userId: 'userId' in recipient ? recipient.userId : null, - })), - ); - - // get admin prefrences - const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, message.title); - - if (preference?.channels?.length === 0) { - this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${message.title}`); - return notifications; - } - - // send in app notification - if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) { - await this.queueService.addBulkInAppNotifications( - notifications.map(notification => ({ - recipient: { adminId: notification.admin?.id || '', restaurantId }, - subject: message.title, - body: message.content, - notificationId: notification.id, - })), - ); - } - - // add sms notifications to queue - if (preference?.channels?.includes(NotifChannelEnum.SMS)) { - await this.queueService.addBulkSmsNotifications( - recipients.map(recipient => ({ - recipient, - templateId: message.sms.templateId, - parameters: message.sms.parameters, - restaurantId, - })), - ); - } - - this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.title}`); - - return notifications; - } - - async createAdminBulkNotifications( - params: { - restaurantId: string; - title: NotifTitleEnum; - content: string; - adminId: string | null; - userId: string | null; - }[], - ): Promise { - const notifications = params.map(param => { - return this.em.create(Notification, { - restaurant: param.restaurantId, - admin: param.adminId, - user: param.userId && param.userId.trim() !== '' ? param.userId : null, - title: param.title, - content: param.content, - }); - }); - await this.em.persistAndFlush(notifications); - return notifications; - } - - async findOne(id: string): Promise { - const notification = await this.em.findOne(Notification, { id }, { populate: ['restaurant', 'user'] }); - if (!notification) { - throw new NotFoundException('Notification not found'); - } - return notification; - } - - async findByRestaurant( - restaurantId: string, - adminId: string, - limit = 50, - cursor?: string, - status?: 'seen' | 'unseen', - ): Promise<{ data: Notification[]; nextCursor: string | null }> { - const where: FilterQuery = { - restaurant: { id: restaurantId }, - admin: { id: adminId }, - }; - - // Filter by status (seen/unseen) - if (status === 'seen') { - where.seenAt = { $ne: null }; - } else if (status === 'unseen') { - where.seenAt = null; - } - - // Cursor-based pagination: if cursor is provided, get items with id < cursor - if (cursor) { - where.id = { $lt: cursor }; - } - - const notifications = await this.em.find(Notification, where, { - orderBy: { createdAt: 'DESC', id: 'DESC' }, - limit: limit + 1, // fetch one extra to determine next page - populate: ['user'], - }); - - const hasNextPage = notifications.length > limit; - const data = hasNextPage ? notifications.slice(0, limit) : notifications; - const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null; - - return { - data, - nextCursor: nextCursor ?? null, - }; - } - - async findByUserAndRestaurant( - userId: string, - restaurantId: string, - limit = 50, - cursor?: string, - status?: 'seen' | 'unseen', - ): Promise<{ data: Notification[]; nextCursor: string | null }> { - const where: FilterQuery = { - user: { id: userId }, - restaurant: { id: restaurantId }, - }; - - // Filter by status (seen/unseen) - if (status === 'seen') { - where.seenAt = { $ne: null }; - } else if (status === 'unseen') { - where.seenAt = null; - } - - // Cursor-based pagination: if cursor is provided, get items with id < cursor (since ULIDs are time-ordered) - if (cursor) { - where.id = { $lt: cursor }; - } - - const notifications = await this.em.find(Notification, where, { - orderBy: { createdAt: 'DESC', id: 'DESC' }, - limit: limit + 1, // Fetch one extra to determine if there's a next page - }); - - // Check if there's a next page - const hasNextPage = notifications.length > limit; - const data = hasNextPage ? notifications.slice(0, limit) : notifications; - const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null; - - return { - data, - nextCursor: nextCursor ?? null, - }; - } - - async readNotificationAdmin(id: string, adminId: string, restaurantId: string): Promise { - const notification = await this.em.findOne(Notification, { - id, - admin: { id: adminId }, - restaurant: { id: restaurantId }, - }); - if (!notification) { - throw new NotFoundException('Notification not found'); - } - notification.seenAt = new Date(); - await this.em.persistAndFlush(notification); - } - async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise { - const notification = await this.em.findOne(Notification, { - id, - user: { id: userId }, - restaurant: { id: restaurantId }, - }); - if (!notification) { - throw new NotFoundException('Notification not found'); - } - notification.seenAt = new Date(); - await this.em.persistAndFlush(notification); - } - - async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum, limit = 50): Promise { - return this.em.find( - Notification, - { - restaurant: { id: restaurantId }, - title, - }, - { - orderBy: { createdAt: 'DESC' }, - limit, - populate: ['user'], - }, - ); - } - - async findByAdminAndRestaurant(adminId: string, restaurantId: string, limit = 50): Promise { - return this.em.find( - Notification, - { admin: { id: adminId }, restaurant: { id: restaurantId } }, - { - orderBy: { createdAt: 'DESC' }, - limit, - }, - ); - } - - async countUnseenByUserAndRestaurant(userId: string, restaurantId: string): Promise { - const where: FilterQuery = { - user: { id: userId }, - restaurant: { id: restaurantId }, - seenAt: null, - }; - return this.em.count(Notification, where); - } - - async countUnseenByRestaurant(adminId: string, restaurantId: string): Promise { - const where: FilterQuery = { - admin: { id: adminId }, - restaurant: { id: restaurantId }, - seenAt: null, - }; - return this.em.count(Notification, where); - } - - readAllNotifsAsUser(userId: string, restaurantId: string): Promise { - const where: FilterQuery = { - user: { id: userId }, - restaurant: { id: restaurantId }, - seenAt: null, - }; - return this.em.nativeUpdate(Notification, where, { seenAt: new Date() }); - } - - readAllNotifsAsAdmin(adminId: string, restaurantId: string): Promise { - const where: FilterQuery = { - admin: { id: adminId }, - restaurant: { id: restaurantId }, - seenAt: null, - }; - return this.em.nativeUpdate(Notification, where, { seenAt: new Date() }); - } - - async getSmsCountByRestaurant( - page: number = 1, - limit: number = 10, - ): Promise> { - return this.smsLogRepository.getSmsCountByRestaurant(page, limit); - } - - async getSmsCountByRestaurantId(restaurantId: string): Promise { - return this.smsLogRepository.getSmsCountByRestaurantId(restaurantId); - } -} diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index 35e4d83..43b3d3e 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -14,13 +14,10 @@ import { AdminRole } from '../../admin/entities/adminRole.entity'; import { Role } from '../../roles/entities/role.entity'; import { normalizePhone } from 'src/modules/utils/phone.util'; import slugify from 'slugify'; -import { NotificationPreference } from '../../notifications/entities/notification-preference.entity'; -import { notificationPreferencesData } from '../../../seeders/data/notification-preferences.data'; - import { Category } from '../../foods/entities/category.entity'; +import { Category } from '../../foods/entities/category.entity'; import { Food } from '../../foods/entities/food.entity'; - import { Schedule } from '../entities/schedule.entity'; -import { WalletTransaction } from '../../users/entities/wallet-transaction.entity'; -import { SmsLog } from '../../notifications/entities/smsLogs.entity'; +import { Schedule } from '../entities/schedule.entity'; + import { Notification } from '../../notifications/entities/notification.entity'; @@ -84,17 +81,10 @@ export class RestaurantsService { restaurant, }); - // Create notification preferences for the restaurant - const notificationPreferences = notificationPreferencesData.map(preferenceData => - em.create(NotificationPreference, { - restaurant, - title: preferenceData.title, - channels: preferenceData.channels, - }) - ); + // Persist all entities - em.persist([restaurant, admin, adminRole, ...notificationPreferences]); + em.persist([restaurant, admin, adminRole]); await em.flush(); return restaurant; @@ -229,7 +219,7 @@ export class RestaurantsService { 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 }); @@ -237,27 +227,24 @@ export class RestaurantsService { // 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 - await em.nativeDelete(NotificationPreference, { restaurant: id }); - + // Delete notifications await em.nativeDelete(Notification, { restaurant: id }); // Delete SMS logs - await em.nativeDelete(SmsLog, { restaurant: id }); - + // Delete admin roles for this restaurant await em.nativeDelete(AdminRole, { restaurant: id }); // Delete wallet transactions - await em.nativeDelete(WalletTransaction, { restaurant: id }); - + // Finally, delete the restaurant itself await em.nativeDelete(Restaurant, { id }); }); diff --git a/src/modules/users/controllers/users.controller.ts b/src/modules/users/controllers/users.controller.ts deleted file mode 100644 index a32a6de..0000000 --- a/src/modules/users/controllers/users.controller.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { Controller, Get, UseGuards, Patch, Body, ValidationPipe, Post, Query, Delete, Param } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse, ApiHeader } from '@nestjs/swagger'; -import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; -import { UserService } from '../providers/user.service'; -import { UpdateUserDto } from '../dto/update-user.dto'; -import { CreateUserAddressDto } from '../dto/create-user-address.dto'; -import { UpdateUserAddressDto } from '../dto/update-user-address.dto'; -import { UserId } from 'src/common/decorators/user-id.decorator'; -import { RestId } from 'src/common/decorators'; -import { FindUsersDto } from '../dto/find-user.dto'; -import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto'; -import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; -import { API_HEADER_SLUG } from 'src/common/constants/index'; -import { UserSuccessMessage } from 'src/common/enums/message.enum'; -import { Permissions } from 'src/common/decorators/permissions.decorator'; -import { Permission } from 'src/common/enums/permission.enum'; -import { WalletService } from '../providers/wallet.service'; - -@ApiTags('User') -@Controller() -export class UsersController { - constructor( - private readonly userService: UserService, - private readonly walletService: WalletService, - ) {} - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get the current authenticated user profile' }) - @ApiHeader(API_HEADER_SLUG) - @Get('public/user/me') - async getUser(@UserId() userId: string) { - const user = await this.userService.findById(userId); - return user; - } - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Update the authenticated user profile' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: UpdateUserDto }) - @Patch('public/user/update') - async updateUser(@UserId() userId: string, @RestId() restId: string, @Body() dto: UpdateUserDto) { - const user = await this.userService.updateUser(userId, restId, dto); - return user; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get all addresses for the authenticated user' }) - @ApiHeader(API_HEADER_SLUG) - @ApiOkResponse({ description: 'List of user addresses' }) - @Get('public/user/addresses') - async getUserAddresses(@UserId() userId: string) { - const addresses = await this.userService.getUserAddresses(userId); - return addresses; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get User Wallet' }) - @ApiHeader(API_HEADER_SLUG) - @Get('public/user/wallet/balance') - async getUserWalletBalance(@UserId() userId: string, @RestId() restId: string) { - const wallet = await this.walletService.getUserCurrentWalletBalance(userId, restId); - return wallet; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get User Wallet' }) - @ApiHeader(API_HEADER_SLUG) - @Get('public/user/points/balance') - async getUserWallet(@UserId() userId: string, @RestId() restId: string) { - const wallet = await this.walletService.getUserCurrentPoinrBalance(userId, restId); - return wallet; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get User Wallet Transactions' }) - @ApiHeader(API_HEADER_SLUG) - @Get('public/user/wallet/transactions') - async getUserWalletTransactions( - @UserId() userId: string, - @RestId() restId: string, - @Query(new ValidationPipe({ transform: true, whitelist: true })) - query: FindWalletTransactionsDto, - ) { - const transactions = await this.userService.getUserWalletTransactions(userId, restId, query); - return transactions; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Create a new address for the authenticated user' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: CreateUserAddressDto }) - @ApiOkResponse({ description: 'Created user address' }) - @Post('public/user/addresses') - async createUserAddress( - @UserId() userId: string, - @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateUserAddressDto, - ) { - const address = await this.userService.createUserAddress(userId, dto); - return address; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get a single address by ID for the authenticated user' }) - @ApiHeader(API_HEADER_SLUG) - @ApiOkResponse({ description: 'User address details' }) - @Get('public/user/addresses/:addressId') - async getUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) { - const address = await this.userService.getUserAddress(userId, addressId); - return address; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Update an address for the authenticated user' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: UpdateUserAddressDto }) - @ApiOkResponse({ description: 'Updated user address' }) - @Patch('public/user/addresses/:addressId') - async updateUserAddress( - @UserId() userId: string, - @Param('addressId') addressId: string, - @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserAddressDto, - ) { - const address = await this.userService.updateUserAddress(userId, addressId, dto); - return address; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Delete an address for the authenticated user' }) - @ApiHeader(API_HEADER_SLUG) - @ApiOkResponse({ description: 'Address deleted successfully' }) - @Delete('public/user/addresses/:addressId') - async deleteUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) { - await this.userService.deleteUserAddress(userId, addressId); - return { message: UserSuccessMessage.ADDRESS_DELETED_SUCCESS }; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Set an address as default for the authenticated user' }) - @ApiHeader(API_HEADER_SLUG) - @ApiOkResponse({ description: 'Address set as default' }) - @Patch('public/user/addresses/:addressId/default') - async setDefaultAddress(@UserId() userId: string, @Param('addressId') addressId: string) { - const address = await this.userService.setDefaultAddress(userId, addressId); - return address; - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Convert user score (points) to wallet balance' }) - @ApiHeader(API_HEADER_SLUG) - @Post('public/user/convert-score-to-wallet') - async convertScoreToWallet(@UserId() userId: string, @RestId() restId: string) { - await this.userService.convertScoreToWallet(userId, restId); - return { - message: UserSuccessMessage.SCORE_CONVERTED_SUCCESS, - }; - } - - /******************** Admin Routes **********************/ - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_USERS) - @ApiOperation({ summary: 'Get paginated list of restuarant users with filters' }) - @Get('admin/users') - async findAllRestaurantUsers( - @Query(new ValidationPipe({ transform: true, whitelist: true })) - query: FindUsersDto, - @RestId() restId: string, - ) { - return this.userService.findAll(restId, query); - } -} diff --git a/src/modules/users/dto/create-user-address.dto.ts b/src/modules/users/dto/create-user-address.dto.ts deleted file mode 100644 index 0e6a454..0000000 --- a/src/modules/users/dto/create-user-address.dto.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -/** - * DTO for creating a new user address. - */ -export class CreateUserAddressDto { - @ApiProperty({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' }) - @IsString() - title!: string; - - @ApiProperty({ description: 'Street address', example: '123 Main Street' }) - @IsString() - address!: string; - - @ApiProperty({ description: 'City name', example: 'Tehran' }) - @IsString() - city!: string; - - @ApiPropertyOptional({ description: 'Province', example: 'Tehran Province' }) - @IsOptional() - @IsString() - province?: string; - - @ApiPropertyOptional({ description: 'Postal/ZIP code', example: '12345-6789' }) - @IsOptional() - @IsString() - postalCode?: string; - - @ApiProperty({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 }) - @IsNumber() - @Min(-90) - @Max(90) - latitude!: number; - - @ApiProperty({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 }) - @IsNumber() - @Min(-180) - @Max(180) - longitude!: number; - - @ApiPropertyOptional({ description: 'Contact phone for this address', example: '+989123456789' }) - @IsOptional() - @IsString() - phone?: string; - - @ApiPropertyOptional({ description: 'Set as default address', example: false, default: false }) - @IsOptional() - @IsBoolean() - isDefault?: boolean; -} diff --git a/src/modules/users/dto/create-wallet-transaction.dto.ts b/src/modules/users/dto/create-wallet-transaction.dto.ts deleted file mode 100644 index 7f52fc3..0000000 --- a/src/modules/users/dto/create-wallet-transaction.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IsNotEmpty, IsNumber, IsIn } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; -import { WalletTransactionType, WalletTransactionReason } from '../interface/wallet'; - -export class CreateWalletTransactionDto { - @ApiProperty({ - description: 'Transaction amount', - type: Number, - example: 1000, - }) - @IsNotEmpty() - @IsNumber() - @Type(() => Number) - amount!: number; - - @ApiProperty({ - description: 'Transaction type', - enum: WalletTransactionType, - example: WalletTransactionType.CREDIT, - }) - @IsNotEmpty() - @IsIn(Object.values(WalletTransactionType)) - type!: WalletTransactionType; - - @ApiProperty({ - description: 'Transaction reason', - enum: WalletTransactionReason, - example: WalletTransactionReason.ORDER_PAYMENT, - }) - @IsNotEmpty() - @IsIn(Object.values(WalletTransactionReason)) - reason!: WalletTransactionReason; -} diff --git a/src/modules/users/dto/find-user.dto.ts b/src/modules/users/dto/find-user.dto.ts deleted file mode 100644 index ba3852b..0000000 --- a/src/modules/users/dto/find-user.dto.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { IsOptional, IsString, IsNumber, Min, IsIn } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { User } from '../entities/user.entity'; - -// Define the valid sort directions -const sortOrderOptions = ['asc', 'desc'] as const; -type SortOrder = (typeof sortOrderOptions)[number]; - -export class FindUsersDto { - /** - * The page number to retrieve. - * @default 1 - */ - @ApiPropertyOptional({ - description: 'شماره صفحه', - type: Number, - default: 1, - minimum: 1, - }) - @IsOptional() - @IsNumber() - @Min(1) - @Type(() => Number) - page?: number = 1; - - /** - * The number of items per page. - * @default 10 - */ - @ApiPropertyOptional({ - description: 'تعداد مورد در هر صفحه', - type: Number, - default: 10, - minimum: 1, - }) - @IsOptional() - @IsNumber() - @Min(1) - @Type(() => Number) - limit?: number = 10; - - /** - * A general search term to filter by. - * Searches firstName, lastName, phone, and nationalCode. - */ - @ApiPropertyOptional({ - description: 'جستجو بر اساس نام، نام‌خانوادگی یا شماره تماس', - type: String, - }) - @IsOptional() - @IsString() - search?: string; - - /** - * The field to sort the results by. - * @default "createdAt" - */ - @ApiPropertyOptional({ - description: 'فیلد مرتب‌سازی', - type: String, - default: 'createdAt', - }) - @IsOptional() - @IsString() - orderBy?: keyof User = 'createdAt'; - - /** - * The direction to sort the results. - * @default "desc" - */ - @ApiPropertyOptional({ - description: 'جهت مرتب‌سازی (asc یا desc)', - enum: sortOrderOptions, - default: 'desc', - }) - @IsOptional() - @IsIn(sortOrderOptions) - order?: SortOrder = 'desc'; -} diff --git a/src/modules/users/dto/find-wallet-transactions.dto.ts b/src/modules/users/dto/find-wallet-transactions.dto.ts deleted file mode 100644 index 2aa5d4b..0000000 --- a/src/modules/users/dto/find-wallet-transactions.dto.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { IsOptional, IsString, IsNumber, Min, IsIn, IsDateString } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { WalletTransactionType, WalletTransactionReason } from '../interface/wallet'; - -// Define the valid sort directions -const sortOrderOptions = ['asc', 'desc'] as const; -type SortOrder = (typeof sortOrderOptions)[number]; - -export class FindWalletTransactionsDto { - /** - * The page number to retrieve. - * @default 1 - */ - @ApiPropertyOptional({ - description: 'Page number', - type: Number, - default: 1, - minimum: 1, - }) - @IsOptional() - @IsNumber() - @Min(1) - @Type(() => Number) - page?: number = 1; - - /** - * The number of items per page. - * @default 10 - */ - @ApiPropertyOptional({ - description: 'Items per page', - type: Number, - default: 10, - minimum: 1, - }) - @IsOptional() - @IsNumber() - @Min(1) - @Type(() => Number) - limit?: number = 10; - - /** - * Filter by transaction type - */ - @ApiPropertyOptional({ - description: 'Transaction type filter', - enum: WalletTransactionType, - }) - @IsOptional() - @IsIn(Object.values(WalletTransactionType)) - type?: WalletTransactionType; - - /** - * Filter by transaction reason - */ - @ApiPropertyOptional({ - description: 'Transaction reason filter', - enum: WalletTransactionReason, - }) - @IsOptional() - @IsIn(Object.values(WalletTransactionReason)) - reason?: WalletTransactionReason; - - /** - * Filter transactions from this date onwards - */ - @ApiPropertyOptional({ - description: 'Start date filter (ISO 8601 format)', - type: String, - }) - @IsOptional() - @IsDateString() - startDate?: string; - - /** - * Filter transactions up to this date - */ - @ApiPropertyOptional({ - description: 'End date filter (ISO 8601 format)', - type: String, - }) - @IsOptional() - @IsDateString() - endDate?: string; - - /** - * Minimum amount filter - */ - @ApiPropertyOptional({ - description: 'Minimum amount filter', - type: Number, - }) - @IsOptional() - @IsNumber() - @Type(() => Number) - minAmount?: number; - - /** - * Maximum amount filter - */ - @ApiPropertyOptional({ - description: 'Maximum amount filter', - type: Number, - }) - @IsOptional() - @IsNumber() - @Type(() => Number) - maxAmount?: number; - - /** - * The field to sort the results by. - * @default "createdAt" - */ - @ApiPropertyOptional({ - description: 'Sort field', - type: String, - default: 'createdAt', - }) - @IsOptional() - @IsString() - orderBy?: string = 'createdAt'; - - /** - * The direction to sort the results. - * @default "desc" - */ - @ApiPropertyOptional({ - description: 'Sort direction', - enum: sortOrderOptions, - default: 'desc', - }) - @IsOptional() - @IsIn(sortOrderOptions) - order?: SortOrder = 'desc'; -} diff --git a/src/modules/users/dto/update-user-address.dto.ts b/src/modules/users/dto/update-user-address.dto.ts deleted file mode 100644 index e82330c..0000000 --- a/src/modules/users/dto/update-user-address.dto.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -/** - * DTO for updating a user address. - */ -export class UpdateUserAddressDto { - @ApiPropertyOptional({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' }) - @IsOptional() - @IsString() - title?: string; - - @ApiPropertyOptional({ description: 'Street address', example: '123 Main Street' }) - @IsOptional() - @IsString() - address?: string; - - @ApiPropertyOptional({ description: 'City name', example: 'Tehran' }) - @IsOptional() - @IsString() - city?: string; - - @ApiPropertyOptional({ description: 'Province', example: 'Tehran Province' }) - @IsOptional() - @IsString() - province?: string; - - @ApiPropertyOptional({ description: 'Postal/ZIP code', example: '12345-6789' }) - @IsOptional() - @IsString() - postalCode?: string; - - @ApiPropertyOptional({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 }) - @IsOptional() - @IsNumber() - @Min(-90) - @Max(90) - latitude?: number; - - @ApiPropertyOptional({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 }) - @IsOptional() - @IsNumber() - @Min(-180) - @Max(180) - longitude?: number; - - @ApiPropertyOptional({ description: 'Contact phone for this address', example: '+989123456789' }) - @IsOptional() - @IsString() - phone?: string; - - @ApiPropertyOptional({ description: 'Set as default address', example: false }) - @IsOptional() - @IsBoolean() - isDefault?: boolean; -} diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts deleted file mode 100644 index 533545c..0000000 --- a/src/modules/users/dto/update-user.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IsString, IsOptional, IsBoolean } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export class UpdateUserDto { - @ApiPropertyOptional({ description: "User's first name", example: 'John' }) - @IsOptional() - @IsString() - firstName?: string; - - @ApiPropertyOptional({ description: "User's last name", example: 'Doe' }) - @IsOptional() - @IsString() - lastName?: string; - - @ApiPropertyOptional({ description: "User's birth date (ISO)", example: '1990-01-01' }) - @IsOptional() - // keep as string here, caller should send ISO date; service will assign to Date - @IsString() - birthDate?: string; - - @ApiPropertyOptional({ description: "User's marriage date (ISO)", example: '2015-06-01' }) - @IsOptional() - @IsString() - marriageDate?: string; - - @ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true }) - @IsOptional() - @IsBoolean() - gender?: boolean; - - @ApiPropertyOptional({ description: 'Avatar URL' }) - @IsOptional() - @IsString() - avatarUrl?: string; -} diff --git a/src/modules/users/entities/point-transaction.entity.ts b/src/modules/users/entities/point-transaction.entity.ts deleted file mode 100644 index 91e97a2..0000000 --- a/src/modules/users/entities/point-transaction.entity.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Entity, Property, ManyToOne, Index, Enum } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; -import { User } from './user.entity'; -import { PointTransactionReason } from '../interface/point'; -import { PointTransactionType } from '../interface/point'; - -@Entity({ tableName: 'point_transactions' }) -@Index({ properties: ['user', 'restaurant'] }) // Composite index for most common query: find wallet by user and restaurant -@Index({ properties: ['user'] }) // Index for queries finding all wallets for a user -@Index({ properties: ['restaurant'] }) // Index for queries finding all wallets for a restaurant -export class PointTransaction extends BaseEntity { - @ManyToOne(() => User) - user!: User; - - @ManyToOne(() => User) - restaurant!: Restaurant; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - amount!: number; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - balance!: number; - - @Enum(() => PointTransaction) - type!: PointTransactionType; - - @Enum(() => PointTransactionReason) - reason!: PointTransactionReason; -} diff --git a/src/modules/users/entities/user-address.entity.ts b/src/modules/users/entities/user-address.entity.ts deleted file mode 100644 index d2ae78c..0000000 --- a/src/modules/users/entities/user-address.entity.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Entity, Property, ManyToOne } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { User } from './user.entity'; - -@Entity({ tableName: 'user_addresses' }) -export class UserAddress extends BaseEntity { - @ManyToOne(() => User) - user!: User; - - @Property() - title!: string; // e.g., "Home", "Work", "Office" - - @Property() - address!: string; - - @Property() - city!: string; - - @Property({ nullable: true }) - province?: string; - - @Property({ nullable: true }) - postalCode?: string | null = null; - - @Property({ type: 'float' }) - latitude!: number; - - @Property({ type: 'float' }) - longitude!: number; - - @Property({ nullable: true }) - phone?: string; // Contact phone for this address - - @Property({ default: false }) - isDefault: boolean = false; // Whether this is the default address -} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts deleted file mode 100644 index f0b4614..0000000 --- a/src/modules/users/entities/user.entity.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Entity, Index, Property, OneToMany, Collection, Cascade } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { UserAddress } from './user-address.entity'; - import { normalizePhone } from '../../utils/phone.util'; - -@Entity({ tableName: 'users' }) -@Index({ properties: ['isActive'] }) -export class User extends BaseEntity { - - - @Property() - firstName!: string; - - @Property({ nullable: true }) - lastName?: string; - - private _phone!: string; - - @Property({ unique: true }) - get phone(): string { - return this._phone; - } - - set phone(value: string) { - this._phone = normalizePhone(value); - } - - @Property({ default: null, type: 'date', nullable: true }) - birthDate?: Date; - - @Property({ default: null, type: 'date', nullable: true }) - marriageDate?: Date; - - @Property({ nullable: true }) - referrer?: string; - - @Property({ default: true }) - isActive?: boolean = true; - - @Property({ default: true, nullable: true }) - gender?: boolean; - - @Property({ nullable: true }) - avatarUrl?: string; - - @OneToMany(() => UserAddress, address => address.user, { - cascade: [Cascade.ALL], - orphanRemoval: true, - }) - addresses = new Collection(this); -} diff --git a/src/modules/users/entities/wallet-transaction.entity.ts b/src/modules/users/entities/wallet-transaction.entity.ts deleted file mode 100644 index 087f9a2..0000000 --- a/src/modules/users/entities/wallet-transaction.entity.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Entity, Index, Property, ManyToOne, Enum } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { User } from './user.entity'; -import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; - -@Entity({ tableName: 'wallet_transactions' }) -@Index({ properties: ['user', 'restaurant'] }) -@Index({ properties: ['user'] }) -@Index({ properties: ['restaurant'] }) -export class WalletTransaction extends BaseEntity { - @ManyToOne(() => User) - user!: User; - - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - amount!: number; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - balance!: number; - - @Enum(() => WalletTransactionType) - type!: WalletTransactionType; - - @Enum(() => WalletTransactionReason) - reason!: WalletTransactionReason; -} diff --git a/src/modules/users/interface/point.ts b/src/modules/users/interface/point.ts deleted file mode 100644 index fa8a1c5..0000000 --- a/src/modules/users/interface/point.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum PointTransactionType { - CREDIT = 'credit', - DEBIT = 'debit', -} - - -export enum PointTransactionReason { - ORDER_COMPLETED_DEPOSIT = 'order_completed_deposit', - CONVERT_SCORE_TO_POINT = 'convert_score_to_point', -} \ No newline at end of file diff --git a/src/modules/users/interface/wallet.ts b/src/modules/users/interface/wallet.ts deleted file mode 100644 index ad91490..0000000 --- a/src/modules/users/interface/wallet.ts +++ /dev/null @@ -1,13 +0,0 @@ -export enum WalletTransactionType { - CREDIT = 'credit', - DEBIT = 'debit', -} - - -export enum WalletTransactionReason { - ORDER_PAYMENT = 'order_payment', - ORDER_REFUND = 'order_refund', - CONVERT_SCORE_TO_WALLET = 'convert_score_to_wallet', - WITHDRAW = 'withdraw', - DEPOSIT = 'deposit', -} \ No newline at end of file diff --git a/src/modules/users/providers/user.service.ts b/src/modules/users/providers/user.service.ts deleted file mode 100644 index 308a824..0000000 --- a/src/modules/users/providers/user.service.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { FilterQuery, RequiredEntityData } from '@mikro-orm/core'; -import { User } from '../entities/user.entity'; -import { UserAddress } from '../entities/user-address.entity'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { UpdateUserDto } from '../dto/update-user.dto'; -import { FindUsersDto } from '../dto/find-user.dto'; -import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto'; -import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.dto'; -import { CreateUserAddressDto } from '../dto/create-user-address.dto'; -import { UpdateUserAddressDto } from '../dto/update-user-address.dto'; -import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; -import { UserRepository } from '../repositories/user.repository'; -import { normalizePhone } from '../../utils/phone.util'; -import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; -import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository'; -import { WalletService } from './wallet.service'; -import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet'; -import { WalletTransaction } from '../entities/wallet-transaction.entity'; - -@Injectable() -export class UserService { - constructor( - private readonly userRepository: UserRepository, - private readonly restaurantRepository: RestRepository, - private readonly walletTransactionRepository: WalletTransactionRepository, - private readonly walletService: WalletService, - private readonly em: EntityManager, - ) { } - - async findOrCreateByPhone(phone: string): Promise { - const normalizedPhone = normalizePhone(phone); - let user = await this.userRepository.findOne({ phone: normalizedPhone }); - - if (!user) { - // firstName is required on the entity; use phone as a sensible default - const _raw = { - phone: normalizedPhone, - firstName: '[نام]', - // keep dates undefined (no value) — cast through unknown to satisfy TS typing - birthDate: undefined, - marriageDate: undefined, - wallet: 0, - points: 0, - }; - - const createData = _raw as unknown as RequiredEntityData; - - user = this.userRepository.create(createData); - await this.em.persistAndFlush(user); - } - - return user; - } - - // async updateUser(userId: string, dto: UpdateUserDto): Promise { - // const user = await this.em.findOneOrFail(User, userId, {}).catch(() => { - // throw new NotFoundException(`User with ID ${userId} not found.`); - // }); - - // this.em.assign(user, { ...dto }); - - // await this.em.flush(); - - // return user; - // } - - async findByPhone(phone: string): Promise { - const normalizedPhone = normalizePhone(phone); - return this.userRepository.findOne({ phone: normalizedPhone }); - } - - async findById(id: string): Promise { - return this.userRepository.findOne({ id }); - } - // todo : transaction code here - async create(phone: string, restId: string): Promise { - const normalizedPhone = normalizePhone(phone); - const _raw = { - phone: normalizedPhone, - firstName: normalizedPhone, - birthDate: undefined, - marriageDate: undefined, - }; - // get registerscore from restaurant - const restaurant = await this.restaurantRepository.findOne({ id: restId }); - if (!restaurant) { - throw new NotFoundException(`Restaurant not found.`); - } - const createData = _raw as unknown as RequiredEntityData; - - const user = this.userRepository.create(createData); - - await this.em.persistAndFlush([user]); - return user; - } - - async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise { - const user = await this.userRepository.findOne({ id: userId }); - if (!user) { - throw new NotFoundException(`User with ID ${userId} not found.`); - } - - // Normalize date strings into Date objects if provided - const assignData: Partial = { ...dto } as Partial; - if (dto.birthDate) { - assignData.birthDate = new Date(dto.birthDate); - } - if (dto.marriageDate) { - assignData.marriageDate = new Date(dto.marriageDate); - } - // get restuarant - this.em.assign(user, assignData); - await this.em.flush(); - - return user; - } - - async findAll(restId: string, dto: FindUsersDto): Promise> { - const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto; - - // 1. Calculate pagination - const offset = (page - 1) * limit; - - // 2. Build the 'where' filter query - const where: FilterQuery = { - // orders: { - // restaurant: { - // id: restId, - // }, - // }, - }; - - // 4. Add 'search' logic (case-insensitive) - if (search) { - const searchPattern = `%${search}%`; - const normalizedSearch = normalizePhone(search); - const normalizedSearchPattern = `%${normalizedSearch}%`; - // $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default) - // Search with both original and normalized phone patterns to handle various input formats - // where.$or = [ - // { firstName: { $ilike: searchPattern } }, - // { lastName: { $ilike: searchPattern } }, - // { phone: { $ilike: searchPattern } }, - // ...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []), - // ]; - } - - // 5. Execute the query using findAndCount - const [users, total] = await this.userRepository.findAndCount(where, { - limit, - offset, - orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - }); - - // 6. Calculate total pages - const totalPages = Math.ceil(total / limit); - - // 7. Return the paginated result - return { - data: users, - meta: { - total, - page, - limit, - totalPages, - }, - }; - } - - async getUserAddresses(userId: string): Promise { - const user = await this.userRepository.findOne({ id: userId }, { populate: ['addresses'] }); - if (!user) { - throw new NotFoundException(`User with ID ${userId} not found.`); - } - - // Load addresses if not already loaded - await user.addresses.loadItems(); - return user.addresses.getItems(); - } - - async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise { - const user = await this.userRepository.findOne({ id: userId }); - if (!user) { - throw new NotFoundException(`User with ID ${userId} not found.`); - } - - // If setting as default, unset other default addresses - if (dto.isDefault) { - const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true }); - for (const address of existingAddresses) { - address.isDefault = false; - } - await this.em.flush(); - } - - // Create new address - const address = this.em.create(UserAddress, { - user, - title: dto.title, - address: dto.address, - city: dto.city, - province: dto.province, - postalCode: dto.postalCode, - latitude: dto.latitude, - longitude: dto.longitude, - phone: dto.phone, - isDefault: dto.isDefault ?? false, - }); - - await this.em.persistAndFlush(address); - return address; - } - - async getUserAddress(userId: string, addressId: string): Promise { - const address = await this.em.findOne(UserAddress, { - id: addressId, - user: { id: userId }, - }); - - if (!address) { - throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); - } - - return address; - } - - async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise { - const address = await this.em.findOne(UserAddress, { - id: addressId, - user: { id: userId }, - }); - - if (!address) { - throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); - } - - // If setting as default, unset other default addresses - if (dto.isDefault === true) { - const existingAddresses = await this.em.find(UserAddress, { - user: { id: userId }, - isDefault: true, - id: { $ne: addressId }, - }); - for (const existingAddress of existingAddresses) { - existingAddress.isDefault = false; - } - await this.em.flush(); - } - - // Update address fields - this.em.assign(address, dto); - await this.em.flush(); - - return address; - } - - async deleteUserAddress(userId: string, addressId: string): Promise { - const address = await this.em.findOne(UserAddress, { - id: addressId, - user: { id: userId }, - }); - - if (!address) { - throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); - } - - // Soft delete the address - address.deletedAt = new Date(); - await this.em.persistAndFlush(address); - } - - async setDefaultAddress(userId: string, addressId: string): Promise { - const address = await this.em.findOne(UserAddress, { - id: addressId, - user: { id: userId }, - }); - - if (!address) { - throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); - } - - // Unset all other default addresses - const existingAddresses = await this.em.find(UserAddress, { - user: { id: userId }, - isDefault: true, - id: { $ne: addressId }, - }); - for (const existingAddress of existingAddresses) { - existingAddress.isDefault = false; - } - - // Set this address as default - address.isDefault = true; - await this.em.flush(); - - return address; - } - - async convertScoreToWallet(userId: string, restId: string) { - return this.em.transactional(async em => { - const user = await em.findOne(User, { id: userId }); - if (!user) { - throw new NotFoundException(`User with ID ${userId} not found.`); - } - const restaurant = await em.findOne(Restaurant, { id: restId }); - if (!restaurant) { - throw new NotFoundException(`Restaurant with ID ${restId} not found.`); - } - let walletTransaction = await em.findOne(WalletTransaction, { user: { id: userId }, restaurant: { id: restId } }, - { orderBy: { createdAt: 'DESC' } }); - - if (!walletTransaction) { - walletTransaction = em.create(WalletTransaction, - { - user: user, - restaurant, - balance: 0, - amount: 0, - type: WalletTransactionType.CREDIT, - reason: WalletTransactionReason.DEPOSIT - }); - await em.persistAndFlush(walletTransaction); - } - // Determine how many points to convert - const pointsToConvert = walletTransaction.balance; - - // Validate points to convert - if (pointsToConvert <= 0) { - throw new BadRequestException('Points to convert must be greater than 0.'); - } - - - - await em.flush(); - - return user; - }); - } - - getUserWallet(userId: string, restId: string): Promise { - return this.walletTransactionRepository.findOne({ user: { id: userId }, restaurant: { id: restId } }, - { orderBy: { createdAt: 'DESC' } }); - } - - getUserWalletTransactions(userId: string, restId: string, dto: FindWalletTransactionsDto) { - return this.walletService.getUserWalletTransactions(userId, restId, dto); - } - - async createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) { - return this.em.transactional(async em => { - return this.walletService.createTransaction(em, userId, restId, dto); - }); - } - -} diff --git a/src/modules/users/providers/wallet.service.ts b/src/modules/users/providers/wallet.service.ts deleted file mode 100644 index 57da157..0000000 --- a/src/modules/users/providers/wallet.service.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { FilterQuery } from '@mikro-orm/core'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { WalletTransaction } from '../entities/wallet-transaction.entity'; -import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository'; -import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto'; -import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.dto'; -import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; -import { WalletTransactionType } from '../interface/wallet'; -import { PointTransactionRepository } from '../repositories/point-transaction.repository'; - -@Injectable() -export class WalletService { - constructor( - private readonly walletTransactionRepository: WalletTransactionRepository, - private readonly pointTransactionRepository: PointTransactionRepository, - ) {} - - async getUserWalletTransactions( - userId: string, - restId: string, - dto: FindWalletTransactionsDto, - ): Promise> { - const { - page = 1, - limit = 10, - type, - reason, - startDate, - endDate, - minAmount, - maxAmount, - orderBy = 'createdAt', - order = 'desc', - } = dto; - - // Calculate pagination - const offset = (page - 1) * limit; - - // Find the user's wallet for this restaurant - const walletTransaction = await this.walletTransactionRepository.findOne({ - user: { id: userId }, - restaurant: { id: restId }, - }); - - if (!walletTransaction) { - throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${restId}.`); - } - - // Build the 'where' filter query - const where: FilterQuery = { - user: { id: userId }, - restaurant: { id: restId }, - }; - - // Add type filter - if (type) { - where.type = type; - } - - // Add reason filter - if (reason) { - where.reason = reason; - } - - // Add date range filters - if (startDate || endDate) { - where.createdAt = {}; - if (startDate) { - where.createdAt.$gte = new Date(startDate); - } - if (endDate) { - where.createdAt.$lte = new Date(endDate); - } - } - - // Add amount range filters - if (minAmount !== undefined || maxAmount !== undefined) { - where.amount = {}; - if (minAmount !== undefined) { - where.amount.$gte = minAmount; - } - if (maxAmount !== undefined) { - where.amount.$lte = maxAmount; - } - } - - // Execute the query using findAndCount - const [transactions, total] = await this.walletTransactionRepository.findAndCount(where, { - limit, - offset, - orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - }); - - // Calculate total pages - const totalPages = Math.ceil(total / limit); - - // Return the paginated result - return { - data: transactions, - meta: { - total, - page, - limit, - totalPages, - }, - }; - } - - getUserWallet(userId: string, restId: string): Promise { - return this.walletTransactionRepository.findOne( - { - user: { id: userId }, - restaurant: { id: restId }, - }, - { orderBy: { createdAt: 'DESC' } }, - ); - } - - async createTransaction( - em: EntityManager, - userId: string, - restId: string, - dto: CreateWalletTransactionDto, - ): Promise { - const { amount, type, reason } = dto; - - // Validate amount - if (amount <= 0) { - throw new BadRequestException('Transaction amount must be greater than 0.'); - } - - // Find the user's wallet for this restaurant - const walletTransaction = await em.findOne(WalletTransaction, { - user: { id: userId }, - restaurant: { id: restId }, - }); - - if (!walletTransaction) { - throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${restId}.`); - } - - // Calculate new balance - const currentBalance = walletTransaction.balance || 0; - let newBalance: number; - - if (type === WalletTransactionType.CREDIT) { - newBalance = currentBalance + amount; - } else if (type === WalletTransactionType.DEBIT) { - if (currentBalance < amount) { - throw new BadRequestException(`Insufficient wallet balance. Current: ${currentBalance}, Required: ${amount}.`); - } - newBalance = currentBalance - amount; - } else { - throw new BadRequestException(`Invalid transaction type: ${type}`); - } - - // Update wallet balance - walletTransaction.balance = newBalance; - await em.persistAndFlush(walletTransaction); - - // Create transaction record - const transaction = em.create(WalletTransaction, { - user: walletTransaction.user, - restaurant: walletTransaction.restaurant, - amount, - type, - reason, - balance: newBalance, - }); - await em.persistAndFlush([walletTransaction, transaction]); - return transaction; - } - - async getUserCurrentWalletBalance(userId: string, restuarantId: string) { - const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restuarantId); - return { balance }; - } - async getUserCurrentPoinrBalance(userId: string, restuarantId: string) { - const balance = await this.pointTransactionRepository.getcurrentPointBalance(userId, restuarantId); - return { balance }; - } -} diff --git a/src/modules/users/repositories/point-transaction.repository.ts b/src/modules/users/repositories/point-transaction.repository.ts deleted file mode 100644 index bbdaa66..0000000 --- a/src/modules/users/repositories/point-transaction.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { PointTransaction } from '../entities/point-transaction.entity'; - -@Injectable() -export class PointTransactionRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, PointTransaction); - } - async getcurrentPointBalance(userId: string, restaurantId: string): Promise { - const pointTransaction = await this.em.findOne(PointTransaction, { - user: { id: userId }, - restaurant: { id: restaurantId }, - }); - return pointTransaction?.balance || 0; - } -} diff --git a/src/modules/users/repositories/user.repository.ts b/src/modules/users/repositories/user.repository.ts deleted file mode 100644 index 07516dc..0000000 --- a/src/modules/users/repositories/user.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { User } from '../entities/user.entity'; - -@Injectable() -export class UserRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, User); - } -} diff --git a/src/modules/users/repositories/wallet-transaction.repository.ts b/src/modules/users/repositories/wallet-transaction.repository.ts deleted file mode 100644 index a127509..0000000 --- a/src/modules/users/repositories/wallet-transaction.repository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { WalletTransaction } from '../entities/wallet-transaction.entity'; - -@Injectable() -export class WalletTransactionRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, WalletTransaction); - } - async getCurrentWalletBalance(userId: string, restaurantId: string): Promise { - const walletTransaction = await this.em.findOne( - WalletTransaction, - { - user: { id: userId }, - restaurant: { id: restaurantId }, - }, - { orderBy: { createdAt: 'desc' } }, - ); - return walletTransaction?.balance || 0; - } -} diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts deleted file mode 100644 index 61a74d1..0000000 --- a/src/modules/users/user.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserService } from './providers/user.service'; -import { UsersController } from './controllers/users.controller'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { User } from './entities/user.entity'; -import { UserAddress } from './entities/user-address.entity'; -import { JwtModule } from '@nestjs/jwt'; -import { UserRepository } from './repositories/user.repository'; -import { RestaurantsModule } from '../restaurants/restaurants.module'; -import { WalletTransactionRepository } from './repositories/wallet-transaction.repository'; -import { WalletService } from './providers/wallet.service'; -import { WalletTransaction } from './entities/wallet-transaction.entity'; -import { PointTransaction } from './entities/point-transaction.entity'; -import { PointTransactionRepository } from './repositories/point-transaction.repository'; - -@Module({ - providers: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository], - controllers: [UsersController], - imports: [ - MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction]), - JwtModule, - RestaurantsModule, - ], - exports: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository], -}) -export class UserModule {} diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index 77c774f..ea30bce 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -6,8 +6,7 @@ import { RolesSeeder } from './roles.seeder'; import { RestaurantsSeeder } from './restaurants.seeder'; import { FoodsSeeder } from './foods.seeder'; import { AdminsSeeder } from './admins.seeder'; -import { UsersSeeder } from './users.seeder'; - + export class DatabaseSeeder extends Seeder { async run(em: EntityManager) { const TOTAL_STEPS = 15; @@ -52,11 +51,7 @@ export class DatabaseSeeder extends Seeder { await adminsSeeder.run(em, rolesMap, restaurantsMap); console.info(`[Seeder] Step 12/${TOTAL_STEPS}: Done`); - // 13. Create Users - console.info(`[Seeder] Step 13/${TOTAL_STEPS}: Creating users`); - const usersSeeder = new UsersSeeder(); - await usersSeeder.run(em); - console.info(`[Seeder] Step 13/${TOTAL_STEPS}: Done`); + // 15. Create Notifications for admins and users for all restaurants diff --git a/src/seeders/data/notification-preferences.data.ts b/src/seeders/data/notification-preferences.data.ts deleted file mode 100644 index eb63bbf..0000000 --- a/src/seeders/data/notification-preferences.data.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NotifChannelEnum, NotifTitleEnum } from '../../modules/notifications/interfaces/notification.interface'; - -export interface NotificationPreferenceData { - title: NotifTitleEnum; - channels: NotifChannelEnum[]; -} - -export const notificationPreferencesData: NotificationPreferenceData[] = [ - { - title: NotifTitleEnum.PAGER_CREATED, - channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - }, - { - title: NotifTitleEnum.ORDER_CREATED, - channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - }, - { - title: NotifTitleEnum.PAYMENT_SUCCESS, - channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - }, - { - title: NotifTitleEnum.REVIEW_CREATED, - channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - }, - { - title: NotifTitleEnum.ORDER_STATUS_CHANGED, - channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - }, -]; diff --git a/src/seeders/data/user-addresses.data.ts b/src/seeders/data/user-addresses.data.ts deleted file mode 100644 index c5404da..0000000 --- a/src/seeders/data/user-addresses.data.ts +++ /dev/null @@ -1,63 +0,0 @@ -export interface UserAddressData { - userPhone: string; - title: string; - address: string; - city: string; - province?: string; - postalCode?: string; - latitude: number; - longitude: number; - phone?: string; - isDefault: boolean; -} - -export const userAddressesData: UserAddressData[] = [ - { - userPhone: '09362532122', - title: 'خانه', - address: 'خیابان ولیعصر، پلاک 123', - city: 'تهران', - province: 'تهران', - postalCode: '1234567890', - latitude: 35.6892, - longitude: 51.389, - phone: '09362532122', - isDefault: true, - }, - { - userPhone: '09185290775', - title: 'خانه', - address: 'خیابان انقلاب، پلاک 456', - city: 'تهران', - province: 'تهران', - postalCode: '1234567891', - latitude: 35.69, - longitude: 51.39, - phone: '09185290775', - isDefault: true, - }, - { - userPhone: '09129283395', - title: 'خانه', - address: 'خیابان آزادی، پلاک 789', - city: 'تهران', - province: 'تهران', - postalCode: '1234567892', - latitude: 35.691, - longitude: 51.391, - phone: '09129283395', - isDefault: true, - }, - { - userPhone: '09184317567', - title: 'خانه', - address: 'خیابان جمهوری، پلاک 321', - city: 'تهران', - province: 'تهران', - postalCode: '1234567893', - latitude: 35.692, - longitude: 51.392, - phone: '09184317567', - isDefault: true, - }, -]; diff --git a/src/seeders/data/users.data.ts b/src/seeders/data/users.data.ts deleted file mode 100644 index a135438..0000000 --- a/src/seeders/data/users.data.ts +++ /dev/null @@ -1,63 +0,0 @@ -export interface UserData { - phone: string; - firstName: string; - lastName: string; - restaurantSlug: string; - birthDate: string; - marriageDate: string; - isActive: boolean; - gender: boolean; - wallet: number; - points: number; -} - -export const usersData: UserData[] = [ - { - phone: '09362532122', - firstName: 'مرتضی', - lastName: 'مرتضایی', - restaurantSlug: 'zhivan', - birthDate: '1990-01-01', - marriageDate: '2015-01-01', - isActive: true, - gender: true, - wallet: 250000, - points: 100, - }, - { - phone: '09185290775', - firstName: 'حمید', - lastName: 'ضرغامی', - restaurantSlug: 'boote', - birthDate: '1992-01-01', - marriageDate: '2017-01-01', - isActive: true, - gender: true, - wallet: 250000, - points: 100, - }, - { - phone: '09129283395', - firstName: 'مهرداد', - lastName: 'مظفری', - restaurantSlug: 'zhivan', - birthDate: '1988-01-01', - marriageDate: '2014-01-01', - isActive: true, - gender: true, - wallet: 250000, - points: 100, - }, - { - phone: '09184317567', - firstName: 'مارال', - lastName: 'صادقی', - restaurantSlug: 'boote', - birthDate: '1995-01-01', - marriageDate: '2020-01-01', - isActive: true, - gender: false, - wallet: 250000, - points: 100, - }, -]; diff --git a/src/seeders/notification-preferences.seeder.ts b/src/seeders/notification-preferences.seeder.ts deleted file mode 100644 index 0791d7e..0000000 --- a/src/seeders/notification-preferences.seeder.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { NotificationPreference } from '../modules/notifications/entities/notification-preference.entity'; -import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; -import { notificationPreferencesData } from './data/notification-preferences.data'; - -export class NotificationPreferencesSeeder { - async run(em: EntityManager, restaurants: Restaurant[]): Promise { - for (const restaurant of restaurants) { - if (!restaurant) continue; - - for (const preferenceData of notificationPreferencesData) { - const existing = await em.findOne(NotificationPreference, { - restaurant: { id: restaurant.id }, - title: preferenceData.title, - }); - - if (!existing) { - const preference = em.create(NotificationPreference, { - restaurant, - title: preferenceData.title, - channels: preferenceData.channels, - }); - em.persist(preference); - } else { - // Update existing preference if notification type changed - if (existing.channels.length !== preferenceData.channels.length) { - existing.channels = preferenceData.channels; - em.persist(existing); - } - } - } - } - - await em.flush(); - } -} diff --git a/src/seeders/notifications.seeder.ts b/src/seeders/notifications.seeder.ts deleted file mode 100644 index 33b97e0..0000000 --- a/src/seeders/notifications.seeder.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { Notification } from '../modules/notifications/entities/notification.entity'; -import { Admin } from '../modules/admin/entities/admin.entity'; -import { User } from '../modules/users/entities/user.entity'; -import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; -import { NotifTitleEnum } from '../modules/notifications/interfaces/notification.interface'; - -export class NotificationsSeeder { - /** - * Seeds notifications for all admins and users for every restaurant. - * Ensures up to 33 notifications exist per (restaurant, admin) and (restaurant, user). - */ - async run(em: EntityManager): Promise { - const restaurants = await em.find(Restaurant, {}); - const admins = await em.find(Admin, {}); - const users = await em.find(User, {}); - - const titles = Object.values(NotifTitleEnum); - - for (const restaurant of restaurants) { - if (!restaurant) continue; - - // Admin notifications - for (const admin of admins) { - if (!admin) continue; - - const existingCount = await em.count(Notification, { - restaurant: { id: restaurant.id }, - admin: { id: admin.id }, - }); - - const toCreate = Math.max(0, 33 - (existingCount || 0)); - for (let i = 0; i < toCreate; i++) { - const notif = em.create(Notification, { - restaurant, - admin, - title: titles[i % titles.length] as NotifTitleEnum, - content: `Seeded notification ${i + 1} for admin ${admin.id} at ${restaurant.slug}`, - }); - em.persist(notif); - } - } - - // User notifications - for (const user of users) { - if (!user) continue; - - const existingCount = await em.count(Notification, { - restaurant: { id: restaurant.id }, - user: { id: user.id }, - }); - - const toCreate = Math.max(0, 33 - (existingCount || 0)); - for (let i = 0; i < toCreate; i++) { - const notif = em.create(Notification, { - restaurant, - user, - title: titles[i % titles.length] as NotifTitleEnum, - content: `Seeded notification ${i + 1} for user ${user.id} at ${restaurant.slug}`, - }); - em.persist(notif); - } - } - } - - await em.flush(); - } -} diff --git a/src/seeders/users.seeder.ts b/src/seeders/users.seeder.ts deleted file mode 100644 index 0cb66b2..0000000 --- a/src/seeders/users.seeder.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { User } from '../modules/users/entities/user.entity'; -import { UserAddress } from '../modules/users/entities/user-address.entity'; -import { usersData } from './data/users.data'; -import { userAddressesData } from './data/user-addresses.data'; -import { normalizePhone } from '../modules/utils/phone.util'; - -export class UsersSeeder { - async run(em: EntityManager): Promise> { - const usersMap = new Map(); - - // Create users - for (const userData of usersData) { - const normalizedPhone = normalizePhone(userData.phone); - let user = await em.findOne(User, { phone: normalizedPhone }); - if (!user) { - user = em.create(User, { - phone: normalizedPhone, // Use normalized phone - firstName: userData.firstName, - lastName: userData.lastName, - birthDate: new Date(userData.birthDate), - marriageDate: new Date(userData.marriageDate), - isActive: userData.isActive, - gender: userData.gender, - }); - em.persist(user); - } - usersMap.set(normalizedPhone, user); // Use normalized phone as key - } - - await em.flush(); - - // Create addresses for users - for (const addressData of userAddressesData) { - const normalizedPhone = normalizePhone(addressData.userPhone); - const user = usersMap.get(normalizedPhone); - if (!user) continue; - - // Check if address already exists for this user - const existingAddress = await em.findOne(UserAddress, { - user: { id: user.id }, - title: addressData.title, - address: addressData.address, - }); - - if (!existingAddress) { - // If setting as default, unset other default addresses for this user - if (addressData.isDefault) { - const existingDefaultAddresses = await em.find(UserAddress, { - user: { id: user.id }, - isDefault: true, - }); - for (const defaultAddress of existingDefaultAddresses) { - defaultAddress.isDefault = false; - em.persist(defaultAddress); - } - } - - const address = em.create(UserAddress, { - user, - title: addressData.title, - address: addressData.address, - city: addressData.city, - province: addressData.province, - postalCode: addressData.postalCode, - latitude: addressData.latitude, - longitude: addressData.longitude, - phone: addressData.phone, - isDefault: addressData.isDefault, - }); - em.persist(address); - } - } - - await em.flush(); - return usersMap; - } -}