notif cursor

This commit is contained in:
2025-12-21 12:34:45 +03:30
parent c36ff7ed85
commit ecba2773be
5 changed files with 72 additions and 155 deletions
@@ -5,7 +5,7 @@ import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
constructor() {}
constructor() { }
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
// 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 {
@@ -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' };
// }
}
@@ -15,7 +15,7 @@ export class NotificationService {
private readonly preferenceService: NotificationPreferenceService,
private readonly queueService: NotificationQueueService,
private readonly notificationGateway: NotificationsGateway,
) {}
) { }
async sendNotification(params: NotifRequest): Promise<Notification[]> {
const { recipients, message, metadata, restaurantId } = params;
@@ -96,16 +96,42 @@ export class NotificationService {
return notification;
}
async findByRestaurant(restaurantId: string, limit = 50): Promise<Notification[]> {
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<Notification> = {
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,
};
}
@@ -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')
@@ -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 }> {