From ecba2773be1127f944378bc15394c4dd844da0f3 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sun, 21 Dec 2025 12:34:45 +0330 Subject: [PATCH] notif cursor --- src/core/interceptors/response.interceptor.ts | 9 +- .../controllers/notifications.controller.ts | 129 ++++-------------- .../services/notification.service.ts | 50 +++++-- .../controllers/payments.controller.ts | 36 +---- .../payments/services/payments.service.ts | 3 - 5 files changed, 72 insertions(+), 155 deletions(-) diff --git a/src/core/interceptors/response.interceptor.ts b/src/core/interceptors/response.interceptor.ts index 5dc0d31..c6a6edc 100755 --- a/src/core/interceptors/response.interceptor.ts +++ b/src/core/interceptors/response.interceptor.ts @@ -5,7 +5,7 @@ import { map } from 'rxjs/operators'; @Injectable() export class ResponseInterceptor implements NestInterceptor { - constructor() {} + constructor() { } intercept(context: ExecutionContext, next: CallHandler>): Observable { // For REST endpoints, wrap the response @@ -35,11 +35,14 @@ export class ResponseInterceptor implements NestInterceptor { }; } - if (data && data.data !== undefined) { + if (data && 'data' in data) { return { statusCode, success: true, - data: data.data, + data: (data as any).data, + ...('nextCursor' in (data as any) && (data as any).nextCursor != null + ? { nextCursor: (data as any).nextCursor } + : {}), }; } return { diff --git a/src/modules/notifications/controllers/notifications.controller.ts b/src/modules/notifications/controllers/notifications.controller.ts index bbf6de2..dee91cd 100644 --- a/src/modules/notifications/controllers/notifications.controller.ts +++ b/src/modules/notifications/controllers/notifications.controller.ts @@ -16,7 +16,7 @@ export class NotificationsController { constructor( private readonly notificationService: NotificationService, private readonly preferenceService: NotificationPreferenceService, - ) {} + ) { } @UseGuards(AuthGuard) @ApiBearerAuth() @@ -24,7 +24,12 @@ export class NotificationsController { @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: '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, @@ -52,13 +57,29 @@ export class NotificationsController { return { message: 'Notification read successfully' }; } + /* ***************** Admin Endpoints ***************** */ + @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Get('admin/notifications') - @ApiOperation({ summary: 'Get Admin restaurant notifications' }) + @ApiOperation({ summary: 'Get Admin notifications' }) @ApiQuery({ name: 'limit', required: false, type: Number }) - async getAdminNotifications(@RestId() restaurantId: string, @Query('limit') limit?: number) { - return await this.notificationService.findByRestaurant(restaurantId, limit ? parseInt(limit.toString(), 30) : 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 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, parsedLimit, cursor, status); } @UseGuards(AdminAuthGuard) @@ -71,72 +92,6 @@ export class NotificationsController { return { message: 'Notification read successfully' }; } - // @UseGuards(AuthGuard) - // @ApiBearerAuth() - // @Get('public/notifications/:id') - // @ApiOperation({ summary: 'Get a notification by ID' }) - // @ApiParam({ name: 'id', description: 'Notification ID' }) - // async getUserNotification(@Param('id') id: string) { - // return this.notificationService.findOne(id); - // } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Post('admin/notifications/send') - // @ApiOperation({ summary: 'Send a notification (queued)' }) - // async sendNotification(@Body() dto: SendNotificationDto) { - // return this.notificationService.sendNotification(dto); - // } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Get('admin/notifications') - // @ApiOperation({ summary: 'Get notifications for a restaurant' }) - // @ApiQuery({ name: 'limit', required: false, type: Number }) - // @ApiQuery({ name: 'notificationType', required: false, type: String }) - // async getRestaurantNotifications( - // @RestId() restaurantId: string, - // @Query('limit') limit?: number, - // @Query('notificationType') notificationType?: string, - // ) { - // if (notificationType) { - // return this.notificationService.findByRestaurantAndType( - // restaurantId, - // notificationType, - // limit ? parseInt(limit.toString(), 10) : 50, - // ); - // } - // return this.notificationService.findByRestaurant(restaurantId, limit ? parseInt(limit.toString(), 10) : 50); - // } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Get('admin/notifications/:id') - // @ApiOperation({ summary: 'Get a notification by ID' }) - // @ApiParam({ name: 'id', description: 'Notification ID' }) - // async getNotification(@Param('id') id: string) { - // return this.notificationService.findOne(id); - // } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Post('admin/notifications/:id/retry') - // @ApiOperation({ summary: 'Retry a failed notification' }) - // @ApiParam({ name: 'id', description: 'Notification ID' }) - // async retryNotification(@Param('id') id: string) { - // await this.notificationService.retryFailedNotification(id); - // return { message: 'Notification queued for retry' }; - // } - - // Preference endpoints - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Post('admin/notification-preferences') - // @ApiOperation({ summary: 'Create notification preference' }) - // createPreference(@RestId() restaurantId: string, @Body() dto: CreatePreferenceDto) { - // return this.preferenceService.createOrUpdate(restaurantId, dto); - // } - @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Get('admin/notification-preferences') @@ -145,28 +100,6 @@ export class NotificationsController { return this.preferenceService.findByRestaurant(restaurantId); } - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Get('admin/notification-preferences/:type') - // @ApiOperation({ summary: 'Get a specific notification preference' }) - // @ApiParam({ name: 'type', description: 'Notification type' }) - // async getPreference(@RestId() restaurantId: string, @Param('type') notificationType: string) { - // return this.preferenceService.findByRestaurantAndType(restaurantId, notificationType); - // } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Patch('admin/notification-preferences/:type/enabled') - // @ApiOperation({ summary: 'Enable or disable a notification preference' }) - // @ApiParam({ name: 'type', description: 'Notification type' }) - // async updatePreferenceEnabled( - // @RestId() restaurantId: string, - // @Param('type') notificationType: string, - // @Body('enabled') enabled: boolean, - // ) { - // return this.preferenceService.updateEnabled(restaurantId, notificationType, enabled); - // } - @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Patch('admin/notification-preferences/:id') @@ -179,14 +112,4 @@ export class NotificationsController { ) { return this.preferenceService.updatePreference(preferenceId, restaurantId, dto); } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Delete('admin/notification-preferences/:id') - // @ApiOperation({ summary: 'Delete a notification preference' }) - // @ApiParam({ name: 'id', description: 'Notification preference ID' }) - // async deletePreference(@RestId() restaurantId: string, @Param('id') preferenceId: string) { - // await this.preferenceService.delete(restaurantId, preferenceId); - // return { message: 'Preference deleted successfully' }; - // } } diff --git a/src/modules/notifications/services/notification.service.ts b/src/modules/notifications/services/notification.service.ts index 034afee..2aa74f5 100644 --- a/src/modules/notifications/services/notification.service.ts +++ b/src/modules/notifications/services/notification.service.ts @@ -15,7 +15,7 @@ export class NotificationService { private readonly preferenceService: NotificationPreferenceService, private readonly queueService: NotificationQueueService, private readonly notificationGateway: NotificationsGateway, - ) {} + ) { } async sendNotification(params: NotifRequest): Promise { const { recipients, message, metadata, restaurantId } = params; @@ -96,16 +96,42 @@ export class NotificationService { return notification; } - async findByRestaurant(restaurantId: string, limit = 50): Promise { - return this.em.find( - Notification, - { restaurant: { id: restaurantId } }, - { - orderBy: { createdAt: 'DESC' }, - limit, - populate: ['user'], - }, - ); + async findByRestaurant( + restaurantId: string, + limit = 50, + cursor?: string, + status?: 'seen' | 'unseen', + ): Promise<{ data: Notification[]; nextCursor: string | null }> { + const where: FilterQuery = { + 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 + 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( @@ -144,7 +170,7 @@ export class NotificationService { return { data, - nextCursor, + nextCursor: nextCursor ?? null, }; } diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts index 1d9ca19..c706b44 100644 --- a/src/modules/payments/controllers/payments.controller.ts +++ b/src/modules/payments/controllers/payments.controller.ts @@ -24,7 +24,7 @@ export class PaymentsController { constructor( private readonly paymentsService: PaymentsService, private readonly paymentMethodService: PaymentMethodService, - ) {} + ) { } @UseGuards(AuthGuard) @ApiBearerAuth() @@ -63,40 +63,8 @@ export class PaymentsController { verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) { return this.paymentsService.verifyPayment(verifyPaymentDto.authority, verifyPaymentDto.orderId); } - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Get('admin/payments/:id') - // @ApiOperation({ summary: 'Get a payment by ID' }) - // @ApiParam({ name: 'id', type: 'number', description: 'Payment ID' }) - // @ApiOkResponse({ description: 'Payment found' }) - // @ApiNotFoundResponse({ description: 'Payment not found' }) - // findOnePayment(@Param('id') id: string) { - // return this.paymentsService.findById(+id); - // } - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Patch('admin/payments/:id') - // @ApiOperation({ summary: 'Update a payment' }) - // @ApiParam({ name: 'id', type: 'number', description: 'Payment ID' }) - // @ApiBody({ type: UpdatePaymentDto }) - // @ApiOkResponse({ description: 'Payment updated successfully' }) - // @ApiNotFoundResponse({ description: 'Payment not found' }) - // updatePayment(@Param('id') id: string, @Body() updatePaymentDto: UpdatePaymentDto) { - // return this.paymentsService.update(+id, updatePaymentDto); - // } - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @Delete('admin/payments/:id') - // @ApiOperation({ summary: 'Delete a payment' }) - // @ApiParam({ name: 'id', type: 'number', description: 'Payment ID' }) - // @ApiOkResponse({ description: 'Payment deleted successfully' }) - // @ApiNotFoundResponse({ description: 'Payment not found' }) - // removePayment(@Param('id') id: string) { - // return this.paymentsService.remove(+id); - // } - - /** payment methods */ + /** admin routes */ @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Get('admin/payments/methods') diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index bd12982..5640bdb 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -7,9 +7,7 @@ import { Logger } from '@nestjs/common'; import { GatewayManager } from '../gateways/gateway.manager'; import { UserWallet } from 'src/modules/users/entities/user-wallet.entity'; import { OrderPaymentContext } from '../interface/payment'; -import { InventoryService } from 'src/modules/inventory/inventory.service'; import { OrderStatus } from 'src/modules/orders/interface/order.interface'; -import { PaymentMethod } from '../entities/payment-method.entity'; @Injectable() export class PaymentsService { @@ -18,7 +16,6 @@ export class PaymentsService { constructor( private readonly em: EntityManager, private readonly gatewayManager: GatewayManager, - private readonly inventoryService: InventoryService, ) {} async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {