From 9f7c695ed1ae38eadddfe0a14964cf3e17d8f12f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 25 Dec 2025 11:15:51 +0330 Subject: [PATCH 01/12] review notification --- src/modules/review/events/review.events.ts | 13 ++++ .../review/listeners/review.listeners.ts | 77 +++++++++++++++++++ .../review/providers/review.service.ts | 22 +++++- src/modules/review/review.module.ts | 7 +- 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/modules/review/events/review.events.ts create mode 100644 src/modules/review/listeners/review.listeners.ts diff --git a/src/modules/review/events/review.events.ts b/src/modules/review/events/review.events.ts new file mode 100644 index 0000000..4d6f08d --- /dev/null +++ b/src/modules/review/events/review.events.ts @@ -0,0 +1,13 @@ +export class ReviewCreatedEvent { + constructor( + public readonly reviewId: string, + public readonly restaurantId: string, + public readonly userId: string, + public readonly foodId: string, + public readonly foodName: string, + public readonly orderId: string, + public readonly orderNumber: string | number, + public readonly rating: number, + ) {} +} + diff --git a/src/modules/review/listeners/review.listeners.ts b/src/modules/review/listeners/review.listeners.ts new file mode 100644 index 0000000..69c6a40 --- /dev/null +++ b/src/modules/review/listeners/review.listeners.ts @@ -0,0 +1,77 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReviewCreatedEvent } from '../events/review.events'; +import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; +import { Permission } from 'src/common/enums/permission.enum'; +import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface'; +import { NotificationService } from 'src/modules/notifications/services/notification.service'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class ReviewListeners { + private readonly logger = new Logger(ReviewListeners.name); + private reviewCreatedSmsTemplateId: string; + + constructor( + private readonly adminService: AdminRepository, + private readonly notificationService: NotificationService, + private readonly configService: ConfigService, + ) { + this.reviewCreatedSmsTemplateId = this.configService.get('SMS_PATTERN_REVIEW_CREATED') ?? '123'; + } + + @OnEvent(ReviewCreatedEvent.name) + async handleReviewCreated(event: ReviewCreatedEvent) { + try { + this.logger.log( + `Review created event received: ${event.reviewId} for restaurant: ${event.restaurantId}, food: ${event.foodName}, rating: ${event.rating}`, + ); + + // Get admins of restaurant that have review management permissions + const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_REVIEWS); + const recipients = admins.map(admin => ({ + adminId: admin.id, + })); + + if (recipients.length === 0) { + this.logger.warn(`No admins found with MANAGE_REVIEWS permission for restaurant ${event.restaurantId}`); + return; + } + + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + message: { + title: NotifTitleEnum.REVIEW_CREATED, + content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ${event.orderNumber} ثبت شد`, + sms: { + templateId: this.reviewCreatedSmsTemplateId, + parameters: { + foodName: event.foodName, + rating: event.rating.toString(), + orderNumber: String(event.orderNumber), + }, + }, + pushNotif: { + title: `نظر جدید`, + content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} ثبت شد`, + icon: `/`, + action: { + type: NotifTitleEnum.REVIEW_CREATED, + url: `/`, + }, + }, + }, + recipients, + metadata: { + priority: 1, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for review created event: ${event.reviewId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } +} + diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts index cff7811..71f97cb 100644 --- a/src/modules/review/providers/review.service.ts +++ b/src/modules/review/providers/review.service.ts @@ -13,6 +13,8 @@ import { Order } from '../../orders/entities/order.entity'; import { OrderItem } from '../../orders/entities/order-item.entity'; import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; import { ReviewStatus } from '../enums/review-status.enum'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ReviewCreatedEvent } from '../events/review.events'; @Injectable() export class ReviewService { @@ -21,6 +23,7 @@ export class ReviewService { private readonly foodRepository: FoodRepository, private readonly userRepository: UserRepository, private readonly em: EntityManager, + private readonly eventEmitter: EventEmitter2, ) { } async create(userId: string, createReviewDto: CreateReviewDto): Promise { @@ -36,8 +39,8 @@ export class ReviewService { throw new NotFoundException(UserMessage.USER_NOT_FOUND); } - // Find order and verify it belongs to the user - const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }); + // Find order and verify it belongs to the user, populate restaurant to get restaurantId + const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }, { populate: ['restaurant'] }); if (!order) { throw new NotFoundException('Order not found or does not belong to the current user'); } @@ -83,6 +86,21 @@ export class ReviewService { // Update food rating average await this.updateFoodRating(foodId); + // Emit ReviewCreatedEvent + this.eventEmitter.emit( + ReviewCreatedEvent.name, + new ReviewCreatedEvent( + review.id, + order.restaurant.id, + userId, + foodId, + food.title || 'Unknown Food', + orderId, + order.orderNumber || '', + rating, + ), + ); + return review; } diff --git a/src/modules/review/review.module.ts b/src/modules/review/review.module.ts index 7596d6e..c76b69d 100644 --- a/src/modules/review/review.module.ts +++ b/src/modules/review/review.module.ts @@ -9,11 +9,14 @@ import { FoodModule } from '../foods/food.module'; import { UserModule } from '../users/user.module'; import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '@nestjs/jwt'; +import { ReviewListeners } from './listeners/review.listeners'; +import { AdminModule } from '../admin/admin.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule], + imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule, AdminModule, NotificationsModule], controllers: [ReviewController], - providers: [ReviewService, ReviewRepository, FoodRatingCronService], + providers: [ReviewService, ReviewRepository, FoodRatingCronService, ReviewListeners], exports: [ReviewRepository], }) export class ReviewModule {} From b18f9b022235cde376994acfdb368a2ed95a2093 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 25 Dec 2025 11:20:38 +0330 Subject: [PATCH 02/12] sanitize review input --- package-lock.json | 185 +++++++++++++++++- package.json | 2 + .../review/providers/review.service.ts | 12 +- src/modules/utils/sanitize.util.ts | 20 ++ 4 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 src/modules/utils/sanitize.util.ts diff --git a/package-lock.json b/package-lock.json index 06f5977..b45adcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "firebase-admin": "^13.6.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "sanitize-html": "^2.17.0", "socket.io": "^4.8.1", "ulid": "^3.0.1", "uuid": "^13.0.0" @@ -63,6 +64,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/sanitize-html": "^2.16.0", "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", "eslint": "^9.18.0", @@ -6476,6 +6478,16 @@ "node": ">= 0.6" } }, + "node_modules/@types/sanitize-html": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz", + "integrity": "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", @@ -8712,7 +8724,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8837,6 +8848,61 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -9081,6 +9147,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -9162,7 +9240,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10786,6 +10863,25 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -11113,6 +11209,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -13011,6 +13116,24 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -13377,6 +13500,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13594,7 +13723,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -13745,6 +13873,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", @@ -14371,6 +14527,20 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-html": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", + "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -14782,6 +14952,15 @@ "node": ">= 8" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", diff --git a/package.json b/package.json index d4415cd..2cc77bc 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "firebase-admin": "^13.6.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "sanitize-html": "^2.17.0", "socket.io": "^4.8.1", "ulid": "^3.0.1", "uuid": "^13.0.0" @@ -79,6 +80,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/sanitize-html": "^2.16.0", "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", "eslint": "^9.18.0", diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts index 71f97cb..f498176 100644 --- a/src/modules/review/providers/review.service.ts +++ b/src/modules/review/providers/review.service.ts @@ -15,6 +15,7 @@ import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; import { ReviewStatus } from '../enums/review-status.enum'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { ReviewCreatedEvent } from '../events/review.events'; +import { sanitizeText } from '../../utils/sanitize.util'; @Injectable() export class ReviewService { @@ -70,7 +71,7 @@ export class ReviewService { food, user, rating, - comment: comment || undefined, + comment: sanitizeText(comment) || undefined, positivePoints: positivePoints || undefined, negativePoints: negativePoints || undefined, status: ReviewStatus.PENDING, @@ -145,7 +146,14 @@ export class ReviewService { } const oldRating = review.rating; - this.em.assign(review, dto); + + // Sanitize comment if provided + const sanitizedDto = { + ...dto, + comment: dto.comment !== undefined ? sanitizeText(dto.comment) || undefined : undefined, + }; + + this.em.assign(review, sanitizedDto); await this.em.persistAndFlush(review); // Update food rating average if rating changed diff --git a/src/modules/utils/sanitize.util.ts b/src/modules/utils/sanitize.util.ts new file mode 100644 index 0000000..bb5a7d1 --- /dev/null +++ b/src/modules/utils/sanitize.util.ts @@ -0,0 +1,20 @@ +import sanitizeHtml from 'sanitize-html'; + +/** + * Sanitizes user input to prevent XSS attacks and remove malicious content + * Allows only safe text content, removing all HTML tags and scripts + * @param input - The string to sanitize + * @returns Sanitized string with all HTML tags and scripts removed + */ +export function sanitizeText(input: string | undefined | null): string | undefined | null { + if (!input) { + return input; + } + + return sanitizeHtml(input, { + allowedTags: [], // No HTML tags allowed + allowedAttributes: {}, // No attributes allowed + allowedSchemes: [], // No URL schemes allowed + }).trim(); +} + From b78eeb14ace2bf4799bb819cbbe018f31077a867 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 25 Dec 2025 11:24:02 +0330 Subject: [PATCH 03/12] get restaurant by subscription id --- .../restaurants/controllers/restaurants.controller.ts | 9 +++++++++ src/modules/restaurants/providers/restaurants.service.ts | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/modules/restaurants/controllers/restaurants.controller.ts b/src/modules/restaurants/controllers/restaurants.controller.ts index 48d12c1..2fc94e9 100644 --- a/src/modules/restaurants/controllers/restaurants.controller.ts +++ b/src/modules/restaurants/controllers/restaurants.controller.ts @@ -80,5 +80,14 @@ export class RestaurantsController { return this.restaurantsService.create(createRestaurantDto); } + @UseGuards(SuperAdminAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Get restaurant by subscription ID' }) + @ApiParam({ name: 'subscriptionId', required: true, description: 'Subscription ID' }) + @Get('super-admin/restaurants/subscription/:subscriptionId') + findOneBySubscriptionId(@Param('subscriptionId') subscriptionId: string) { + return this.restaurantsService.findOneBySubscriptionId(subscriptionId); + } + } diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index 697779b..b1e65f2 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -64,6 +64,14 @@ export class RestaurantsService { return restaurant; } + async findOneBySubscriptionId(subscriptionId: string): Promise { + const restaurant = await this.restRepository.findOne({ subscriptionId }); + if (!restaurant) { + throw new NotFoundException(RestMessage.NOT_FOUND); + } + return restaurant; + } + async getRestaurantSpecification(slug: string) { const restaurant = await this.findBySlug(slug); return restaurant; From f91b5b871009f99056d8292bde0199583e5c79bd Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 25 Dec 2025 18:54:06 +0330 Subject: [PATCH 04/12] fix bug pager created notification --- PAGER_SOCKET_TEST.md | 148 ------------------ .../pager/listeners/notification.listeners.ts | 10 +- 2 files changed, 7 insertions(+), 151 deletions(-) delete mode 100644 PAGER_SOCKET_TEST.md diff --git a/PAGER_SOCKET_TEST.md b/PAGER_SOCKET_TEST.md deleted file mode 100644 index eaa1b0d..0000000 --- a/PAGER_SOCKET_TEST.md +++ /dev/null @@ -1,148 +0,0 @@ -# Pager Socket.IO Testing Guide - -## Quick Start - -1. **Start your NestJS server:** - ```bash - npm run start:dev - ``` - -2. **Open the test page:** - - Open `test-pager-socket.html` in your web browser - - Or serve it via a simple HTTP server: - ```bash - # Using Python - python3 -m http.server 8080 - # Then open http://localhost:8080/test-pager-socket.html - - # Using Node.js (if you have http-server installed) - npx http-server -p 8080 - ``` - -## Testing Scenarios - -### Scenario 1: Test as Restaurant Admin/Staff - -1. **Get your admin JWT token:** - - Login as admin via your API to get the JWT token - - Example: - ```bash - curl -X POST http://localhost:4000/admin/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "email": "admin@example.com", - "password": "password" - }' - ``` - - Copy the `accessToken` from the response - -2. **Connect to the server:** - - Enter server URL: `http://localhost:4000` - - Enter your admin JWT token in the "Admin Token" field - - Click "Connect" - -3. **Join restaurant room:** - - Select "Restaurant (Admin/Staff)" from User Type - - Click "Join Room" - - The restaurant ID will be automatically extracted from your token - -3. **Create a pager request (via API):** - ```bash - curl -X POST http://localhost:4000/public/pager \ - -H "Content-Type: application/json" \ - -H "X-Slug: your-restaurant-slug" \ - -d '{ - "tableNumber": "5", - "message": "Need assistance" - }' - ``` - -4. **Observe the event:** - - You should see `pager:created` event in the Events Log - -### Scenario 2: Test as Public User - -1. **Create a pager request first (to get cookie ID):** - ```bash - curl -X POST http://localhost:4000/public/pager \ - -H "Content-Type: application/json" \ - -H "X-Slug: your-restaurant-slug" \ - -d '{ - "tableNumber": "3", - "message": "Need water" - }' \ - -c cookies.txt - ``` - -2. **Get the cookie ID:** - - Check the response or cookies.txt file for `pagerId` cookie value - -3. **Connect and join session:** - - Connect to the server - - Select "Session (Public User)" from User Type - - Enter the cookie ID from step 2 - - Click "Join Room" - -4. **Update pager status (via API as admin):** - ```bash - curl -X PATCH http://localhost:4000/admin/pager/{pagerId}/status \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ - -d '{ - "status": "acknowledged" - }' - ``` - -5. **Observe the event:** - - You should see `pager:status:updated` event in the Events Log - -## Socket Events Reference - -### Client → Server Events - -| Event | Payload | Description | -|-------|---------|-------------| -| `join:restaurant` | `{}` (empty, restId extracted from token) | Join restaurant room for admin/staff. **Requires authentication via JWT token in connection auth** | -| `join:session` | `{ cookieId: string }` | Join session room for public user | -| `leave:room` | `{ room: string }` | Leave a specific room | - -**Note:** For `join:restaurant`, the restaurant ID is automatically extracted from the authenticated admin's JWT token. You must provide the token when connecting: - -```javascript -const socket = io('http://localhost:4000/pager', { - auth: { - token: 'your-jwt-token-here' // or 'Bearer your-jwt-token-here' - } -}); -``` - -### Server → Client Events - -| Event | Payload | Description | -|-------|---------|-------------| -| `pager:created` | `{ pager: {...} }` | New pager request created | -| `pager:status:updated` | `{ pager: {...} }` | Pager status updated | -| `joined` | `{ room: string, message: string }` | Successfully joined a room | -| `left` | `{ room: string, message: string }` | Successfully left a room | -| `error` | `{ message: string }` | Error occurred | - -## Room Names - -- **Restaurant Room:** `restaurant:{restId}` -- **Session Room:** `cookie:{cookieId}` - -## Testing Tips - -1. **Open multiple browser tabs/windows** to simulate multiple clients -2. **Use browser DevTools** to inspect WebSocket connections -3. **Check server logs** to see gateway activity -4. **Test reconnection** by disconnecting and reconnecting -5. **Test room isolation** by joining different rooms in different tabs - -## Troubleshooting - -- **Connection fails:** Check if server is running on the correct port -- **No events received:** Verify you've joined the correct room -- **CORS errors:** Ensure CORS is enabled in your NestJS app (already configured in gateway) -- **Events not showing:** Check browser console for errors - diff --git a/src/modules/pager/listeners/notification.listeners.ts b/src/modules/pager/listeners/notification.listeners.ts index 1ae8bfd..58921db 100644 --- a/src/modules/pager/listeners/notification.listeners.ts +++ b/src/modules/pager/listeners/notification.listeners.ts @@ -5,15 +5,19 @@ import { PagerCreatedEvent } from '../events/pager.events'; import { Permission } from 'src/common/enums/permission.enum'; import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface'; import { NotificationService } from 'src/modules/notifications/services/notification.service'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class PagerListeners { private readonly logger = new Logger(PagerListeners.name); - + private readonly smsPatternPagerCreated: string constructor( private readonly adminService: AdminRepository, private readonly notificationService: NotificationService, - ) {} + private readonly configService: ConfigService, + ) { + this.smsPatternPagerCreated = this.configService.get('SMS_PATTERN_PAGER_CREATED') ?? '123'; + } @OnEvent(PagerCreatedEvent.name) async handlePagerCreated(event: PagerCreatedEvent) { @@ -30,7 +34,7 @@ export class PagerListeners { title: NotifTitleEnum.PAGER_CREATED, content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`, sms: { - templateId: '1234567890', + templateId: this.smsPatternPagerCreated, parameters: { tableNumber: event.tableNumber.toString(), }, From a85ebaada46cc73c12b39202cf9375a84018f21c Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 26 Dec 2025 19:19:25 +0330 Subject: [PATCH 05/12] add food title if not exist --- src/modules/cart/providers/cart-validation.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/cart/providers/cart-validation.service.ts b/src/modules/cart/providers/cart-validation.service.ts index 7d3aa69..3a7a9c1 100644 --- a/src/modules/cart/providers/cart-validation.service.ts +++ b/src/modules/cart/providers/cart-validation.service.ts @@ -46,7 +46,7 @@ export class CartValidationService { validateStock(food: Food, quantity: number): void { const availableStock = food.inventory!.availableStock; if (availableStock < quantity) { - throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK); + throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK + food.title); } } From 2b1f8df6b9cd2be2b39eff19104bb9a17ca8d3a2 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 26 Dec 2025 19:32:06 +0330 Subject: [PATCH 06/12] subscription start and end date --- .../payments/controllers/payments.controller.ts | 2 +- src/modules/payments/services/payments.service.ts | 2 +- src/modules/restaurants/dto/create-restaurant.dto.ts | 10 +++++++++- src/modules/restaurants/entities/restaurant.entity.ts | 7 +++++++ .../restaurants/providers/restaurants.service.ts | 2 ++ src/seeders/restaurants.seeder.ts | 6 +++++- 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts index 004a38c..5a03d47 100644 --- a/src/modules/payments/controllers/payments.controller.ts +++ b/src/modules/payments/controllers/payments.controller.ts @@ -45,7 +45,7 @@ export class PaymentsController { @ApiOperation({ summary: 'Get all the restaurant payments for user' }) @ApiHeader(API_HEADER_SLUG) findAllByRestaurant(@UserId() userId: string, @RestId() restId: string) { - return this.paymentsService.findAllByRestaurantId(restId, userId); + return this.paymentsService.findAllPaymentsByRestaurantId(restId, userId); } @UseGuards(AuthGuard) diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index e6a2a48..d5f0e06 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -219,7 +219,7 @@ export class PaymentsService { return payment; } - findAllByRestaurantId(restId: string, userId: string) { + findAllPaymentsByRestaurantId(restId: string, userId: string) { return this.em.find( Payment, { order: { restaurant: { id: restId }, user: { id: userId } } }, diff --git a/src/modules/restaurants/dto/create-restaurant.dto.ts b/src/modules/restaurants/dto/create-restaurant.dto.ts index e6432c7..6117347 100644 --- a/src/modules/restaurants/dto/create-restaurant.dto.ts +++ b/src/modules/restaurants/dto/create-restaurant.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsOptional, IsNumber } from 'class-validator'; +import { IsString, IsOptional, IsNumber, IsDate } from 'class-validator'; export class CreateRestaurantDto { @ApiProperty({ example: 'کافه ژیوان', description: 'نام رستوران' }) @@ -24,4 +24,12 @@ export class CreateRestaurantDto { @IsString() subscriptionId!: string; + @ApiProperty({ example: '2025-12-26', description: 'تاریخ انقضای اشتراک' }) + @IsDate() + subscriptionEndDate!: Date; + + @ApiProperty({ example: '2025-12-26', description: 'تاریخ شروع اشتراک' }) + @IsDate() + subscriptionStartDate!: Date; + } diff --git a/src/modules/restaurants/entities/restaurant.entity.ts b/src/modules/restaurants/entities/restaurant.entity.ts index a890129..f738be8 100644 --- a/src/modules/restaurants/entities/restaurant.entity.ts +++ b/src/modules/restaurants/entities/restaurant.entity.ts @@ -102,4 +102,11 @@ export class Restaurant extends BaseEntity { @Property({unique: true}) subscriptionId!: string; + + @Property() + subscriptionEndDate!: Date; + + @Property() + subscriptionStartDate!: Date; + } diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index b1e65f2..4911c2a 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -26,6 +26,8 @@ export class RestaurantsService { plan: PlanEnum.Base, domain: `https://dmenu-plus-front.dev.danakcorp.com/${dto.slug}`, subscriptionId: dto.subscriptionId, + subscriptionEndDate: dto.subscriptionEndDate, + subscriptionStartDate: dto.subscriptionStartDate, }); await this.em.persistAndFlush(restaurant); diff --git a/src/seeders/restaurants.seeder.ts b/src/seeders/restaurants.seeder.ts index 467e2ba..c05a6cc 100644 --- a/src/seeders/restaurants.seeder.ts +++ b/src/seeders/restaurants.seeder.ts @@ -9,7 +9,11 @@ export class RestaurantsSeeder { for (const restaurantData of restaurantsData) { let restaurant = await em.findOne(Restaurant, { slug: restaurantData.slug }); if (!restaurant) { - restaurant = em.create(Restaurant, restaurantData); + restaurant = em.create(Restaurant, { + ...restaurantData, + subscriptionEndDate: new Date('2025-12-26'), + subscriptionStartDate: new Date('2026-12-26'), + }); em.persist(restaurant); } restaurantsMap.set(restaurantData.slug, restaurant); From 9a938bf8cdd52b1d63cc7e6832f2fb57fbc9e392 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 26 Dec 2025 19:36:29 +0330 Subject: [PATCH 07/12] cron to inactivate expired subscriptions --- .../restaurants/crone/restaurant.crone.ts | 50 +++++++++++++++++++ src/modules/restaurants/restaurants.module.ts | 3 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/modules/restaurants/crone/restaurant.crone.ts diff --git a/src/modules/restaurants/crone/restaurant.crone.ts b/src/modules/restaurants/crone/restaurant.crone.ts new file mode 100644 index 0000000..b521563 --- /dev/null +++ b/src/modules/restaurants/crone/restaurant.crone.ts @@ -0,0 +1,50 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { Restaurant } from '../entities/restaurant.entity'; + +@Injectable() +export class RestaurantCrone { + private readonly logger = new Logger(RestaurantCrone.name); + + constructor(private readonly em: EntityManager) { } + + // run every day at 03:00 (3:00 AM) + @Cron('0 3 * * *', { + name: 'deactivateExpiredSubscriptions', + timeZone: 'UTC', + }) + async handleCron() { + try { + this.logger.debug('Starting daily subscription expiration check'); + + // Get current date + const now = new Date(); + + // Find restaurants where subscription has expired and they're still active + const expiredRestaurants = await this.em.find(Restaurant, { + subscriptionEndDate: { $lt: now }, + isActive: true, + }); + + if (!expiredRestaurants || expiredRestaurants.length === 0) { + this.logger.debug('No restaurants with expired subscriptions found'); + return; + } + + this.logger.log(`Found ${expiredRestaurants.length} restaurants with expired subscriptions`); + + // Deactivate expired restaurants + for (const restaurant of expiredRestaurants) { + restaurant.isActive = false; + } + + // Persist all changes + await this.em.persistAndFlush(expiredRestaurants); + + this.logger.log(`Successfully deactivated ${expiredRestaurants.length} restaurants with expired subscriptions`); + } catch (err) { + this.logger.error(`RestaurantCrone failed: ${err?.message}`, err); + } + } +} diff --git a/src/modules/restaurants/restaurants.module.ts b/src/modules/restaurants/restaurants.module.ts index b9d9121..62a5bd4 100644 --- a/src/modules/restaurants/restaurants.module.ts +++ b/src/modules/restaurants/restaurants.module.ts @@ -8,12 +8,13 @@ import { ScheduleRepository } from './repositories/schedule.repository'; import { Schedule } from './entities/schedule.entity'; import { ScheduleService } from './providers/schedule.service'; import { ScheduleController } from './controllers/schedule.controller'; +import { RestaurantCrone } from './crone/restaurant.crone'; import { JwtModule } from '@nestjs/jwt'; import { AuthModule } from '../auth/auth.module'; @Module({ controllers: [RestaurantsController, ScheduleController], - providers: [RestaurantsService, RestRepository, ScheduleRepository, ScheduleService], + providers: [RestaurantsService, RestRepository, ScheduleRepository, ScheduleService, RestaurantCrone], imports: [MikroOrmModule.forFeature([Restaurant, Schedule]), JwtModule, forwardRef(() => AuthModule)], exports: [RestRepository, ScheduleRepository, ScheduleService], }) From 21da5d52f820e471400ce990eef48824cf64990d Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 26 Dec 2025 22:34:40 +0330 Subject: [PATCH 08/12] fix bug in create rest --- .../restaurants/controllers/restaurants.controller.ts | 1 + src/modules/restaurants/dto/create-restaurant.dto.ts | 7 +++++-- .../restaurants/providers/restaurants.service.ts | 10 +++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/modules/restaurants/controllers/restaurants.controller.ts b/src/modules/restaurants/controllers/restaurants.controller.ts index 2fc94e9..4687119 100644 --- a/src/modules/restaurants/controllers/restaurants.controller.ts +++ b/src/modules/restaurants/controllers/restaurants.controller.ts @@ -77,6 +77,7 @@ export class RestaurantsController { @ApiOperation({ summary: 'Create a new restaurant' }) @Post('super-admin/restaurants') createRestaurant(@Body() createRestaurantDto: CreateRestaurantDto) { + console.log('create rest') return this.restaurantsService.create(createRestaurantDto); } diff --git a/src/modules/restaurants/dto/create-restaurant.dto.ts b/src/modules/restaurants/dto/create-restaurant.dto.ts index 6117347..50f1f9d 100644 --- a/src/modules/restaurants/dto/create-restaurant.dto.ts +++ b/src/modules/restaurants/dto/create-restaurant.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString, IsOptional, IsNumber, IsDate } from 'class-validator'; +import { Type } from 'class-transformer'; export class CreateRestaurantDto { @ApiProperty({ example: 'کافه ژیوان', description: 'نام رستوران' }) @@ -13,22 +14,24 @@ export class CreateRestaurantDto { @ApiPropertyOptional({ example: 2020, description: 'سال تأسیس' }) @IsOptional() @IsNumber() - establishedYear?: number; + establishedYear: number; @ApiPropertyOptional({ example: '09123456789', description: 'شماره تلفن' }) @IsOptional() @IsString() - phone?: string; + phone: string; @ApiProperty({ example: 'sub_1234567890', description: 'شناسه اشتراک' }) @IsString() subscriptionId!: string; @ApiProperty({ example: '2025-12-26', description: 'تاریخ انقضای اشتراک' }) + @Type(() => Date) @IsDate() subscriptionEndDate!: Date; @ApiProperty({ example: '2025-12-26', description: 'تاریخ شروع اشتراک' }) + @Type(() => Date) @IsDate() subscriptionStartDate!: Date; diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index 4911c2a..924f2c8 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { CreateRestaurantDto } from '../dto/create-restaurant.dto'; import { UpdateRestaurantDto } from '../dto/update-restaurant.dto'; import { Restaurant } from '../entities/restaurant.entity'; @@ -17,6 +17,14 @@ export class RestaurantsService { ) { } async create(dto: CreateRestaurantDto): Promise { + const validateSlug = await this.em.findOne(Restaurant, { slug: dto.slug }) + if (validateSlug) { + throw new BadRequestException("Duplicate slug: " + dto.slug) + } + const validateSubscriptionId = await this.em.findOne(Restaurant, { subscriptionId: dto.subscriptionId }) + if (validateSubscriptionId) { + throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId) + } const restaurant = this.em.create(Restaurant, { name: dto.name, slug: dto.slug, From c8e00f9e1a266d72c7a11d5031586b3de66ff495 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 27 Dec 2025 10:40:15 +0330 Subject: [PATCH 09/12] setup restuarant --- .../controllers/restaurants.controller.ts | 3 +- .../restaurants/dto/create-restaurant.dto.ts | 7 +- .../providers/restaurants.service.ts | 82 +++++++++++++------ src/seeders/data/admins.data.ts | 6 +- src/seeders/data/restaurants.data.ts | 6 +- src/seeders/data/roles.data.ts | 13 ++- 6 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/modules/restaurants/controllers/restaurants.controller.ts b/src/modules/restaurants/controllers/restaurants.controller.ts index 4687119..27d684a 100644 --- a/src/modules/restaurants/controllers/restaurants.controller.ts +++ b/src/modules/restaurants/controllers/restaurants.controller.ts @@ -77,8 +77,7 @@ export class RestaurantsController { @ApiOperation({ summary: 'Create a new restaurant' }) @Post('super-admin/restaurants') createRestaurant(@Body() createRestaurantDto: CreateRestaurantDto) { - console.log('create rest') - return this.restaurantsService.create(createRestaurantDto); + return this.restaurantsService.setupRestuarant(createRestaurantDto); } @UseGuards(SuperAdminAuthGuard) diff --git a/src/modules/restaurants/dto/create-restaurant.dto.ts b/src/modules/restaurants/dto/create-restaurant.dto.ts index 50f1f9d..aeace68 100644 --- a/src/modules/restaurants/dto/create-restaurant.dto.ts +++ b/src/modules/restaurants/dto/create-restaurant.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsOptional, IsNumber, IsDate } from 'class-validator'; +import { IsString, IsOptional, IsNumber, IsDate, IsEnum } from 'class-validator'; import { Type } from 'class-transformer'; +import { PlanEnum } from '../interface/plan.interface'; export class CreateRestaurantDto { @ApiProperty({ example: 'کافه ژیوان', description: 'نام رستوران' }) @@ -21,6 +22,10 @@ export class CreateRestaurantDto { @IsString() phone: string; + @ApiProperty({ example: 'base', enum: PlanEnum, description: 'پلن رستوران' }) + @IsEnum(PlanEnum) + plan!: PlanEnum; + @ApiProperty({ example: 'sub_1234567890', description: 'شناسه اشتراک' }) @IsString() subscriptionId!: string; diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index 924f2c8..1c36308 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -8,6 +8,9 @@ import { RestMessage } from 'src/common/enums/message.enum'; import { FindRestaurantsDto } from '../dto/find-restaurants.dto'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { PlanEnum } from '../interface/plan.interface'; +import { Admin } from '../../admin/entities/admin.entity'; +import { AdminRole } from '../../admin/entities/adminRole.entity'; +import { Role } from '../../roles/entities/role.entity'; @Injectable() export class RestaurantsService { @@ -16,30 +19,63 @@ export class RestaurantsService { private readonly restRepository: RestRepository, ) { } - async create(dto: CreateRestaurantDto): Promise { - const validateSlug = await this.em.findOne(Restaurant, { slug: dto.slug }) - if (validateSlug) { - throw new BadRequestException("Duplicate slug: " + dto.slug) - } - const validateSubscriptionId = await this.em.findOne(Restaurant, { subscriptionId: dto.subscriptionId }) - if (validateSubscriptionId) { - throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId) - } - const restaurant = this.em.create(Restaurant, { - name: dto.name, - slug: dto.slug, - establishedYear: dto.establishedYear, - phone: dto.phone, - isActive: true, - plan: PlanEnum.Base, - domain: `https://dmenu-plus-front.dev.danakcorp.com/${dto.slug}`, - subscriptionId: dto.subscriptionId, - subscriptionEndDate: dto.subscriptionEndDate, - subscriptionStartDate: dto.subscriptionStartDate, - }); + async setupRestuarant(dto: CreateRestaurantDto): Promise { + return await this.em.transactional(async (em) => { + // Validate unique constraints + const validateSlug = await em.findOne(Restaurant, { slug: dto.slug }) + if (validateSlug) { + throw new BadRequestException("Duplicate slug: " + dto.slug) + } + const validateSubscriptionId = await em.findOne(Restaurant, { subscriptionId: dto.subscriptionId }) + if (validateSubscriptionId) { + throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId) + } - await this.em.persistAndFlush(restaurant); - return restaurant; + // Create restaurant + const restaurant = em.create(Restaurant, { + name: dto.name, + slug: dto.slug, + establishedYear: dto.establishedYear, + phone: dto.phone, + isActive: true, + plan: dto.plan, + domain: `https://dmenu-plus-front.dev.danakcorp.com/${dto.slug}`, + subscriptionId: dto.subscriptionId, + subscriptionEndDate: dto.subscriptionEndDate, + subscriptionStartDate: dto.subscriptionStartDate, + }); + + // Find the appropriate role based on plan + const roleName = dto.plan === PlanEnum.Base ? 'مدیر (پلن پایه)' : 'مدیر ( پلن ویژه)'; + const role = await em.findOne(Role, { name: roleName }); + if (!role) { + throw new BadRequestException(`Role not found for plan: ${dto.plan}`); + } + + // Create admin (using a default phone number for now - this could be made configurable) + const adminPhone = `admin_${dto.slug}@system.local`; // Temporary phone format for system-generated admins + let admin = await em.findOne(Admin, { phone: adminPhone }); + if (!admin) { + admin = em.create(Admin, { + phone: adminPhone, + firstName: 'مدیر', + lastName: dto.name, + }); + } + + // Create admin role relationship + const adminRole = em.create(AdminRole, { + admin: admin, + role: role, + restaurant: restaurant, + }); + + // Persist all entities + em.persist([restaurant, admin, adminRole]); + await em.flush(); + + return restaurant; + }); } async findAll(dto: FindRestaurantsDto): Promise> { diff --git a/src/seeders/data/admins.data.ts b/src/seeders/data/admins.data.ts index 7992b59..9e5ec90 100644 --- a/src/seeders/data/admins.data.ts +++ b/src/seeders/data/admins.data.ts @@ -11,14 +11,14 @@ export const adminsData: AdminData[] = [ phone: '09362532122', firstName: 'مرتضی', lastName: 'مرتضایی', - roleName: 'restaurant', + roleName: 'مدیر ( پلن ویژه)', restaurantSlug: 'zhivan', }, { phone: '09185290775', firstName: 'حمید', lastName: 'ضرقامی', - roleName: 'restaurant', + roleName: 'مدیر (پلن پایه)', restaurantSlug: 'boote', }, { @@ -32,7 +32,7 @@ export const adminsData: AdminData[] = [ phone: '09184317567', firstName: 'ادمین', lastName: 'هنر', - roleName: 'restaurant', + roleName: 'مدیر (پلن پایه)', restaurantSlug: 'honar', }, ]; diff --git a/src/seeders/data/restaurants.data.ts b/src/seeders/data/restaurants.data.ts index e706cb9..0bcb7cd 100644 --- a/src/seeders/data/restaurants.data.ts +++ b/src/seeders/data/restaurants.data.ts @@ -20,7 +20,7 @@ export interface RestaurantData { marriageDateScore: string; referrerScore: string; }; - plan: PlanEnum.Premium; + plan: PlanEnum; subscriptionId: string; } @@ -84,7 +84,7 @@ export const restaurantsData: RestaurantData[] = [ marriageDateScore: '10000', referrerScore: '10000', }, - plan: PlanEnum.Premium, + plan: PlanEnum.Base, subscriptionId: 'sub_seed_boote_001', }, { @@ -115,7 +115,7 @@ export const restaurantsData: RestaurantData[] = [ marriageDateScore: '10000', referrerScore: '10000', }, - plan: PlanEnum.Premium, + plan: PlanEnum.Base, subscriptionId: 'sub_seed_honar_001', }, ]; diff --git a/src/seeders/data/roles.data.ts b/src/seeders/data/roles.data.ts index faa7412..c2907f5 100644 --- a/src/seeders/data/roles.data.ts +++ b/src/seeders/data/roles.data.ts @@ -8,12 +8,21 @@ export interface RoleConfig { export const rolesData: RoleConfig[] = [ { - name: 'restaurant', + name: 'مدیر (پلن پایه)', + isSystem: false, + permissionFilter: (name: Permission) => + name === Permission.MANAGE_FOODS || name === Permission.MANAGE_CATEGORIES || + name === Permission.MANAGE_SCHEDULES || + name === Permission.MANAGE_PAGER || + name === Permission.MANAGE_CONTACTS, + }, + { + name: 'مدیر ( پلن ویژه)', isSystem: false, permissionFilter: () => true, // All permissions for restaurant role }, { - name: 'accountant', + name: 'حسابدار ', isSystem: false, permissionFilter: (name: Permission) => name === Permission.VIEW_REPORTS || name === Permission.MANAGE_PAYMENTS || name === Permission.MANAGE_ORDERS, From a7c18a9bff5f155f5ab677ca148042461d24a543 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 27 Dec 2025 12:00:31 +0330 Subject: [PATCH 10/12] fix:bug --> get admin permission --- .../admin/repositories/admin.repository.ts | 2 +- .../providers/restaurants.service.ts | 18 ++++---- .../roles/providers/permissions.service.ts | 44 +++++++------------ 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/modules/admin/repositories/admin.repository.ts b/src/modules/admin/repositories/admin.repository.ts index e6cb6ea..04868a5 100644 --- a/src/modules/admin/repositories/admin.repository.ts +++ b/src/modules/admin/repositories/admin.repository.ts @@ -40,7 +40,7 @@ export class AdminRepository extends EntityRepository { // Ensure all roles are populated await adminRole.admin.roles.loadItems(); for (const role of adminRole.admin.roles.getItems()) { - if (role.role && !role.role.permissions.isInitialized()) { + if (role.role && role.role.permissions && !role.role.permissions.isInitialized()) { await role.role.permissions.loadItems(); } } diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index 1c36308..98d0693 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -11,6 +11,7 @@ import { PlanEnum } from '../interface/plan.interface'; import { Admin } from '../../admin/entities/admin.entity'; import { AdminRole } from '../../admin/entities/adminRole.entity'; import { Role } from '../../roles/entities/role.entity'; +import { normalizePhone } from 'src/modules/utils/phone.util'; @Injectable() export class RestaurantsService { @@ -52,22 +53,21 @@ export class RestaurantsService { throw new BadRequestException(`Role not found for plan: ${dto.plan}`); } - // Create admin (using a default phone number for now - this could be made configurable) - const adminPhone = `admin_${dto.slug}@system.local`; // Temporary phone format for system-generated admins - let admin = await em.findOne(Admin, { phone: adminPhone }); +const normalizedPhone = normalizePhone(dto.phone); + let admin = await em.findOne(Admin, { phone: normalizedPhone }); if (!admin) { admin = em.create(Admin, { - phone: adminPhone, - firstName: 'مدیر', - lastName: dto.name, + phone: normalizedPhone, + firstName: 'نام مدیر', + lastName: 'نام خانوادگی مدیر', }); } // Create admin role relationship const adminRole = em.create(AdminRole, { - admin: admin, - role: role, - restaurant: restaurant, + admin, + role, + restaurant, }); // Persist all entities diff --git a/src/modules/roles/providers/permissions.service.ts b/src/modules/roles/providers/permissions.service.ts index f4fc79d..8e96fa4 100644 --- a/src/modules/roles/providers/permissions.service.ts +++ b/src/modules/roles/providers/permissions.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { EntityRepository } from '@mikro-orm/core'; +import { EntityManager, EntityRepository } from '@mikro-orm/core'; import { Permission } from '../entities/permission.entity'; import { CacheService } from 'src/modules/utils/cache.service'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; +import { AdminRole } from 'src/modules/admin/entities/adminRole.entity'; @Injectable() export class PermissionsService { @@ -14,7 +15,8 @@ export class PermissionsService { private readonly permissionRepository: EntityRepository, private readonly cacheService: CacheService, private readonly adminRepository: AdminRepository, - ) {} + private readonly em: EntityManager, + ) { } async findAll() { const permissions = await this.permissionRepository.findAll(); @@ -76,33 +78,19 @@ export class PermissionsService { } async getAdminFullPermissions(adminId: string, restId: string): Promise { - const admin = await this.adminRepository.findOne( - { id: adminId, roles: { restaurant: { id: restId } } }, - { populate: ['roles', 'roles.role', 'roles.role.permissions'] }, - ); - - if (!admin || !admin.roles) { - return []; + const listOfPermissions = [] + const adminRoles = await this.em.findOne(AdminRole, { + admin: { + id: adminId, + }, + restaurant: { + id: restId, + }, + }, { populate: ['role', 'role.permissions'] }); + if (adminRoles) { + listOfPermissions.push(...adminRoles.role.permissions.getItems()); } - - // Ensure roles collection is loaded - await admin.roles.loadItems(); - - // Extract permission names as array of strings - const permissions = await Promise.all( - admin.roles - .getItems() - .filter(r => r.role) // Filter out any null/undefined roles - .map(async r => { - // Ensure permissions collection is initialized - if (!r.role.permissions.isInitialized()) { - await r.role.permissions.loadItems(); - } - return r.role.permissions.getItems(); - }), - ); - - return permissions.flat(); + return listOfPermissions.map(permission => permission); } /** From 829f09fdb4875fb84db3184f345bbd39a4e999fc Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 27 Dec 2025 12:12:47 +0330 Subject: [PATCH 11/12] roles update --- src/seeders/data/roles.data.ts | 7 +++++-- src/seeders/roles.seeder.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/seeders/data/roles.data.ts b/src/seeders/data/roles.data.ts index c2907f5..e83c256 100644 --- a/src/seeders/data/roles.data.ts +++ b/src/seeders/data/roles.data.ts @@ -11,10 +11,13 @@ export const rolesData: RoleConfig[] = [ name: 'مدیر (پلن پایه)', isSystem: false, permissionFilter: (name: Permission) => - name === Permission.MANAGE_FOODS || name === Permission.MANAGE_CATEGORIES || + name === Permission.MANAGE_FOODS || + name === Permission.MANAGE_CATEGORIES || name === Permission.MANAGE_SCHEDULES || name === Permission.MANAGE_PAGER || - name === Permission.MANAGE_CONTACTS, + name === Permission.MANAGE_CONTACTS || + name === Permission.MANAGE_ROLES || + name === Permission.MANAGE_SETTINGS }, { name: 'مدیر ( پلن ویژه)', diff --git a/src/seeders/roles.seeder.ts b/src/seeders/roles.seeder.ts index d2a5279..3f07cb5 100644 --- a/src/seeders/roles.seeder.ts +++ b/src/seeders/roles.seeder.ts @@ -13,7 +13,7 @@ export class RolesSeeder { if (!role) { role = em.create(Role, { name: roleConfig.name, - isSystem: roleConfig.isSystem, + isSystem: true, }); // Add permissions based on role configuration From 5beebf166855ca00e8320b9ca330a047d28f0ca2 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 27 Dec 2025 16:52:26 +0330 Subject: [PATCH 12/12] update sms count route --- .../notifications/controllers/notifications.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/notifications/controllers/notifications.controller.ts b/src/modules/notifications/controllers/notifications.controller.ts index 0d1aa5f..1952192 100644 --- a/src/modules/notifications/controllers/notifications.controller.ts +++ b/src/modules/notifications/controllers/notifications.controller.ts @@ -155,9 +155,9 @@ export class NotificationsController { return this.preferenceService.updatePreference(preferenceId, restaurantId, dto); } - @UseGuards(SuperAdminAuthGuard) + // @UseGuards(SuperAdminAuthGuard) @ApiBearerAuth() - @Get('super-admin/sms-count') + @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 })