From 4033760d0fcde1329e306ac0e15331cf12558515 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 9 Mar 2026 14:55:40 +0330 Subject: [PATCH] remove unused --- src/app.module.ts | 16 +- src/modules/cart/cart.module.ts | 53 -- .../cart/controllers/cart.controller.ts | 92 --- src/modules/cart/dto/apply-coupon.dto.ts | 9 - src/modules/cart/dto/bulk-add-items.dto.ts | 34 - src/modules/cart/dto/create-cart.dto.ts | 29 - .../cart/dto/set-all-cart-params.dto.ts | 40 -- src/modules/cart/dto/set-car-delivery.dto.ts | 19 - src/modules/cart/dto/update-cart.dto.ts | 9 - src/modules/cart/interfaces/cart.interface.ts | 38 -- .../providers/cart-calculation.service.ts | 244 ------- .../cart/providers/cart-item.service.ts | 113 ---- .../cart/providers/cart-validation.service.ts | 422 ------------ src/modules/cart/providers/cart.service.ts | 487 -------------- .../cart/repositories/cart.repository.ts | 81 --- src/modules/cart/utils/geographic.utils.ts | 68 -- src/modules/foods/crone/food.crone.ts | 21 +- src/modules/foods/entities/food.entity.ts | 12 +- src/modules/foods/providers/food.service.ts | 30 +- .../foods/repositories/food.repository.ts | 2 +- .../icons/controllers/icons.controller.ts | 114 ---- src/modules/icons/dto/create-group.dto.ts | 9 - src/modules/icons/dto/create-icon.dto.ts | 12 - src/modules/icons/dto/update-group.dto.ts | 5 - src/modules/icons/dto/update-icon.dto.ts | 4 - src/modules/icons/entities/group.entity.ts | 15 - src/modules/icons/entities/icon.entity.ts | 13 - src/modules/icons/icons.module.ts | 18 - src/modules/icons/providers/group.service.ts | 56 -- src/modules/icons/providers/icon.service.ts | 76 --- .../icons/repositories/group.repository.ts | 10 - .../icons/repositories/icon.repository.ts | 10 - .../inventory/dto/bulk-reserve-food.dto.ts | 34 - .../inventory/dto/bulk-set-stock.dto.ts | 27 - .../inventory/dto/create-inventory.dto.ts | 1 - src/modules/inventory/dto/set-stock.dto.ts | 17 - .../inventory/entities/inventory.entity.ts | 18 - src/modules/inventory/inteface/reservation.ts | 6 - src/modules/inventory/inventory.controller.ts | 38 -- src/modules/inventory/inventory.module.ts | 15 - src/modules/inventory/inventory.service.ts | 227 ------- .../orders/controllers/orders.controller.ts | 120 ---- src/modules/orders/crone/order.crone.ts | 187 ------ src/modules/orders/dto/find-orders.dto.ts | 83 --- .../orders/dto/update-order-status.dto.ts | 10 - src/modules/orders/dto/update-order.dto.ts | 5 - .../orders/entities/order-item.entity.ts | 27 - src/modules/orders/entities/order.entity.ts | 130 ---- src/modules/orders/events/order.events.ts | 22 - .../orders/interface/order.interface.ts | 42 -- .../orders/listeners/order.listeners.ts | 184 ------ src/modules/orders/orders.module.ts | 41 -- .../orders/providers/orders.service.ts | 601 ------------------ .../orders/repositories/order.repository.ts | 184 ------ .../pager/controllers/pager.controller.ts | 101 --- .../pager/decorators/ws-admin-id.decorator.ts | 26 - .../pager/decorators/ws-rest-id.decorator.ts | 26 - src/modules/pager/dto/create-pager.dto.ts | 15 - .../pager/dto/pager-socket-connection.dto.ts | 208 ------ src/modules/pager/dto/update-pager.dto.ts | 10 - src/modules/pager/entities/pager.entity.ts | 30 - src/modules/pager/events/pager.events.ts | 6 - .../pager/guards/ws-admin-auth.guard.ts | 104 --- src/modules/pager/interface/pager.ts | 6 - .../pager/listeners/notification.listeners.ts | 64 -- src/modules/pager/pager.module.ts | 17 - src/modules/pager/providers/pager.service.ts | 115 ---- .../controllers/payments.controller.ts | 143 ----- .../payments/dto/create-payment-method.dto.ts | 35 - src/modules/payments/dto/payment-chart.dto.ts | 41 -- .../payments/dto/update-payment-method.dto.ts | 4 - .../payments/dto/update-payment.dto.ts | 8 - .../payments/dto/verify-payment.dto.ts | 12 - .../entities/payment-method.entity.ts | 30 - .../payments/entities/payment.entity.ts | 44 -- src/modules/payments/events/payment.events.ts | 23 - .../payments/gateways/gateway.manager.ts | 20 - .../payments/gateways/zarinpal.gateway.ts | 158 ----- src/modules/payments/interface/gateway.ts | 70 -- src/modules/payments/interface/payment.ts | 33 - .../payments/listeners/payment.listeners.ts | 170 ----- src/modules/payments/payments.module.ts | 36 -- .../repositories/payment-method.repository.ts | 10 - .../repositories/payment.repository.ts | 10 - .../services/payment-method.service.ts | 63 -- .../payments/services/payments.service.ts | 442 ------------- .../providers/restaurants.service.ts | 11 - .../review/controllers/review.controller.ts | 165 ----- src/modules/review/dto/change-status.dto.ts | 13 - src/modules/review/dto/create-review.dto.ts | 51 -- src/modules/review/dto/find-reviews.dto.ts | 55 -- src/modules/review/dto/update-review.dto.ts | 45 -- src/modules/review/entities/review.entity.ts | 39 -- src/modules/review/enums/review-point.enum.ts | 15 - .../review/enums/review-status.enum.ts | 5 - src/modules/review/events/review.events.ts | 13 - .../review/listeners/review.listeners.ts | 77 --- .../providers/food-rating-cron.service.ts | 77 --- .../review/providers/review.service.ts | 219 ------- .../review/repositories/review.repository.ts | 71 --- src/modules/review/review.module.ts | 22 - src/modules/users/entities/user.entity.ts | 6 +- src/modules/users/providers/user.service.ts | 22 +- src/seeders/DatabaseSeeder.ts | 53 +- src/seeders/categories.seeder.ts | 395 ------------ src/seeders/coupons.seeder.ts | 42 -- src/seeders/data/payment-methods.data.ts | 35 - src/seeders/inventory.seeder.ts | 29 - src/seeders/payment-methods.seeder.ts | 52 -- 109 files changed, 33 insertions(+), 7739 deletions(-) delete mode 100644 src/modules/cart/cart.module.ts delete mode 100644 src/modules/cart/controllers/cart.controller.ts delete mode 100644 src/modules/cart/dto/apply-coupon.dto.ts delete mode 100644 src/modules/cart/dto/bulk-add-items.dto.ts delete mode 100644 src/modules/cart/dto/create-cart.dto.ts delete mode 100644 src/modules/cart/dto/set-all-cart-params.dto.ts delete mode 100644 src/modules/cart/dto/set-car-delivery.dto.ts delete mode 100644 src/modules/cart/dto/update-cart.dto.ts delete mode 100644 src/modules/cart/interfaces/cart.interface.ts delete mode 100644 src/modules/cart/providers/cart-calculation.service.ts delete mode 100644 src/modules/cart/providers/cart-item.service.ts delete mode 100644 src/modules/cart/providers/cart-validation.service.ts delete mode 100644 src/modules/cart/providers/cart.service.ts delete mode 100644 src/modules/cart/repositories/cart.repository.ts delete mode 100644 src/modules/cart/utils/geographic.utils.ts delete mode 100644 src/modules/icons/controllers/icons.controller.ts delete mode 100644 src/modules/icons/dto/create-group.dto.ts delete mode 100644 src/modules/icons/dto/create-icon.dto.ts delete mode 100644 src/modules/icons/dto/update-group.dto.ts delete mode 100644 src/modules/icons/dto/update-icon.dto.ts delete mode 100644 src/modules/icons/entities/group.entity.ts delete mode 100644 src/modules/icons/entities/icon.entity.ts delete mode 100644 src/modules/icons/icons.module.ts delete mode 100644 src/modules/icons/providers/group.service.ts delete mode 100644 src/modules/icons/providers/icon.service.ts delete mode 100644 src/modules/icons/repositories/group.repository.ts delete mode 100644 src/modules/icons/repositories/icon.repository.ts delete mode 100644 src/modules/inventory/dto/bulk-reserve-food.dto.ts delete mode 100644 src/modules/inventory/dto/bulk-set-stock.dto.ts delete mode 100644 src/modules/inventory/dto/create-inventory.dto.ts delete mode 100644 src/modules/inventory/dto/set-stock.dto.ts delete mode 100644 src/modules/inventory/entities/inventory.entity.ts delete mode 100644 src/modules/inventory/inteface/reservation.ts delete mode 100644 src/modules/inventory/inventory.controller.ts delete mode 100644 src/modules/inventory/inventory.module.ts delete mode 100644 src/modules/inventory/inventory.service.ts delete mode 100644 src/modules/orders/controllers/orders.controller.ts delete mode 100644 src/modules/orders/crone/order.crone.ts delete mode 100644 src/modules/orders/dto/find-orders.dto.ts delete mode 100644 src/modules/orders/dto/update-order-status.dto.ts delete mode 100644 src/modules/orders/dto/update-order.dto.ts delete mode 100644 src/modules/orders/entities/order-item.entity.ts delete mode 100644 src/modules/orders/entities/order.entity.ts delete mode 100644 src/modules/orders/events/order.events.ts delete mode 100644 src/modules/orders/interface/order.interface.ts delete mode 100644 src/modules/orders/listeners/order.listeners.ts delete mode 100644 src/modules/orders/orders.module.ts delete mode 100644 src/modules/orders/providers/orders.service.ts delete mode 100644 src/modules/orders/repositories/order.repository.ts delete mode 100644 src/modules/pager/controllers/pager.controller.ts delete mode 100644 src/modules/pager/decorators/ws-admin-id.decorator.ts delete mode 100644 src/modules/pager/decorators/ws-rest-id.decorator.ts delete mode 100644 src/modules/pager/dto/create-pager.dto.ts delete mode 100644 src/modules/pager/dto/pager-socket-connection.dto.ts delete mode 100644 src/modules/pager/dto/update-pager.dto.ts delete mode 100644 src/modules/pager/entities/pager.entity.ts delete mode 100644 src/modules/pager/events/pager.events.ts delete mode 100644 src/modules/pager/guards/ws-admin-auth.guard.ts delete mode 100644 src/modules/pager/interface/pager.ts delete mode 100644 src/modules/pager/listeners/notification.listeners.ts delete mode 100644 src/modules/pager/pager.module.ts delete mode 100644 src/modules/pager/providers/pager.service.ts delete mode 100644 src/modules/payments/controllers/payments.controller.ts delete mode 100644 src/modules/payments/dto/create-payment-method.dto.ts delete mode 100644 src/modules/payments/dto/payment-chart.dto.ts delete mode 100644 src/modules/payments/dto/update-payment-method.dto.ts delete mode 100644 src/modules/payments/dto/update-payment.dto.ts delete mode 100644 src/modules/payments/dto/verify-payment.dto.ts delete mode 100644 src/modules/payments/entities/payment-method.entity.ts delete mode 100644 src/modules/payments/entities/payment.entity.ts delete mode 100644 src/modules/payments/events/payment.events.ts delete mode 100644 src/modules/payments/gateways/gateway.manager.ts delete mode 100755 src/modules/payments/gateways/zarinpal.gateway.ts delete mode 100644 src/modules/payments/interface/gateway.ts delete mode 100644 src/modules/payments/interface/payment.ts delete mode 100644 src/modules/payments/listeners/payment.listeners.ts delete mode 100644 src/modules/payments/payments.module.ts delete mode 100644 src/modules/payments/repositories/payment-method.repository.ts delete mode 100644 src/modules/payments/repositories/payment.repository.ts delete mode 100644 src/modules/payments/services/payment-method.service.ts delete mode 100644 src/modules/payments/services/payments.service.ts delete mode 100644 src/modules/review/controllers/review.controller.ts delete mode 100644 src/modules/review/dto/change-status.dto.ts delete mode 100644 src/modules/review/dto/create-review.dto.ts delete mode 100644 src/modules/review/dto/find-reviews.dto.ts delete mode 100644 src/modules/review/dto/update-review.dto.ts delete mode 100644 src/modules/review/entities/review.entity.ts delete mode 100644 src/modules/review/enums/review-point.enum.ts delete mode 100644 src/modules/review/enums/review-status.enum.ts delete mode 100644 src/modules/review/events/review.events.ts delete mode 100644 src/modules/review/listeners/review.listeners.ts delete mode 100644 src/modules/review/providers/food-rating-cron.service.ts delete mode 100644 src/modules/review/providers/review.service.ts delete mode 100644 src/modules/review/repositories/review.repository.ts delete mode 100644 src/modules/review/review.module.ts delete mode 100644 src/seeders/categories.seeder.ts delete mode 100644 src/seeders/coupons.seeder.ts delete mode 100644 src/seeders/data/payment-methods.data.ts delete mode 100644 src/seeders/inventory.seeder.ts delete mode 100644 src/seeders/payment-methods.seeder.ts diff --git a/src/app.module.ts b/src/app.module.ts index cc1afb7..748a7af 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,19 +11,12 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { ScheduleModule } from '@nestjs/schedule'; import { RestaurantsModule } from './modules/restaurants/restaurants.module'; import { FoodModule } from './modules/foods/food.module'; -import { CartModule } from './modules/cart/cart.module'; import { RolesModule } from './modules/roles/roles.module'; -import { PaymentsModule } from './modules/payments/payments.module'; import { DeliveryModule } from './modules/delivery/delivery.module'; -import { OrdersModule } from './modules/orders/orders.module'; import { CouponModule } from './modules/coupons/coupon.module'; -import { ReviewModule } from './modules/review/review.module'; import { NotificationsModule } from './modules/notifications/notifications.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; -import { PagerModule } from './modules/pager/pager.module'; import { ContactModule } from './modules/contact/contact.module'; -import { InventoryModule } from './modules/inventory/inventory.module'; -import { IconsModule } from './modules/icons/icons.module'; import { CacheModule } from '@nestjs/cache-manager'; import { cacheConfig } from './config/cache.config'; import { BusinessModule } from './modules/business/business.module'; @@ -48,19 +41,12 @@ import { CatalogueModule } from './modules/catalogue/catalogue.module'; ScheduleModule.forRoot(), RestaurantsModule, FoodModule, - CartModule, RolesModule, - PaymentsModule, DeliveryModule, - OrdersModule, CouponModule, - ReviewModule, NotificationsModule, EventEmitterModule.forRoot(), - PagerModule, ContactModule, - InventoryModule, - IconsModule, BusinessModule, CatalogueModule, ], @@ -68,4 +54,4 @@ import { CatalogueModule } from './modules/catalogue/catalogue.module'; providers: [], // exports: [CacheService], }) -export class AppModule {} +export class AppModule { } diff --git a/src/modules/cart/cart.module.ts b/src/modules/cart/cart.module.ts deleted file mode 100644 index 0549845..0000000 --- a/src/modules/cart/cart.module.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { CartService } from './providers/cart.service'; -import { CartController } from './controllers/cart.controller'; -import { Food } from '../foods/entities/food.entity'; -import { Restaurant } from '../restaurants/entities/restaurant.entity'; -import { PaymentMethod } from '../payments/entities/payment-method.entity'; -import { UserAddress } from '../users/entities/user-address.entity'; -import { User } from '../users/entities/user.entity'; -import { Delivery } from '../delivery/entities/delivery.entity'; -import { Order } from '../orders/entities/order.entity'; -import { AuthModule } from '../auth/auth.module'; -import { UserModule } from '../users/user.module'; -import { JwtModule } from '@nestjs/jwt'; -import { UtilsModule } from '../utils/utils.module'; -import { CouponModule } from '../coupons/coupon.module'; -import { CartRepository } from './repositories/cart.repository'; -import { CartValidationService } from './providers/cart-validation.service'; -import { CartCalculationService } from './providers/cart-calculation.service'; -import { CartItemService } from './providers/cart-item.service'; -import { PointTransaction } from '../users/entities/point-transaction.entity'; -import { WalletTransaction } from '../users/entities/wallet-transaction.entity'; - -@Module({ - imports: [ - MikroOrmModule.forFeature([ - Food, - Restaurant, - PaymentMethod, - UserAddress, - User, - PointTransaction, - WalletTransaction, - Delivery, - Order, - ]), - AuthModule, - UserModule, - JwtModule, - UtilsModule, - CouponModule, - ], - controllers: [CartController], - providers: [ - CartService, - CartRepository, - CartValidationService, - CartCalculationService, - CartItemService, - ], - exports: [CartService], -}) -export class CartModule { } diff --git a/src/modules/cart/controllers/cart.controller.ts b/src/modules/cart/controllers/cart.controller.ts deleted file mode 100644 index 59ce4bf..0000000 --- a/src/modules/cart/controllers/cart.controller.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common'; -import { CartService } from '../providers/cart.service'; -import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto'; -import { ApplyCouponDto } from '../dto/apply-coupon.dto'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger'; -import { AuthGuard } from '../../auth/guards/auth.guard'; -import { UserId } from 'src/common/decorators/user-id.decorator'; -import { RestId } from 'src/common/decorators/rest-id.decorator'; -import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto'; -import { API_HEADER_SLUG } from 'src/common/constants/index'; - -@UseGuards(AuthGuard) -@ApiBearerAuth() -@ApiTags('cart') -@Controller('public/cart') -export class CartController { - constructor(private readonly cartService: CartService) {} - - @Get() - @ApiOperation({ summary: 'Get cart for current user and restaurant' }) - @ApiHeader(API_HEADER_SLUG) - async findOne(@UserId() userId: string, @RestId() restaurantId: string) { - return this.cartService.getOrCreateCart(userId, restaurantId); - } - - @Post('items/:foodId/increment') - @ApiOperation({ summary: 'Increment item quantity in cart by 1' }) - @ApiHeader(API_HEADER_SLUG) - @ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' }) - async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { - return this.cartService.incrementItem(userId, restaurantId, foodId, 1); - } - - @Post('items/:foodId/decrement') - @ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' }) - async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { - return this.cartService.decrementItem(userId, restaurantId, foodId); - } - - @Post('items/bulk') - @ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: BulkAddItemsToCartDto }) - bulkAddItems( - @UserId() userId: string, - @RestId() restaurantId: string, - @Body() bulkAddItemsDto: BulkAddItemsToCartDto, - ) { - return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto); - } - - @Delete('items/:foodId') - @ApiOperation({ summary: 'Remove item from cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiParam({ name: 'foodId', description: 'Food ID in the cart' }) - async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { - return this.cartService.removeItem(userId, restaurantId, foodId); - } - - @Delete() - @ApiOperation({ summary: 'Clear entire cart' }) - @ApiHeader(API_HEADER_SLUG) - async clearCart(@UserId() userId: string, @RestId() restaurantId: string) { - return this.cartService.clearCart(userId, restaurantId); - } - - @Post('apply-coupon') - @ApiOperation({ summary: 'Apply coupon to cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: ApplyCouponDto }) - async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) { - return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto); - } - - @Delete('coupon') - @ApiOperation({ summary: 'Remove coupon from cart' }) - @ApiHeader(API_HEADER_SLUG) - async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) { - return this.cartService.removeCoupon(userId, restaurantId); - } - - - @Patch('all') - @ApiOperation({ summary: 'Set all cart params' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetAllCartParmsDto }) - setAllCartParams(@UserId() userId: string, @RestId() restaurantId: string, @Body() dto: SetAllCartParmsDto) { - return this.cartService.setAllCartParams(userId, restaurantId, dto); - } -} diff --git a/src/modules/cart/dto/apply-coupon.dto.ts b/src/modules/cart/dto/apply-coupon.dto.ts deleted file mode 100644 index cb182a0..0000000 --- a/src/modules/cart/dto/apply-coupon.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class ApplyCouponDto { - @ApiProperty({ description: 'Coupon code', example: 'DISCOUNT10' }) - @IsNotEmpty() - @IsString() - code!: string; -} diff --git a/src/modules/cart/dto/bulk-add-items.dto.ts b/src/modules/cart/dto/bulk-add-items.dto.ts deleted file mode 100644 index 71ac392..0000000 --- a/src/modules/cart/dto/bulk-add-items.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsInt, Min, IsString } from 'class-validator'; -import { Type } from 'class-transformer'; - -class AddItemToCartDto { - @ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 }) - @IsNotEmpty() - @Type(() => Number) - @IsInt() - @Min(1) - quantity!: number; - - @ApiProperty({ description: 'Food ID' }) - @IsNotEmpty() - @IsString() - foodId!: string; -} - -export class BulkAddItemsToCartDto { - @ApiProperty({ - description: 'Array of items to add to cart', - type: [AddItemToCartDto], - example: [ - { foodId: 'food-123', quantity: 2 }, - { foodId: 'food-456', quantity: 1 }, - ], - }) - @IsNotEmpty() - @IsArray() - @ArrayMinSize(1, { message: 'At least one item is required' }) - @ValidateNested({ each: true }) - @Type(() => AddItemToCartDto) - items!: AddItemToCartDto[]; -} diff --git a/src/modules/cart/dto/create-cart.dto.ts b/src/modules/cart/dto/create-cart.dto.ts deleted file mode 100644 index ec5763b..0000000 --- a/src/modules/cart/dto/create-cart.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsArray, ValidateNested, IsInt, Min } from 'class-validator'; -import { Type } from 'class-transformer'; - -export class CartItemDto { - @ApiProperty({ description: 'Food ID' }) - @IsNotEmpty() - @IsString() - foodId!: string; - - @ApiProperty({ description: 'Quantity of the food item', example: 2, minimum: 1 }) - @IsNotEmpty() - @Type(() => Number) - @IsInt() - @Min(1) - quantity!: number; -} - -export class CreateCartDto { - @ApiProperty({ - description: 'List of cart items', - type: [CartItemDto], - }) - @IsNotEmpty() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CartItemDto) - items!: CartItemDto[]; -} diff --git a/src/modules/cart/dto/set-all-cart-params.dto.ts b/src/modules/cart/dto/set-all-cart-params.dto.ts deleted file mode 100644 index d329a51..0000000 --- a/src/modules/cart/dto/set-all-cart-params.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator'; -import { SetCarDeliveryDto } from './set-car-delivery.dto'; - - -export class SetAllCartParmsDto { - @ApiProperty({ - description: 'Cart description or notes', - required: false, - example: 'Please deliver to the back door', - }) - @IsOptional() - @IsString() - description?: string; - - @ApiProperty({ description: 'Delivery method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' }) - @IsNotEmpty() - @IsString() - deliveryMethodId!: string; - - @ApiProperty({ description: 'User address ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' }) - @IsOptional() - @IsString() - addressId?: string; - - @ApiProperty({ description: 'Payment method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' }) - @IsNotEmpty() - @IsString() - paymentMethodId!: string; - - @ApiProperty({ description: 'Table number for dine-in orders', example: '5', required: false }) - @IsOptional() - @IsString() - tableNumber?: string; - - @ApiProperty() - @IsOptional() - @IsObject() - carAddress?: SetCarDeliveryDto; -} diff --git a/src/modules/cart/dto/set-car-delivery.dto.ts b/src/modules/cart/dto/set-car-delivery.dto.ts deleted file mode 100644 index 61c44dc..0000000 --- a/src/modules/cart/dto/set-car-delivery.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class SetCarDeliveryDto { - @ApiProperty({ description: 'Car model', example: 'Toyota Camry' }) - @IsNotEmpty() - @IsString() - carModel!: string; - - @ApiProperty({ description: 'Car color', example: 'White' }) - @IsNotEmpty() - @IsString() - carColor!: string; - - @ApiProperty({ description: 'License plate number', example: '12ABC345' }) - @IsNotEmpty() - @IsString() - plateNumber!: string; -} diff --git a/src/modules/cart/dto/update-cart.dto.ts b/src/modules/cart/dto/update-cart.dto.ts deleted file mode 100644 index b70b562..0000000 --- a/src/modules/cart/dto/update-cart.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PartialType, ApiProperty } from '@nestjs/swagger'; -import { CreateCartDto, CartItemDto } from './create-cart.dto'; - -export class UpdateCartItemDto extends PartialType(CartItemDto) {} - -export class UpdateCartDto { - @ApiProperty({ description: 'List of cart items to update', type: [UpdateCartItemDto], required: false }) - items?: UpdateCartItemDto[]; -} diff --git a/src/modules/cart/interfaces/cart.interface.ts b/src/modules/cart/interfaces/cart.interface.ts deleted file mode 100644 index b281661..0000000 --- a/src/modules/cart/interfaces/cart.interface.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { OrderCouponDetail, OrderUserAddress, OrderCarAddress } from 'src/modules/orders/interface/order.interface'; - -export interface CartItem { - foodId: string; - foodTitle?: string; - quantity: number; - price: number; - discount: number; - totalPrice: number; -} - -export interface Cart { - userId: string; - restaurantId: string; - restaurantName?: string; - - items: CartItem[]; - coupon?: OrderCouponDetail; - paymentMethodId?: string; - deliveryMethodId?: string; - description?: string; - tableNumber?: string; - - deliveryFee: number; - subTotal: number; - tax: number; - couponDiscount: number; - itemsDiscount: number; - totalDiscount: number; - total: number; - - carAddress?: OrderCarAddress | null; - userAddress?: OrderUserAddress | null; - - totalItems: number; - createdAt: string; - updatedAt: string; -} diff --git a/src/modules/cart/providers/cart-calculation.service.ts b/src/modules/cart/providers/cart-calculation.service.ts deleted file mode 100644 index bd2e714..0000000 --- a/src/modules/cart/providers/cart-calculation.service.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { Delivery } from '../../delivery/entities/delivery.entity'; -import { DeliveryFeeTypeEnum } from '../../delivery/interface/delivery'; -import { CouponType } from '../../coupons/interface/coupon'; -import { Food } from '../../foods/entities/food.entity'; -import { Cart } from '../interfaces/cart.interface'; -import { CouponService } from '../../coupons/providers/coupon.service'; -import { GeographicUtils } from '../utils/geographic.utils'; - -@Injectable() -export class CartCalculationService { - constructor( - private readonly em: EntityManager, - private readonly couponService: CouponService, - ) {} - - /** - * Calculate total price for a cart item - */ - calculateItemTotalPrice(unitPrice: number, unitDiscount: number, quantity: number): number { - const safeUnitPrice = Number(unitPrice) || 0; - const safeUnitDiscount = Math.min(Number(unitDiscount) || 0, safeUnitPrice); - const itemTotalPrice = safeUnitPrice * quantity; - const itemTotalDiscount = safeUnitDiscount * quantity; - return itemTotalPrice - itemTotalDiscount; - } - - /** - * Calculate items totals (subtotal, items discount, total items) - */ - calculateItemsTotals(cart: Cart): { subTotal: number; itemsDiscount: number; totalItems: number } { - let subTotal = 0; - let itemsDiscount = 0; - let totalItems = 0; - - for (const item of cart.items) { - const unitPrice = Number(item.price) || 0; - const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice); - const itemPrice = unitPrice * item.quantity; - const itemDiscount = unitDiscount * item.quantity; - subTotal += itemPrice; - itemsDiscount += itemDiscount; - totalItems += item.quantity; - item.totalPrice = itemPrice - itemDiscount; - } - - return { subTotal, itemsDiscount, totalItems }; - } - - /** - * Calculate coupon discount - */ - async calculateCouponDiscount(cart: Cart, subTotal: number, itemsDiscount: number): Promise { - if (!cart.coupon) return 0; - - const coupon = await this.getCouponRestrictionsOrClear(cart); - if (!cart.coupon || !coupon) return 0; - - const hasCategoryRestriction = (coupon.foodCategories?.length ?? 0) > 0; - const hasFoodRestriction = (coupon.foods?.length ?? 0) > 0; - - let eligibleItemsTotal = subTotal; - let eligibleItemsDiscount = itemsDiscount; - - if (hasCategoryRestriction || hasFoodRestriction) { - eligibleItemsTotal = 0; - eligibleItemsDiscount = 0; - - const foodMap = await this.getFoodsInCartWithCategories(cart); - - for (const item of cart.items) { - const food = foodMap.get(item.foodId); - if (!food) continue; - - const matchesCategory = - hasCategoryRestriction && food.category && (coupon.foodCategories ?? []).includes(food.category.id); - const matchesFood = hasFoodRestriction && (coupon.foods ?? []).includes(food.id); - - if (matchesCategory || matchesFood) { - const unitPrice = Number(item.price) || 0; - const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice); - eligibleItemsTotal += unitPrice * item.quantity; - eligibleItemsDiscount += unitDiscount * item.quantity; - } - } - } - - const priceAfterItemDiscount = Math.max(0, eligibleItemsTotal - eligibleItemsDiscount); - - if (cart.coupon.type === CouponType.PERCENTAGE) { - let discount = (priceAfterItemDiscount * cart.coupon.value) / 100; - if (cart.coupon.maxDiscount && discount > cart.coupon.maxDiscount) { - discount = cart.coupon.maxDiscount; - } - return discount; - } - - return Math.min(cart.coupon.value, priceAfterItemDiscount); - } - - /** - * Calculate tax - */ - async calculateTax(restaurantId: string, amountAfterDiscounts: number): Promise { - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - const vat = restaurant?.vat ? Number(restaurant.vat) : 0; - if (!vat || vat <= 0) return 0; - return (Math.max(0, amountAfterDiscounts) * vat) / 100; - } - - /** - * Calculate delivery fee - */ - async calculateDeliveryFee(cart: Cart): Promise { - const deliveryMethodId = cart.deliveryMethodId; - if (!deliveryMethodId) return 0; - - const deliveryMethod = await this.em.findOne(Delivery, { id: deliveryMethodId }, { populate: ['restaurant'] }); - if (!deliveryMethod || !deliveryMethod.enabled) return 0; - - // If not distance based, return fixed delivery fee - if (deliveryMethod.deliveryFeeType !== DeliveryFeeTypeEnum.DISTANCE_BASED) { - return Number(deliveryMethod.deliveryFee) || 0; - } - - // For distance based calculation we need restaurant and user coordinates - const restaurant = (deliveryMethod as any).restaurant as Restaurant | undefined; - const userAddr = cart.userAddress; - - if (!restaurant || restaurant.latitude == null || restaurant.longitude == null) { - // fallback to configured fixed fee when coordinates are missing - return Number(deliveryMethod.deliveryFee) || 0; - } - - if (!userAddr || userAddr.latitude == null || userAddr.longitude == null) { - return Number(deliveryMethod.deliveryFee) || 0; - } - - const restLat = Number(restaurant.latitude); - const restLng = Number(restaurant.longitude); - const userLat = Number(userAddr.latitude); - const userLng = Number(userAddr.longitude); - - const distanceKm = GeographicUtils.getDistanceKmRounded(restLat, restLng, userLat, userLng); - - // Try to read either possible property names (legacy vs new) - const perKm = Number(deliveryMethod.perKilometerFee); - const minFee = Number(deliveryMethod.distanceBasedMinCost); - - let fee = 0; - if (perKm <= 0) { - fee = Number(deliveryMethod.deliveryFee) || 0; - } else { - fee = distanceKm * perKm; - } - - if (minFee > 0 && fee < minFee) fee = minFee; - - return Math.max(0, Number(fee)); - } - - /** - * Recalculate cart totals (including coupon discount and tax) - */ - async recalculateCartTotals(cart: Cart): Promise { - const { subTotal, itemsDiscount, totalItems } = this.calculateItemsTotals(cart); - cart.subTotal = subTotal; - cart.itemsDiscount = itemsDiscount; - - const couponDiscount = await this.calculateCouponDiscount(cart, subTotal, itemsDiscount); - cart.couponDiscount = couponDiscount; - cart.totalDiscount = couponDiscount + itemsDiscount; - - cart.tax = await this.calculateTax(cart.restaurantId, Math.max(0, subTotal - cart.totalDiscount)); - cart.deliveryFee = await this.calculateDeliveryFee(cart); - - // total = subtotal – totalDiscount + tax + deliveryFee - cart.total = Math.max(0, subTotal - cart.totalDiscount) + cart.tax + cart.deliveryFee; - cart.totalItems = totalItems; - cart.updatedAt = this.nowIso(); - } - - /** - * Get coupon restrictions or clear if invalid - */ - private async getCouponRestrictionsOrClear(cart: Cart): Promise<{ - isActive?: boolean; - startDate?: Date; - endDate?: Date; - foodCategories?: string[]; - foods?: string[]; - } | null> { - if (!cart.coupon) return null; - - try { - const c = await this.couponService.findById(cart.coupon.couponId); - const now = new Date(); - if (c.isActive === false) { - cart.coupon = undefined; - return null; - } - if (c.startDate && now < c.startDate) { - cart.coupon = undefined; - return null; - } - if (c.endDate && now > c.endDate) { - cart.coupon = undefined; - return null; - } - - return { - isActive: c.isActive, - startDate: c.startDate, - endDate: c.endDate, - foodCategories: c.foodCategories, - foods: c.foods, - }; - } catch { - cart.coupon = undefined; - return null; - } - } - - /** - * Get foods in cart with categories - */ - private async getFoodsInCartWithCategories(cart: Cart): Promise> { - const foodIds = cart.items.map(item => item.foodId); - if (foodIds.length === 0) return new Map(); - - const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] }); - return new Map(foods.map(f => [f.id, f])); - } - - /** - * Get current ISO timestamp - */ - private nowIso(): string { - return new Date().toISOString(); - } -} - diff --git a/src/modules/cart/providers/cart-item.service.ts b/src/modules/cart/providers/cart-item.service.ts deleted file mode 100644 index d513016..0000000 --- a/src/modules/cart/providers/cart-item.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { Food } from '../../foods/entities/food.entity'; -import { Cart, CartItem } from '../interfaces/cart.interface'; -import { CartValidationService } from './cart-validation.service'; -import { CartCalculationService } from './cart-calculation.service'; -import { CartMessage } from 'src/common/enums/message.enum'; - -@Injectable() -export class CartItemService { - constructor( - private readonly validationService: CartValidationService, - private readonly calculationService: CartCalculationService, - ) {} - - /** - * Create a new cart item from food - */ - createCartItem(food: Food, quantity: number): CartItem { - const itemPrice = food.price || 0; - const itemDiscount = food.discount || 0; - - return { - foodId: food.id, - foodTitle: food.title, - quantity, - price: itemPrice, - discount: itemDiscount, - totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity), - }; - } - - /** - * Build cart item from food (updating existing item if provided) - */ - buildCartItemFromFood(food: Food, quantity: number, previous?: CartItem): CartItem { - const itemPrice = food.price || 0; - const itemDiscount = food.discount || 0; - return { - ...(previous ?? ({} as CartItem)), - foodId: food.id, - foodTitle: food.title, - quantity, - price: itemPrice, - discount: itemDiscount, - totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity), - }; - } - - /** - * Get item index in cart - */ - getItemIndex(cart: Cart, foodId: string): number { - return cart.items.findIndex(item => item.foodId === foodId); - } - - /** - * Add or increment item in cart - */ - async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise { - const food = await this.validationService.validateAndGetFood(foodId, restaurantId, quantityToAdd); - - // Validate meal type compatibility - this.validationService.assertMealTypeCompatibility(food); - - // Validate weekday compatibility - this.validationService.assertWeekdayCompatibility(food); - - const index = this.getItemIndex(cart, food.id); - - if (index < 0) { - cart.items.push(this.createCartItem(food, quantityToAdd)); - return; - } - - const existingItem = cart.items[index]; - const newQuantity = existingItem.quantity + quantityToAdd; - this.validationService.validateStock(food, newQuantity); - cart.items[index] = this.buildCartItemFromFood(food, newQuantity, existingItem); - } - - /** - * Remove item from cart - */ - removeItemOrFail(cart: Cart, foodId: string): void { - const itemIndex = this.getItemIndex(cart, foodId); - if (itemIndex < 0) { - throw new NotFoundException(CartMessage.ITEM_NOT_FOUND); - } - cart.items.splice(itemIndex, 1); - } - - /** - * Decrement item quantity or remove if quantity reaches 0 - */ - async decrementOrRemoveItem(cart: Cart, foodId: string): Promise { - const itemIndex = this.getItemIndex(cart, foodId); - if (itemIndex < 0) { - throw new NotFoundException(CartMessage.ITEM_NOT_FOUND); - } - - const existingItem = cart.items[itemIndex]; - const newQuantity = existingItem.quantity - 1; - - if (newQuantity <= 0) { - cart.items.splice(itemIndex, 1); - return; - } - - const food = await this.validationService.getFoodOrFail(foodId); - cart.items[itemIndex] = this.buildCartItemFromFood(food, newQuantity, existingItem); - } -} - diff --git a/src/modules/cart/providers/cart-validation.service.ts b/src/modules/cart/providers/cart-validation.service.ts deleted file mode 100644 index 6af1065..0000000 --- a/src/modules/cart/providers/cart-validation.service.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Food } from '../../foods/entities/food.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { UserAddress } from '../../users/entities/user-address.entity'; -import { User } from '../../users/entities/user.entity'; -import { PaymentMethod } from '../../payments/entities/payment-method.entity'; -import { Delivery } from '../../delivery/entities/delivery.entity'; -import { DeliveryMethodEnum } from '../../delivery/interface/delivery'; -import { PointTransaction } from '../../users/entities/point-transaction.entity'; -import { Order } from '../../orders/entities/order.entity'; -import { OrderStatus } from '../../orders/interface/order.interface'; -import { Cart } from '../interfaces/cart.interface'; -import { GeographicUtils } from '../utils/geographic.utils'; -import { MealType } from '../../foods/interface/food.interface'; -import { CartMessage } from 'src/common/enums/message.enum'; -import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository'; - -@Injectable() -export class CartValidationService { - constructor( - private readonly em: EntityManager, - private readonly walletTransactionRepository: WalletTransactionRepository, - ) { } - - /** - * Validate food exists, belongs to restaurant, has inventory, and check stock - */ - async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise { - const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] }); - if (!food) { - throw new NotFoundException(CartMessage.FOOD_NOT_FOUND); - } - - if (food.restaurant.id !== restaurantId) { - throw new BadRequestException(CartMessage.FOOD_NOT_BELONGS_TO_RESTAURANT); - } - - if (!food.inventory) { - throw new BadRequestException(CartMessage.FOOD_NO_INVENTORY); - } - - this.validateStock(food, quantity); - return food; - } - - /** - * Validate stock availability for food - */ - validateStock(food: Food, quantity: number): void { - const availableStock = food.inventory!.availableStock; - if (availableStock < quantity) { - throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK + food.title); - } - } - - /** - * Get user or throw if not found - */ - async getUserOrFail(userId: string): Promise { - const user = await this.em.findOne(User, { id: userId }); - if (!user) { - throw new NotFoundException(CartMessage.USER_NOT_FOUND); - } - return user; - } - - /** - * Get user address or throw if not found - */ - async getUserAddressOrFail(addressId: string): Promise { - const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] }); - if (!address) { - throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND); - } - return address; - } - - /** - * Get food or throw if not found - */ - async getFoodOrFail(foodId: string): Promise { - const food = await this.em.findOne(Food, { id: foodId }); - if (!food) { - throw new NotFoundException(CartMessage.FOOD_NOT_FOUND); - } - return food; - } - - /** - * Get restaurant or throw if not found - */ - async getRestaurantOrFail(restaurantId: string): Promise { - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) { - throw new NotFoundException(CartMessage.RESTAURANT_NOT_FOUND); - } - return restaurant; - } - - /** - * Validate address belongs to user - */ - validateAddressOwnership(address: UserAddress, userId: string): void { - if (address.user.id !== userId) { - throw new BadRequestException(CartMessage.ADDRESS_NOT_BELONGS_TO_USER); - } - } - - /** - * Assert address is inside restaurant service area - */ - async assertAddressInsideServiceArea( - restaurantId: string, - latitude?: number | null, - longitude?: number | null, - ): Promise { - if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) { - throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES); - } - - const restaurant = await this.getRestaurantOrFail(restaurantId); - - const serviceArea = restaurant.serviceArea; - // If no service area is defined, assume service is available everywhere - if (!serviceArea || !serviceArea.coordinates || !Array.isArray(serviceArea.coordinates)) return; - - // GeoJSON coordinates: [ [ [lng, lat], ... ] ] -> take first ring - const rings = serviceArea.coordinates; - if (!rings || rings.length === 0 || !Array.isArray(rings[0])) return; - - const ring = rings[0]; - const point: [number, number] = [Number(longitude), Number(latitude)]; - - // Ensure polygon has the correct tuple typing ([lng, lat] pairs) - const polygon: [number, number][] = ring.map( - coord => [Number(coord[0] ?? 0), Number(coord[1] ?? 0)] as [number, number], - ); - - if (!GeographicUtils.isPointInPolygon(point, polygon)) { - throw new BadRequestException(CartMessage.ADDRESS_OUTSIDE_SERVICE_AREA); - } - } - - /** - * Get delivery method for restaurant or throw if not found - */ - async getDeliveryMethodForRestaurantOrFail( - restaurantId: string, - deliveryMethodId: string, - ): Promise { - const deliveryMethod = await this.em.findOne(Delivery, { - id: deliveryMethodId, - restaurant: { id: restaurantId }, - }); - - if (!deliveryMethod) { - throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT); - } - - return deliveryMethod; - } - - /** - * Get enabled delivery method or throw if not found or disabled - */ - async getEnabledDeliveryMethodOrFail(restaurantId: string, deliveryMethodId: string): Promise { - const deliveryMethod = await this.em.findOne( - Delivery, - { id: deliveryMethodId, restaurant: { id: restaurantId } }, - { populate: ['restaurant'] }, - ); - if (!deliveryMethod) { - throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND); - } - if (!deliveryMethod.enabled) { - throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED); - } - return deliveryMethod; - } - - /** - * Assert delivery method matches expected type - */ - assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void { - if (actual !== expected) { - throw new BadRequestException(message); - } - } - - /** - * Require delivery method ID or throw - */ - requireDeliveryMethodId(cart: Cart, message: string): string { - if (!cart.deliveryMethodId) { - throw new BadRequestException(message); - } - return cart.deliveryMethodId; - } - - /** - * Get enabled payment method or throw if not found or disabled - */ - async getEnabledPaymentMethodOrFail(restaurantId: string, paymentMethodId: string): Promise { - const paymentMethod = await this.em.findOne( - PaymentMethod, - { id: paymentMethodId, restaurant: { id: restaurantId } }, - { populate: ['restaurant'] }, - ); - if (!paymentMethod) { - throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND); - } - if (!paymentMethod.enabled) { - throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED); - } - return paymentMethod; - } - - /** - * Assert wallet has enough balance - */ - async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise { - const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restaurantId); - - if (balance < amount) { - throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT); - } - } - - /** - * Assert coupon usage limit - */ - async assertCouponUsageLimit(userId: string, couponId: string, maxUsesPerUser?: number): Promise { - if (!maxUsesPerUser) return; - - const ordersThatUsedTheCouponCount = await this.em.count(Order, { - user: { id: userId }, - couponDetail: { couponId: couponId }, - status: { $ne: OrderStatus.CANCELED }, - deletedAt: null, - }); - - if (ordersThatUsedTheCouponCount >= maxUsesPerUser) { - throw new BadRequestException(CartMessage.COUPON_MAX_USES_REACHED); - } - } - - /** - * Assert coupon matches cart if restricted - */ - async assertCouponMatchesCartIfRestricted( - cart: Cart, - foodCategories?: string[] | null, - foods?: string[] | null, - ): Promise { - const categoryRestriction = foodCategories?.filter(Boolean) ?? []; - const foodRestriction = foods?.filter(Boolean) ?? []; - const hasCategoryRestriction = categoryRestriction.length > 0; - const hasFoodRestriction = foodRestriction.length > 0; - if (!hasCategoryRestriction && !hasFoodRestriction) return; - - const foodMap = await this.getFoodsInCartWithCategories(cart); - - for (const item of cart.items) { - const food = foodMap.get(item.foodId); - if (!food) continue; - - const matchesCategory = hasCategoryRestriction && food.category && categoryRestriction.includes(food.category.id); - const matchesFood = hasFoodRestriction && foodRestriction.includes(food.id); - if (matchesCategory || matchesFood) return; - } - - throw new BadRequestException(CartMessage.COUPON_CANNOT_BE_APPLIED); - } - - /** - * Get foods in cart with categories - */ - async getFoodsInCartWithCategories(cart: Cart): Promise> { - const foodIds = cart.items.map(item => item.foodId); - if (foodIds.length === 0) return new Map(); - - const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] }); - return new Map(foods.map(f => [f.id, f])); - } - - /** - * Assert all foods in cart have pickupServe true when delivery method is courier - */ - async assertAllFoodsHavePickupServeForCourier(cart: Cart, deliveryMethod: DeliveryMethodEnum): Promise { - if (deliveryMethod !== DeliveryMethodEnum.DeliveryCourier) { - return; - } - - if (cart.items.length === 0) { - return; - } - - const foodIds = cart.items.map(item => item.foodId); - const foods = await this.em.find(Food, { id: { $in: foodIds } }); - - const foodsWithoutPickupServe = foods.filter(food => !food.pickupServe); - - if (foodsWithoutPickupServe.length > 0) { - const foodTitles = foodsWithoutPickupServe.map(f => f.title || f.id).join(', '); - throw new BadRequestException( - CartMessage.FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER.replace('[foodTitles]', foodTitles), - ); - } - } - - /** - * Get current Iran time context (weekday and meal type) - */ - private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } { - const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' })); - const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday - const currentHour = nowInIran.getHours(); - - // Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6 - // JavaScript: Sunday=0, Monday=1, ..., Saturday=6 - // Iran week: Saturday=0, Sunday=1, ..., Friday=6 - const iranWeekDay = (weekDay + 1) % 7; - - const mealType = this.getMealTypeByHour(currentHour); - - return { iranWeekDay, mealType }; - } - - /** - * Determine meal type based on current hour in Iran timezone. - * @param hour - Current hour (0-23) - * @returns Meal type or null if outside meal hours - */ - private getMealTypeByHour(hour: number): MealType | null { - const MEAL_TIME_RANGES = { - BREAKFAST: { start: 6, end: 11 }, - LUNCH: { start: 11, end: 15 }, - SNACK: { start: 15, end: 19 }, - DINNER: { start: 19, end: 24 }, - } as const; - - if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) { - return MealType.BREAKFAST; - } - if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) { - return MealType.LUNCH; - } - if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) { - return MealType.SNACK; - } - if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) { - return MealType.DINNER; - } - - return null; - } - - /** - * Assert meal type compatibility - if food has mealTypes, current meal time must be in that array - */ - assertMealTypeCompatibility(food: Food): void { - // If food has no meal type restrictions, allow it - if (!food.mealTypes || food.mealTypes.length === 0) { - return; - } - - const { mealType } = this.getCurrentIranTimeContext(); - - // If current time is outside meal hours, throw error - if (mealType === null) { - const foodTitle = food.title || food.id; - throw new BadRequestException( - CartMessage.FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES.replace('[foodTitle]', foodTitle), - ); - } - - // Check if current meal type is in food's allowed meal types - if (!food.mealTypes.includes(mealType)) { - const foodTitle = food.title || food.id; - const mealTypeMap: Record = { - [MealType.BREAKFAST]: 'صبحانه', - [MealType.LUNCH]: 'ناهار', - [MealType.SNACK]: 'عصرانه', - [MealType.DINNER]: 'شام', - }; - const allowedMealTypes = food.mealTypes.map(mt => mealTypeMap[mt]).join(', '); - const currentMealType = mealType ? mealTypeMap[mealType] : mealType; - throw new BadRequestException( - CartMessage.FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES.replace('[foodTitle]', foodTitle) - .replace('[allowedMealTypes]', allowedMealTypes) - .replace('[mealType]', currentMealType), - ); - } - } - - /** - * Assert weekday compatibility - current weekday must be in food's weekDays array - */ - assertWeekdayCompatibility(food: Food): void { - // If food has no weekday restrictions (empty array or all days), allow it - if (!food.weekDays || food.weekDays.length === 0 || food.weekDays.length === 7) { - return; - } - - const { iranWeekDay } = this.getCurrentIranTimeContext(); - - // Check if current weekday is in food's allowed weekdays - if (!food.weekDays.includes(iranWeekDay)) { - const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه']; - const currentDayName = dayNames[iranWeekDay]; - const allowedDays = food.weekDays.map(day => dayNames[day]).join(', '); - const foodTitle = food.title || food.id; - throw new BadRequestException( - CartMessage.FOOD_ONLY_AVAILABLE_ON_DAYS.replace('[foodTitle]', foodTitle) - .replace('[allowedDays]', allowedDays) - .replace('[currentDay]', currentDayName), - ); - } - } -} - diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts deleted file mode 100644 index e791c62..0000000 --- a/src/modules/cart/providers/cart.service.ts +++ /dev/null @@ -1,487 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { DeliveryMethodEnum } from '../../delivery/interface/delivery'; -import { PaymentMethodEnum } from '../../payments/interface/payment'; -import { OrderCouponDetail } from '../../orders/interface/order.interface'; -import { Cart } from '../interfaces/cart.interface'; -import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto'; -import { ApplyCouponDto } from '../dto/apply-coupon.dto'; - import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto'; -import { CouponService } from '../../coupons/providers/coupon.service'; -import { CartRepository } from '../repositories/cart.repository'; -import { CartValidationService } from './cart-validation.service'; -import { CartCalculationService } from './cart-calculation.service'; -import { CartItemService } from './cart-item.service'; -import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto'; -import { CartMessage } from 'src/common/enums/message.enum'; - -@Injectable() -export class CartService { - constructor( - private readonly cartRepository: CartRepository, - private readonly validationService: CartValidationService, - private readonly calculationService: CartCalculationService, - private readonly itemService: CartItemService, - private readonly couponService: CouponService, - ) { } - - /** - * Set all cart parameters at once - */ - async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise { - const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params; - - // get existing cart (or throw) - const cart = await this.findOneOrFail(userId, restaurantId); - - // Validate and get delivery method - const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail( - restaurantId, - deliveryMethodId, - ); - cart.deliveryMethodId = deliveryMethodId; - - // Handle each delivery method with its specific requirements - switch (deliveryMethod.method) { - case DeliveryMethodEnum.DineIn: - // DineIn requires table number and clears incompatible fields - if (!tableNumber) { - throw new BadRequestException(CartMessage.TABLE_NUMBER_REQUIRED); - } - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn); - cart.tableNumber = tableNumber; - break; - - case DeliveryMethodEnum.CustomerPickup: - // CustomerPickup just clears incompatible fields - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.CustomerPickup); - break; - - case DeliveryMethodEnum.DeliveryCar: - // DeliveryCar requires carAddress and clears incompatible fields - if (!carAddress) { - throw new BadRequestException(CartMessage.CAR_ADDRESS_REQUIRED); - } - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); - const user = await this.validationService.getUserOrFail(userId); - cart.carAddress = { - carModel: carAddress.carModel, - carColor: carAddress.carColor, - plateNumber: carAddress.plateNumber, - phone: user.phone, - }; - break; - - case DeliveryMethodEnum.DeliveryCourier: - // DeliveryCourier requires addressId and clears incompatible fields - if (!addressId) { - throw new BadRequestException(CartMessage.ADDRESS_REQUIRED); - } - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier); - const address = await this.validationService.getUserAddressOrFail(addressId); - this.validationService.validateAddressOwnership(address, userId); - - // ensure address is within restaurant service area - await this.validationService.assertAddressInsideServiceArea( - restaurantId, - address.latitude, - address.longitude, - ); - - cart.userAddress = { - address: address.address, - latitude: address.latitude, - longitude: address.longitude, - city: address.city, - province: address.province || '', - postalCode: address.postalCode || undefined, - fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(), - phone: address.user.phone, - }; - break; - } - - // Validate and set payment method - const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail( - restaurantId, - paymentMethodId, - ); - - // Recalculate totals first so wallet check uses up-to-date total (delivery method may have changed above). - await this.calculationService.recalculateCartTotals(cart); - - if (paymentMethod.method === PaymentMethodEnum.Wallet) { - await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total); - } - - cart.paymentMethodId = paymentMethodId; - - // Set description (if provided) - if (description !== undefined) { - cart.description = description; - } - - // Final validation: if effective delivery method is courier, ensure all foods have pickupServe - await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method); - - // Final recalculation + save and return cart - return this.recalculateAndSaveCart(cart); - } - - /** - * Get or create cart for user and restaurant - */ - async getOrCreateCart(userId: string, restaurantId: string): Promise { - const existingCart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId); - if (existingCart) { - return existingCart; - } - - // Find restaurant - const restaurant = await this.validationService.getRestaurantOrFail(restaurantId); - - // Create new cart - const now = this.nowIso(); - const cart: Cart = { - userId, - restaurantId, - restaurantName: restaurant.name, - items: [], - deliveryFee: 0, - subTotal: 0, - itemsDiscount: 0, - couponDiscount: 0, - totalDiscount: 0, - tax: 0, - total: 0, - totalItems: 0, - createdAt: now, - updatedAt: now, - }; - - await this.cartRepository.save(cart); - return cart; - } - - /** - * Find cart by user and restaurant - */ - async findOneOrFail(userId: string, restaurantId: string): Promise { - const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId); - if (!cart) { - throw new NotFoundException(CartMessage.NOT_FOUND); - } - return cart; - } - - /** - * Increment item quantity in cart - */ - async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise { - const cart = await this.getOrCreateCart(userId, restaurantId); - await this.itemService.addOrIncrementItem(cart, foodId, restaurantId, quantity); - return this.recalculateAndSaveCart(cart); - } - - /** - * Decrement item quantity in cart by 1 (removes item if quantity reaches 0) - */ - async decrementItem(userId: string, restaurantId: string, foodId: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - await this.itemService.decrementOrRemoveItem(cart, foodId); - return this.recalculateAndSaveCart(cart); - } - - /** - * Bulk add items to cart (increments quantity if items exist) - */ - async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise { - const cart = await this.getOrCreateCart(userId, restaurantId); - - for (const addItemDto of bulkAddItemsDto.items) { - await this.itemService.addOrIncrementItem(cart, addItemDto.foodId, restaurantId, addItemDto.quantity); - } - - return this.recalculateAndSaveCart(cart); - } - - /** - * Remove item from cart - */ - async removeItem(userId: string, restaurantId: string, foodId: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - this.itemService.removeItemOrFail(cart, foodId); - return this.recalculateAndSaveCart(cart); - } - - /** - * Apply coupon to cart - */ - async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const orderAmount = cart.subTotal - cart.itemsDiscount; - const user = await this.validationService.getUserOrFail(userId); - // check if coupon is valid and belong to the restaurant - const { valid, coupon, message } = await this.couponService.validateCoupon( - applyCouponDto.code, - restaurantId, - Number(orderAmount), - user.phone, - ); - - if (!valid) { - throw new BadRequestException(message || CartMessage.COUPON_NOT_FOUND); - } - - if (!coupon) { - throw new BadRequestException(CartMessage.COUPON_NOT_FOUND); - } - - // Check maxUsesPerUser limit by counting how many orders this user has with this coupon - await this.validationService.assertCouponUsageLimit(userId, coupon.id, coupon.maxUsesPerUser); - await this.validationService.assertCouponMatchesCartIfRestricted( - cart, - coupon.foodCategories, - coupon.foods, - ); - - const couponDetail: OrderCouponDetail = { - couponId: coupon.id, - couponName: coupon.name, - couponCode: coupon.code, - type: coupon.type, - value: coupon.value, - maxDiscount: coupon.maxDiscount, - }; - - cart.coupon = couponDetail; - return this.recalculateAndSaveCart(cart); - } - - /** - * Remove coupon from cart - */ - async removeCoupon(userId: string, restaurantId: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - cart.coupon = undefined; - return this.recalculateAndSaveCart(cart); - } - - /** - * Set address for cart - */ - async setAddress(userId: string, restaurantId: string, addressId: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const deliveryMethodId = this.validationService.requireDeliveryMethodId( - cart, - 'Delivery method must be set before setting address', - ); - const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail( - restaurantId, - deliveryMethodId, - ); - this.validationService.assertDeliveryMethod( - deliveryMethod.method, - DeliveryMethodEnum.DeliveryCourier, - 'Delivery method must be DeliveryCourier', - ); - - // Find and validate address belongs to user - const address = await this.validationService.getUserAddressOrFail(addressId); - - // Verify address belongs to the user - this.validationService.validateAddressOwnership(address, userId); - - // ensure address is within restaurant service area - await this.validationService.assertAddressInsideServiceArea( - restaurantId, - address.latitude, - address.longitude, - ); - - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier); - cart.userAddress = { - address: address.address, - latitude: address.latitude, - longitude: address.longitude, - city: address.city, - province: address.province || '', - postalCode: address.postalCode || undefined, - fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(), - phone: address.user.phone, - }; - return this.saveTouchedCart(cart); - } - - /** - * Set payment method for cart - */ - async setPaymentMethod( - userId: string, - restaurantId: string, - paymentMethodId: string, - ): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail( - restaurantId, - paymentMethodId, - ); - if (paymentMethod.method === PaymentMethodEnum.Wallet) { - await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total); - } - cart.paymentMethodId = paymentMethodId; - return this.recalculateAndSaveCart(cart); - } - - /** - * Set delivery method for cart - */ - async setDeliveryMethod( - userId: string, - restaurantId: string, - deliveryMethodId: string, - ): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail( - restaurantId, - deliveryMethodId, - ); - - cart.deliveryMethodId = deliveryMethodId; - this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method); - - return this.recalculateAndSaveCart(cart); - } - - /** - * Set description for cart - */ - async setDescription(userId: string, restaurantId: string, description: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - cart.description = description; - return this.saveTouchedCart(cart); - } - - /** - * Set table number for cart - */ - async setTableNumber(userId: string, restaurantId: string, tableNumber: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const deliveryMethodId = this.validationService.requireDeliveryMethodId( - cart, - 'Delivery method must be set before setting table number', - ); - const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail( - restaurantId, - deliveryMethodId, - ); - this.validationService.assertDeliveryMethod( - deliveryMethod.method, - DeliveryMethodEnum.DineIn, - 'Delivery method must be DineIn', - ); - - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn); - cart.tableNumber = tableNumber; - return this.saveTouchedCart(cart); - } - - /** - * Set car delivery for cart - */ - async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const deliveryMethodId = this.validationService.requireDeliveryMethodId( - cart, - 'Delivery method must be set before setting car delivery', - ); - const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail( - restaurantId, - deliveryMethodId, - ); - this.validationService.assertDeliveryMethod( - deliveryMethod.method, - DeliveryMethodEnum.DeliveryCar, - 'Delivery method must be DeliveryCar', - ); - const user = await this.validationService.getUserOrFail(userId); - - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); - cart.carAddress = { - carModel: setCarDeliveryDto.carModel, - carColor: setCarDeliveryDto.carColor, - plateNumber: setCarDeliveryDto.plateNumber, - phone: user.phone, - }; - return this.saveTouchedCart(cart); - } - - /** - * Clear cart from cache - */ - async clearCart(userId: string, restaurantId: string): Promise { - return this.cartRepository.delete(userId, restaurantId); - } - - /** - * Clears cart fields that are not applicable for a given delivery method. - */ - private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void { - if (method === DeliveryMethodEnum.DineIn) { - cart.userAddress = null; - cart.carAddress = null; - return; - } - - if (method === DeliveryMethodEnum.CustomerPickup) { - cart.userAddress = null; - cart.carAddress = null; - cart.tableNumber = undefined; - return; - } - - if (method === DeliveryMethodEnum.DeliveryCourier) { - cart.carAddress = null; - cart.tableNumber = undefined; - return; - } - - if (method === DeliveryMethodEnum.DeliveryCar) { - cart.userAddress = null; - cart.tableNumber = undefined; - return; - } - } - - /** - * Save cart with updated timestamp - */ - private async saveTouchedCart(cart: Cart): Promise { - cart.updatedAt = this.nowIso(); - await this.cartRepository.save(cart); - return cart; - } - - /** - * Recalculate cart totals and save - */ - private async recalculateAndSaveCart(cart: Cart): Promise { - await this.calculationService.recalculateCartTotals(cart); - await this.cartRepository.save(cart); - return cart; - } - - /** - * Get current ISO timestamp - */ - private nowIso(): string { - return new Date().toISOString(); - } -} diff --git a/src/modules/cart/repositories/cart.repository.ts b/src/modules/cart/repositories/cart.repository.ts deleted file mode 100644 index 4c1a6f4..0000000 --- a/src/modules/cart/repositories/cart.repository.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { CacheService } from '../../utils/cache.service'; -import { Cart } from '../interfaces/cart.interface'; - -@Injectable() -export class CartRepository { - private readonly CART_TTL = 3600; // 1 hour in seconds - private readonly CART_KEY_PREFIX = 'cart'; - - constructor(private readonly cacheService: CacheService) {} - - /** - * Get cart by user and restaurant - */ - async findByUserAndRestaurant(userId: string, restaurantId: string): Promise { - const cacheKey = this.getCacheKey(userId, restaurantId); - const cachedCart = await this.cacheService.get(cacheKey); - - if (cachedCart) { - try { - const parsed: unknown = JSON.parse(cachedCart); - if (this.isCart(parsed) && parsed.userId === userId && parsed.restaurantId === restaurantId) { - return parsed; - } - } catch { - // If parsing fails, return null - } - } - - return null; - } - - /** - * Save cart to cache - */ - async save(cart: Cart): Promise { - const cacheKey = this.getCacheKey(cart.userId, cart.restaurantId); - await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL); - } - - /** - * Delete cart from cache - */ - async delete(userId: string, restaurantId: string): Promise { - const cacheKey = this.getCacheKey(userId, restaurantId); - await this.cacheService.del(cacheKey); - } - - /** - * Generate cache key for cart - */ - private getCacheKey(userId: string, restaurantId: string): string { - return `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`; - } - - /** - * Type guard to check if an object is a Cart - */ - private isCart(obj: unknown): obj is Cart { - if (typeof obj !== 'object' || obj === null) { - return false; - } - const cart = obj as Record; - return ( - typeof cart.userId === 'string' && - typeof cart.restaurantId === 'string' && - Array.isArray(cart.items) && - typeof cart.subTotal === 'number' && - typeof cart.itemsDiscount === 'number' && - typeof cart.couponDiscount === 'number' && - typeof cart.totalDiscount === 'number' && - typeof cart.tax === 'number' && - typeof cart.deliveryFee === 'number' && - typeof cart.total === 'number' && - typeof cart.totalItems === 'number' && - typeof cart.createdAt === 'string' && - typeof cart.updatedAt === 'string' - ); - } -} - diff --git a/src/modules/cart/utils/geographic.utils.ts b/src/modules/cart/utils/geographic.utils.ts deleted file mode 100644 index 8b2d888..0000000 --- a/src/modules/cart/utils/geographic.utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Utility class for geographic calculations - */ -export class GeographicUtils { - /** - * Check if a point is inside a polygon using ray-casting algorithm - * @param point - [longitude, latitude] - * @param polygon - Array of [longitude, latitude] pairs - */ - static isPointInPolygon(point: [number, number], polygon: [number, number][]): boolean { - const x = point[0]; - const y = point[1]; - let inside = false; - - for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - const xi = polygon[i][0]; - const yi = polygon[i][1]; - const xj = polygon[j][0]; - const yj = polygon[j][1]; - - const intersect = - yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi + Number.EPSILON) + xi; - if (intersect) inside = !inside; - } - - return inside; - } - - /** - * Convert degrees to radians - */ - private static toRad(value: number): number { - return (value * Math.PI) / 180; - } - - /** - * Calculate distance between two points in kilometers using Haversine formula - */ - static getDistanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number { - const R = 6371; // Earth radius in KM - - const dLat = this.toRad(lat2 - lat1); - const dLng = this.toRad(lng2 - lng1); - - const a = - Math.sin(dLat / 2) ** 2 + - Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLng / 2) ** 2; - - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; - } - - /** - * Calculate distance between two points in kilometers (rounded) - */ - static getDistanceKmRounded( - lat1: number, - lng1: number, - lat2: number, - lng2: number, - precision = 2, - ): number { - const distance = this.getDistanceKm(lat1, lng1, lat2, lng2); - return Number(distance.toFixed(precision)); - } -} - diff --git a/src/modules/foods/crone/food.crone.ts b/src/modules/foods/crone/food.crone.ts index 5749702..41bc0f5 100644 --- a/src/modules/foods/crone/food.crone.ts +++ b/src/modules/foods/crone/food.crone.ts @@ -1,8 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { EntityManager } from '@mikro-orm/postgresql'; -import { Inventory } from '../../inventory/entities/inventory.entity'; - + @Injectable() export class FoodStockCrone { private readonly logger = new Logger(FoodStockCrone.name); @@ -18,22 +17,10 @@ export class FoodStockCrone { try { this.logger.debug('Starting daily inventory reset (availableStock = totalStock)'); - const inventories = await this.em.find(Inventory, {}); - if (!inventories || inventories.length === 0) { - this.logger.debug('No inventory records found to reset'); - return; - } - - this.logger.log(`Resetting available stock for ${inventories.length} inventory records`); - + + await this.em.transactional(async em => { - for (const inv of inventories) { - // reload inside transaction to avoid concurrency issues - const record = await em.findOne(Inventory, { id: inv.id }); - if (!record) continue; - record.availableStock = record.totalStock; - em.persist(record); - } + await em.flush(); }); diff --git a/src/modules/foods/entities/food.entity.ts b/src/modules/foods/entities/food.entity.ts index 14443ae..f8a046f 100644 --- a/src/modules/foods/entities/food.entity.ts +++ b/src/modules/foods/entities/food.entity.ts @@ -2,8 +2,7 @@ import { Cascade, Collection, Entity, Index, ManyToOne, OneToMany, Property, One import { Category } from './category.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; -import { Review } from 'src/modules/review/entities/review.entity'; -import { Inventory } from 'src/modules/inventory/entities/inventory.entity'; + import { MealType } from '../interface/food.interface'; import { Favorite } from './favorite.entity'; @@ -18,15 +17,6 @@ export class Food extends BaseEntity { @ManyToOne(() => Category) category: Category; - @OneToMany(() => Review, review => review.food, { cascade: [Cascade.ALL], orphanRemoval: true }) - reviews = new Collection(this); - - @OneToOne(() => Inventory, { - mappedBy: 'food', - nullable: true, - }) - inventory?: Inventory; - @OneToMany(() => Favorite, favorite => favorite.food) favorites = new Collection(this); diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index 2972052..7428334 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -11,7 +11,6 @@ import { RestRepository } from 'src/modules/restaurants/repositories/rest.reposi import { CacheService } from '../../utils/cache.service'; import { Favorite } from '../entities/favorite.entity'; import { MealType } from '../interface/food.interface'; -import { Inventory } from 'src/modules/inventory/entities/inventory.entity'; @Injectable() export class FoodService { @@ -33,7 +32,7 @@ export class FoodService { throw new NotFoundException(CategoryMessage.NOT_FOUND); } - const { food, inventory } = await this.em.transactional(async em => { + const { food } = await this.em.transactional(async em => { // prepare data with defaults for required fields so repository.create typing is satisfied const data: RequiredEntityData = { desc: rest.desc, @@ -58,23 +57,18 @@ export class FoodService { }; const food = em.create(Food, data); - const newInventoryRecord = em.create(Inventory, { - food: food, - availableStock: dailyStock, - totalStock: dailyStock, - }); + await em.flush(); - return { food, inventory: newInventoryRecord }; + return { food }; }); // Re-load created entities with the primary EM to ensure they're attached const savedFood = food?.id ? await this.foodRepository.findOne({ id: food.id }, { populate: ['category', 'restaurant'] }) : null; - const savedInventory = inventory?.id ? await this.em.findOne(Inventory, { id: inventory.id }) : inventory; - return { food: savedFood ?? food, inventory: savedInventory ?? inventory }; + return { food: savedFood ?? food }; } findAll(restId: string, dto: FindFoodsDto) { @@ -85,7 +79,7 @@ export class FoodService { * Public food detail (only active foods are visible). */ async findPublicById(foodId: string, userId?: string): Promise { - const food = await this.foodRepository.findOne({ id: foodId, isActive: true }, { populate: ['category', 'inventory'] }); + 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) { @@ -101,7 +95,7 @@ export class FoodService { * Admin food detail (scoped to the authenticated restaurant). */ async findAdminById(restId: string, id: string): Promise { - const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['category', 'inventory'] }); + const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['category',] }); if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND); return food; @@ -206,18 +200,6 @@ export class FoodService { await this.em.transactional(async em => { await em.persistAndFlush(food); - if (typeof dailyStock !== 'undefined') { - const inventoryRecord = await em.findOne(Inventory, { food: food }); - if (inventoryRecord) { - inventoryRecord.totalStock = dailyStock; - } else { - em.create(Inventory, { - food: food, - availableStock: dailyStock, - totalStock: dailyStock, - }); - } - } await em.flush(); }); diff --git a/src/modules/foods/repositories/food.repository.ts b/src/modules/foods/repositories/food.repository.ts index 3957061..970d2e2 100644 --- a/src/modules/foods/repositories/food.repository.ts +++ b/src/modules/foods/repositories/food.repository.ts @@ -48,7 +48,7 @@ export class FoodRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['category','inventory'], + populate: ['category' ], }); const totalPages = Math.ceil(total / limit); diff --git a/src/modules/icons/controllers/icons.controller.ts b/src/modules/icons/controllers/icons.controller.ts deleted file mode 100644 index bdc0c7c..0000000 --- a/src/modules/icons/controllers/icons.controller.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; -import { IconService } from '../providers/icon.service'; -import { GroupService } from '../providers/group.service'; -import { CreateIconDto } from '../dto/create-icon.dto'; -import { UpdateIconDto } from '../dto/update-icon.dto'; -import { CreateGroupDto } from '../dto/create-group.dto'; -import { UpdateGroupDto } from '../dto/update-group.dto'; -import { ApiTags, ApiOperation, ApiParam, ApiBody, ApiBearerAuth } from '@nestjs/swagger'; -import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard'; -import { UseGuards } from '@nestjs/common'; -import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; - -@ApiTags('icons') -@Controller() -export class IconsController { - constructor( - private readonly iconService: IconService, - private readonly groupService: GroupService, - ) {} - - @Get('admin/groups/icons') - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get all icons' }) - findAllPublicIcons() { - return this.groupService.findAllGroups(); - } - - /** - * Super Admin Endpoints - */ - @Post('super-admin/icons') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Create icon' }) - @ApiBody({ type: CreateIconDto }) - createIcon(@Body() dto: CreateIconDto) { - return this.iconService.create(dto); - } - - @Get('super-admin/icons') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Get all icons' }) - findAllIcons() { - return this.iconService.findAll(); - } - - @Get('super-admin/icons/:id') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Get icon by id' }) - @ApiParam({ name: 'id', required: true, type: String }) - findOneIcon(@Param('id') id: string) { - return this.iconService.findOne(id); - } - - @Patch('super-admin/icons/:id') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Update icon' }) - @ApiParam({ name: 'id' }) - @ApiBody({ type: UpdateIconDto }) - updateIcon(@Param('id') id: string, @Body() dto: UpdateIconDto) { - return this.iconService.update(id, dto); - } - - @Delete('super-admin/icons/:id') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Delete icon' }) - @ApiParam({ name: 'id' }) - removeIcon(@Param('id') id: string) { - return this.iconService.remove(id); - } - - // Group endpoints (must come before :id routes to avoid route conflicts) - @Post('super-admin/icons/groups') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Create icon group' }) - @ApiBody({ type: CreateGroupDto }) - createGroup(@Body() dto: CreateGroupDto) { - return this.groupService.create(dto); - } - - @Get('super-admin/icons/groups') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Get all icon groups' }) - async findAllGroups() { - console.log('find icona'); - const a = await this.groupService.findAllGroups(); - console.log(a); - return a; - } - - @Get('super-admin/icons/groups/:id') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Get icon group by id' }) - @ApiParam({ name: 'id', required: true, type: String }) - findOneGroup(@Param('id') id: string) { - return this.groupService.findOne(id); - } - - @Patch('super-admin/icons/groups/:id') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Update icon group' }) - @ApiParam({ name: 'id' }) - updateGroup(@Param('id') id: string, @Body() dto: UpdateGroupDto) { - return this.groupService.update(id, dto); - } - - @Delete('super-admin/icons/groups/:id') - @UseGuards(SuperAdminAuthGuard) - @ApiOperation({ summary: 'Delete icon group' }) - @ApiParam({ name: 'id' }) - removeGroup(@Param('id') id: string) { - return this.groupService.remove(id); - } -} diff --git a/src/modules/icons/dto/create-group.dto.ts b/src/modules/icons/dto/create-group.dto.ts deleted file mode 100644 index 1cb0d72..0000000 --- a/src/modules/icons/dto/create-group.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateGroupDto { - @IsString() - @ApiProperty({ example: 'Social Media' }) - name!: string; -} - diff --git a/src/modules/icons/dto/create-icon.dto.ts b/src/modules/icons/dto/create-icon.dto.ts deleted file mode 100644 index 5c06089..0000000 --- a/src/modules/icons/dto/create-icon.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateIconDto { - @IsString() - @ApiProperty({ example: 'https://example.com/icons/facebook.svg' }) - url!: string; - - @IsString() - @ApiProperty({ example: 'group-id-here' }) - groupId!: string; -} diff --git a/src/modules/icons/dto/update-group.dto.ts b/src/modules/icons/dto/update-group.dto.ts deleted file mode 100644 index 8174604..0000000 --- a/src/modules/icons/dto/update-group.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateGroupDto } from './create-group.dto'; - -export class UpdateGroupDto extends PartialType(CreateGroupDto) {} - diff --git a/src/modules/icons/dto/update-icon.dto.ts b/src/modules/icons/dto/update-icon.dto.ts deleted file mode 100644 index 6ad1b53..0000000 --- a/src/modules/icons/dto/update-icon.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateIconDto } from './create-icon.dto'; - -export class UpdateIconDto extends PartialType(CreateIconDto) {} diff --git a/src/modules/icons/entities/group.entity.ts b/src/modules/icons/entities/group.entity.ts deleted file mode 100644 index a62c4e4..0000000 --- a/src/modules/icons/entities/group.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Entity, Index, Property, Collection, OneToMany, Cascade } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Icon } from './icon.entity'; - -@Entity({ tableName: 'icon_groups' }) -export class Group extends BaseEntity { - @Property() - name!: string; - - @OneToMany(() => Icon, icon => icon.group,{ - cascade: [Cascade.ALL], - orphanRemoval: true, - }) - icons = new Collection(this); -} diff --git a/src/modules/icons/entities/icon.entity.ts b/src/modules/icons/entities/icon.entity.ts deleted file mode 100644 index f848d7a..0000000 --- a/src/modules/icons/entities/icon.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Entity, Index, Property, ManyToOne } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Group } from './group.entity'; - -@Entity({ tableName: 'icons' }) -@Index({ properties: ['group'] }) -export class Icon extends BaseEntity { - @Property() - url!: string; - - @ManyToOne(() => Group) - group!: Group; -} diff --git a/src/modules/icons/icons.module.ts b/src/modules/icons/icons.module.ts deleted file mode 100644 index 2e8931a..0000000 --- a/src/modules/icons/icons.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { Icon } from './entities/icon.entity'; -import { Group } from './entities/group.entity'; -import { IconService } from './providers/icon.service'; -import { GroupService } from './providers/group.service'; -import { IconRepository } from './repositories/icon.repository'; -import { GroupRepository } from './repositories/group.repository'; -import { IconsController } from './controllers/icons.controller'; -import { JwtModule } from '@nestjs/jwt'; - -@Module({ - imports: [MikroOrmModule.forFeature([Icon, Group]), JwtModule], - controllers: [IconsController], - providers: [IconService, GroupService, IconRepository, GroupRepository], - exports: [IconRepository, GroupRepository, IconService, GroupService], -}) -export class IconsModule {} diff --git a/src/modules/icons/providers/group.service.ts b/src/modules/icons/providers/group.service.ts deleted file mode 100644 index 625bfb4..0000000 --- a/src/modules/icons/providers/group.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateGroupDto } from '../dto/create-group.dto'; -import { UpdateGroupDto } from '../dto/update-group.dto'; -import { GroupRepository } from '../repositories/group.repository'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { RequiredEntityData } from '@mikro-orm/core'; -import { Group } from '../entities/group.entity'; -import { GroupMessage } from 'src/common/enums/message.enum'; - -@Injectable() -export class GroupService { - constructor( - private readonly groupRepository: GroupRepository, - private readonly em: EntityManager, - ) {} - - async create(dto: CreateGroupDto): Promise { - const data: RequiredEntityData = { - name: dto.name, - }; - - const group = this.groupRepository.create(data); - await this.em.persistAndFlush(group); - return group; - } - - async findOne(id: string): Promise { - const group = await this.groupRepository.findOne({ id }, { populate: ['icons'] }); - if (!group) { - throw new NotFoundException(GroupMessage.NOT_FOUND); - } - return group; - } - - async update(id: string, dto: UpdateGroupDto): Promise { - const group = await this.groupRepository.findOne({ id }); - if (!group) { - throw new NotFoundException(GroupMessage.NOT_FOUND); - } - this.em.assign(group, dto); - await this.em.persistAndFlush(group); - return group; - } - - async remove(id: string): Promise { - const group = await this.groupRepository.findOne({ id }, { populate: ['icons'] }); - if (!group) { - throw new NotFoundException(GroupMessage.NOT_FOUND); - } - await this.em.removeAndFlush(group); - } - - findAllGroups(): Promise { - return this.groupRepository.find({}, { populate: ['icons'] }); - } -} diff --git a/src/modules/icons/providers/icon.service.ts b/src/modules/icons/providers/icon.service.ts deleted file mode 100644 index 66ccfda..0000000 --- a/src/modules/icons/providers/icon.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateIconDto } from '../dto/create-icon.dto'; -import { UpdateIconDto } from '../dto/update-icon.dto'; -import { IconRepository } from '../repositories/icon.repository'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { RequiredEntityData } from '@mikro-orm/core'; -import { Icon } from '../entities/icon.entity'; -import { IconMessage, GroupMessage } from 'src/common/enums/message.enum'; -import { Group } from '../entities/group.entity'; - -@Injectable() -export class IconService { - constructor( - private readonly iconRepository: IconRepository, - private readonly em: EntityManager, - ) {} - - async create(dto: CreateIconDto): Promise { - const group = await this.em.findOne(Group, { id: dto.groupId }); - if (!group) { - throw new NotFoundException(GroupMessage.NOT_FOUND); - } - - const data: RequiredEntityData = { - url: dto.url, - group: group, - }; - - const icon = this.iconRepository.create(data); - await this.em.persistAndFlush(icon); - return icon; - } - - async findAll(): Promise { - return this.iconRepository.find({}, { populate: ['group'] }); - } - - async findOne(id: string): Promise { - const icon = await this.iconRepository.findOne({ id }, { populate: ['group'] }); - if (!icon) { - throw new NotFoundException(IconMessage.NOT_FOUND); - } - return icon; - } - - async update(id: string, dto: UpdateIconDto): Promise { - const icon = await this.iconRepository.findOne({ id }); - if (!icon) { - throw new NotFoundException(IconMessage.NOT_FOUND); - } - - if (dto.groupId) { - const group = await this.em.findOne(Group, { id: dto.groupId }); - if (!group) { - throw new NotFoundException(GroupMessage.NOT_FOUND); - } - icon.group = group; - } - - this.em.assign(icon, { - url: dto.url, - }); - - await this.em.persistAndFlush(icon); - return icon; - } - - async remove(id: string): Promise { - const icon = await this.iconRepository.findOne({ id }); - if (!icon) { - throw new NotFoundException(IconMessage.NOT_FOUND); - } - await this.em.removeAndFlush(icon); - } -} - diff --git a/src/modules/icons/repositories/group.repository.ts b/src/modules/icons/repositories/group.repository.ts deleted file mode 100644 index fdc6749..0000000 --- a/src/modules/icons/repositories/group.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { Group } from '../entities/group.entity'; - -@Injectable() -export class GroupRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, Group); - } -} diff --git a/src/modules/icons/repositories/icon.repository.ts b/src/modules/icons/repositories/icon.repository.ts deleted file mode 100644 index c748b94..0000000 --- a/src/modules/icons/repositories/icon.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { Icon } from '../entities/icon.entity'; - -@Injectable() -export class IconRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, Icon); - } -} diff --git a/src/modules/inventory/dto/bulk-reserve-food.dto.ts b/src/modules/inventory/dto/bulk-reserve-food.dto.ts deleted file mode 100644 index d8ce346..0000000 --- a/src/modules/inventory/dto/bulk-reserve-food.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsString, IsNumber, Min } from 'class-validator'; -import { Type } from 'class-transformer'; - -export class BulkReserveFoodItemDto { - @ApiProperty({ example: 'food-123', description: 'Food ID' }) - @IsNotEmpty() - @IsString() - foodId!: string; - - @ApiProperty({ example: 5, description: 'Quantity to reserve' }) - @IsNotEmpty() - @IsNumber() - @Min(1) - @Type(() => Number) - quantity!: number; -} - -export class BulkReserveFoodDto { - @ApiProperty({ - description: 'Array of food reservations to create', - type: [BulkReserveFoodItemDto], - example: [ - { foodId: 'food-123', quantity: 5 }, - { foodId: 'food-789', quantity: 3 }, - ], - }) - @IsNotEmpty() - @IsArray() - @ArrayMinSize(1, { message: 'At least one item is required' }) - @ValidateNested({ each: true }) - @Type(() => BulkReserveFoodItemDto) - items!: BulkReserveFoodItemDto[]; -} diff --git a/src/modules/inventory/dto/bulk-set-stock.dto.ts b/src/modules/inventory/dto/bulk-set-stock.dto.ts deleted file mode 100644 index a4daf35..0000000 --- a/src/modules/inventory/dto/bulk-set-stock.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize } from 'class-validator'; -import { Type } from 'class-transformer'; -import { SetStockDto } from './set-stock.dto'; - -export class BulkSetStockItemDto extends SetStockDto { - @ApiProperty({ example: 'food-123', description: 'Food ID' }) - @IsNotEmpty() - foodId!: string; -} - -export class BulkSetStockDto { - @ApiProperty({ - description: 'Array of stock items to set', - type: [BulkSetStockItemDto], - example: [ - { foodId: 'food-123', totalStock: 100, availableStock: 80 }, - { foodId: 'food-456', totalStock: 50, availableStock: 45 }, - ], - }) - @IsNotEmpty() - @IsArray() - @ArrayMinSize(1, { message: 'At least one item is required' }) - @ValidateNested({ each: true }) - @Type(() => BulkSetStockItemDto) - items!: BulkSetStockItemDto[]; -} diff --git a/src/modules/inventory/dto/create-inventory.dto.ts b/src/modules/inventory/dto/create-inventory.dto.ts deleted file mode 100644 index 2ebf1f1..0000000 --- a/src/modules/inventory/dto/create-inventory.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateInventoryDto {} diff --git a/src/modules/inventory/dto/set-stock.dto.ts b/src/modules/inventory/dto/set-stock.dto.ts deleted file mode 100644 index 6b06cd9..0000000 --- a/src/modules/inventory/dto/set-stock.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, Min } from 'class-validator'; -import { Type } from 'class-transformer'; - -export class SetStockDto { - @ApiProperty({ example: 100, description: 'Total stock quantity' }) - @IsNumber() - @Min(0) - @Type(() => Number) - totalStock!: number; - - @ApiProperty({ example: 80, description: 'Available stock quantity' }) - @IsNumber() - @Min(0) - @Type(() => Number) - availableStock!: number; -} diff --git a/src/modules/inventory/entities/inventory.entity.ts b/src/modules/inventory/entities/inventory.entity.ts deleted file mode 100644 index 8897990..0000000 --- a/src/modules/inventory/entities/inventory.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Cascade, Entity, Property, OneToOne } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Food } from '../../foods/entities/food.entity'; - -@Entity({ tableName: 'inventory' }) -export class Inventory extends BaseEntity { - @OneToOne(() => Food, { - cascade: [Cascade.ALL], - orphanRemoval: true, - }) - food!: Food; - - @Property({ type: 'int' }) - totalStock!: number; - - @Property({ type: 'int' }) - availableStock!: number; -} diff --git a/src/modules/inventory/inteface/reservation.ts b/src/modules/inventory/inteface/reservation.ts deleted file mode 100644 index c3f8f5a..0000000 --- a/src/modules/inventory/inteface/reservation.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum ReservationStatus { - ACTIVE = 'active', - CONFIRMED = 'confirmed', -} - - diff --git a/src/modules/inventory/inventory.controller.ts b/src/modules/inventory/inventory.controller.ts deleted file mode 100644 index 9a71e3c..0000000 --- a/src/modules/inventory/inventory.controller.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Controller, Body, Patch, Param, Post, UseGuards } from '@nestjs/common'; -import { InventoryService } from './inventory.service'; -import { SetStockDto } from './dto/set-stock.dto'; -import { BulkSetStockDto } from './dto/bulk-set-stock.dto'; -import { ApiTags, ApiOperation, ApiBody, ApiParam, ApiBearerAuth } from '@nestjs/swagger'; -import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; -import { RestId } from 'src/common/decorators'; -import { Inventory } from './entities/inventory.entity'; -import { Permissions } from 'src/common/decorators/permissions.decorator'; -import { Permission } from 'src/common/enums/permission.enum'; - -@ApiTags('inventory') -@UseGuards(AdminAuthGuard) -@ApiBearerAuth() -@Permissions(Permission.MANAGE_FOODS) -@Controller() -export class InventoryController { - constructor(private readonly inventoryService: InventoryService) { } - - @Patch('admin/inventory/food/:foodId/stock') - @ApiOperation({ summary: 'Set available and total stock for a food item' }) - @ApiParam({ name: 'foodId', description: 'Food ID' }) - @ApiBody({ type: SetStockDto }) - setStockForFood( - @Param('foodId') foodId: string, - @RestId() restaurantId: string, - @Body() setStockDto: SetStockDto, - ): Promise { - return this.inventoryService.setStockForFood(foodId, restaurantId, setStockDto); - } - - @Post('admin/inventory/foods/stock/bulk') - @ApiOperation({ summary: 'Bulk set available and total stock for multiple food items' }) - @ApiBody({ type: BulkSetStockDto }) - bulkSetStockForFoods(@RestId() restaurantId: string, @Body() bulkSetStockDto: BulkSetStockDto): Promise { - return this.inventoryService.bulkSetStockForFoods(restaurantId, bulkSetStockDto); - } -} diff --git a/src/modules/inventory/inventory.module.ts b/src/modules/inventory/inventory.module.ts deleted file mode 100644 index d333b5a..0000000 --- a/src/modules/inventory/inventory.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { InventoryService } from './inventory.service'; -import { InventoryController } from './inventory.controller'; -import { Inventory } from './entities/inventory.entity'; -import { AuthModule } from '../auth/auth.module'; -import { JwtModule } from '@nestjs/jwt'; - -@Module({ - imports: [MikroOrmModule.forFeature([Inventory]), AuthModule, JwtModule], - controllers: [InventoryController], - providers: [InventoryService], - exports: [InventoryService], -}) -export class InventoryModule {} diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts deleted file mode 100644 index 8fcfa1f..0000000 --- a/src/modules/inventory/inventory.service.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { SetStockDto } from './dto/set-stock.dto'; -import { BulkSetStockDto } from './dto/bulk-set-stock.dto'; -import { BulkReserveFoodDto } from './dto/bulk-reserve-food.dto'; -import { Inventory } from './entities/inventory.entity'; -import { Food } from '../foods/entities/food.entity'; -import { Restaurant } from '../restaurants/entities/restaurant.entity'; - -@Injectable() -export class InventoryService { - constructor(private readonly em: EntityManager) {} - - async setStockForFood(foodId: string, restaurantId: string, setStockDto: SetStockDto): Promise { - // Validate that availableStock doesn't exceed totalStock - if (setStockDto.availableStock > setStockDto.totalStock) { - throw new BadRequestException('Available stock cannot exceed total stock'); - } - - // Find food and verify it belongs to the restaurant - const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant'] }); - if (!food) { - throw new NotFoundException(`Food with ID ${foodId} not found`); - } - - if (food.restaurant.id !== restaurantId) { - throw new BadRequestException(`Food does not belong to restaurant ${restaurantId}`); - } - - // Find or create inventory record - let inventory = await this.em.findOne(Inventory, { - food: { id: foodId, restaurant: { id: restaurantId } }, - }); - - if (!inventory) { - // Create new inventory record - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) { - throw new NotFoundException(`Restaurant with ID ${restaurantId} not found`); - } - - inventory = this.em.create(Inventory, { - food, - totalStock: setStockDto.totalStock, - availableStock: setStockDto.availableStock, - }); - } else { - // Update existing inventory record - inventory.totalStock = setStockDto.totalStock; - inventory.availableStock = setStockDto.availableStock; - } - - await this.em.flush(); - - return inventory; - } - - async bulkSetStockForFoods(restaurantId: string, bulkSetStockDto: BulkSetStockDto): Promise { - const { items } = bulkSetStockDto; - // Validate all items first - for (const item of items) { - if (item.availableStock > item.totalStock) { - throw new BadRequestException(`Available stock cannot exceed total stock for food ${item.foodId}`); - } - } - - // Get all food IDs - const foodIds = items.map(item => item.foodId); - - // Load all foods in one query - const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['restaurant'] }); - - // Verify all foods exist and belong to the restaurant - const foodMap = new Map(); - for (const food of foods) { - if (food.restaurant.id !== restaurantId) { - throw new BadRequestException(`Food ${food.id} does not belong to restaurant ${restaurantId}`); - } - foodMap.set(food.id, food); - } - - // Check for missing foods - const missingFoodIds = foodIds.filter(id => !foodMap.has(id)); - if (missingFoodIds.length > 0) { - throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`); - } - - // Load all existing inventories in one query - const existingInventories = await this.em.find(Inventory, { - food: { id: { $in: foodIds }, restaurant: { id: restaurantId } }, - }); - - const inventoryMap = new Map(); - for (const inventory of existingInventories) { - inventoryMap.set(inventory.food.id, inventory); - } - - // Get restaurant once - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) { - throw new NotFoundException(`Restaurant with ID ${restaurantId} not found`); - } - - // Process all items - const results: Inventory[] = []; - for (const item of items) { - const food = foodMap.get(item.foodId)!; - let inventory = inventoryMap.get(item.foodId); - - if (!inventory) { - // Create new inventory record - inventory = this.em.create(Inventory, { - food, - totalStock: item.totalStock, - availableStock: item.availableStock, - }); - } else { - // Update existing inventory record - inventory.totalStock = item.totalStock; - inventory.availableStock = item.availableStock; - } - - results.push(inventory); - } - - // Flush all changes at once - await this.em.flush(); - - return results; - } - - async deductFromInventory(em: EntityManager, bulkReserveFoodDto: BulkReserveFoodDto): Promise { - return em.transactional(async em => { - const { items } = bulkReserveFoodDto; - // Get all unique food IDs - const foodIds = [...new Set(items.map(item => item.foodId))]; - - // Load all foods in one query - const foods = await em.find(Food, { id: { $in: foodIds } }); - - // Verify all foods exist and belong to the restaurant - const foodMap = new Map(); - for (const food of foods) { - foodMap.set(food.id, food); - } - - // Check for missing foods - const missingFoodIds = foodIds.filter(id => !foodMap.has(id)); - if (missingFoodIds.length > 0) { - throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`); - } - - // Load all existing inventories in one query - const existingInventories = await em.find(Inventory, { - food: { id: { $in: foodIds } }, - }); - - const inventoryMap = new Map(); - for (const inventory of existingInventories) { - inventoryMap.set(inventory.food.id, inventory); - } - - // Validate stock availability and create reservations - const inventories: Inventory[] = []; - for (const item of items) { - const inventory = inventoryMap.get(item.foodId); - - // Check if inventory exists - if (!inventory) { - throw new NotFoundException(`Inventory not found for food ${item.foodId}`); - } - - // Check if available stock is sufficient - if (inventory.availableStock < item.quantity) { - throw new BadRequestException( - `Insufficient stock for food ${item.foodId}. Available: ${inventory.availableStock}, Requested: ${item.quantity}`, - ); - } - inventory.availableStock -= item.quantity; - inventories.push(inventory); - em.persist(inventory); - } - return inventories; - }); - } - - async restoreToInventory(em: EntityManager, bulkRestoreDto: BulkReserveFoodDto): Promise { - return em.transactional(async em => { - const { items } = bulkRestoreDto; - - const foodIds = [...new Set(items.map(item => item.foodId))]; - - const foods = await em.find(Food, { id: { $in: foodIds } }); - const foodMap = new Map(); - for (const food of foods) { - foodMap.set(food.id, food); - } - - const missingFoodIds = foodIds.filter(id => !foodMap.has(id)); - if (missingFoodIds.length > 0) { - throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`); - } - - const existingInventories = await em.find(Inventory, { - food: { id: { $in: foodIds } }, - }); - - const inventoryMap = new Map(); - for (const inventory of existingInventories) { - inventoryMap.set(inventory.food.id, inventory); - } - - const inventories: Inventory[] = []; - for (const item of items) { - const inventory = inventoryMap.get(item.foodId); - if (!inventory) { - throw new NotFoundException(`Inventory not found for food ${item.foodId}`); - } - inventory.availableStock += item.quantity; - inventories.push(inventory); - em.persist(inventory); - } - - return inventories; - }); - } -} diff --git a/src/modules/orders/controllers/orders.controller.ts b/src/modules/orders/controllers/orders.controller.ts deleted file mode 100644 index 93515f5..0000000 --- a/src/modules/orders/controllers/orders.controller.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Controller, Get, Post, Param, UseGuards, Patch, Query, Body } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiHeader, ApiBody } from '@nestjs/swagger'; -import { OrdersService } from '../providers/orders.service'; -import { AuthGuard } from '../../auth/guards/auth.guard'; -import { UserId } from '../../../common/decorators/user-id.decorator'; -import { RestId } from 'src/common/decorators/rest-id.decorator'; -import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard'; -import { FindOrdersDto } from '../dto/find-orders.dto'; -import { OrderStatus } from '../interface/order.interface'; -import { API_HEADER_SLUG } from 'src/common/constants/index'; -import { UpdateOrderStatusDto } from '../dto/update-order-status.dto'; -import { Permissions } from 'src/common/decorators/permissions.decorator'; -import { Permission } from 'src/common/enums/permission.enum'; - -@ApiTags('orders') -@ApiBearerAuth() -@Controller() -export class OrdersController { - constructor(private readonly ordersService: OrdersService) { } - - @UseGuards(AuthGuard) - @Post('public/checkout') - @ApiHeader(API_HEADER_SLUG) - @ApiOperation({ summary: 'Checkout : create order and payment record' }) - checkout(@UserId() userId: string, @RestId() restaurantId: string) { - return this.ordersService.checkout(userId, restaurantId); - } - - @UseGuards(AuthGuard) - @Get('public/orders') - @ApiHeader(API_HEADER_SLUG) - @ApiOperation({ summary: 'Get all orders with pagination and filters' }) - findAll(@RestId() restId: string, @Query() dto: FindOrdersDto, @UserId() userId: string) { - return this.ordersService.findAllForUser(restId, dto, userId); - } - - @UseGuards(AuthGuard) - @ApiOperation({ summary: 'Get an order By id for User' }) - @ApiParam({ name: 'orderId', description: 'Order ID' }) - @ApiHeader(API_HEADER_SLUG) - @Get('public/orders/:orderId') - findOne(@Param('orderId') orderId: string, @RestId() restId: string) { - return this.ordersService.findOne(orderId, restId); - } - - @UseGuards(AuthGuard) - @Patch('public/orders/:id/:status') - @ApiParam({ - name: 'status', - description: 'Order status', - enum: OrderStatus, - }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: UpdateOrderStatusDto }) - @ApiOperation({ summary: 'Update status of an order By User' }) - @ApiParam({ name: 'id', description: 'Order ID' }) - cancelOrder( - @Body() dto: UpdateOrderStatusDto, - @Param('id') orderId: string, - @Param('status') status: OrderStatus, - @RestId() restId: string, - ) { - return this.ordersService.changeOrderStatus(orderId, restId, status, 'user', dto?.desc); - } - - /******************** Admin Routes **********************/ - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) - @Get('admin/orders') - @ApiOperation({ summary: 'Get all orders with pagination and filters' }) - findAllAdmin(@RestId() restId: string, @Query() dto: FindOrdersDto) { - return this.ordersService.findAllForAdmin(restId, dto); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) - @ApiOperation({ summary: 'Get an order By id for User' }) - @ApiParam({ name: 'orderId', description: 'Order ID' }) - @Get('admin/orders/:orderId') - findOneAsAdmin(@Param('orderId') orderId: string, @RestId() restId: string) { - return this.ordersService.findOne(orderId, restId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) - @Patch('admin/orders/:orderId/:status') - @ApiOperation({ summary: 'Update an order status' }) - @ApiParam({ name: 'orderId', description: 'Order ID' }) - @ApiBody({ type: UpdateOrderStatusDto }) - @ApiParam({ - name: 'status', - description: 'Order status', - enum: OrderStatus, - }) - updateStatus( - @Param('orderId') orderId: string, - @Body() dto: UpdateOrderStatusDto, - @Param('status') status: OrderStatus, - @RestId() restId: string, - ) { - return this.ordersService.changeOrderStatus(orderId, restId, status, 'admin', dto?.desc); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.VIEW_REPORTS) - @ApiOperation({ summary: 'Get Stats for report page' }) - @Get('admin/orders/stats') - findStats(@RestId() restId: string) { - return this.ordersService.getStats(restId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.VIEW_REPORTS) - @ApiOperation({ summary: 'Get food sales pie chart data for last month' }) - @ApiHeader(API_HEADER_SLUG) - @Get('admin/orders/food-sales-pie-chart') - getFoodSalesPieChart(@RestId() restId: string) { - return this.ordersService.getFoodSalesPieChart(restId); - } -} diff --git a/src/modules/orders/crone/order.crone.ts b/src/modules/orders/crone/order.crone.ts deleted file mode 100644 index b183cc9..0000000 --- a/src/modules/orders/crone/order.crone.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Payment } from '../../payments/entities/payment.entity'; -import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment'; -import { InventoryService } from '../../inventory/inventory.service'; -import { OrderStatus } from '../interface/order.interface'; -import { Order } from '../entities/order.entity'; -import { OrderStatusChangedEvent } from '../events/order.events'; - -@Injectable() -export class OrdersCrone { - private readonly logger = new Logger(OrdersCrone.name); - - constructor( - private readonly em: EntityManager, - private readonly inventoryService: InventoryService, - private readonly eventEmitter: EventEmitter2, - ) { } - - // run every minute and fail pending online payments older than 15 minutes - @Cron('*/1 * * * *', { - name: 'failOldOnlinePayments', - timeZone: 'UTC', - }) - async cancelOldOnlinePendingOrders() { - try { - const cutoff = new Date(Date.now() - 15 * 60 * 1000); - - this.logger.debug('Searching for pending online payments older than 15 minutes'); - - const payments = await this.em.find( - Payment, - { - method: PaymentMethodEnum.Online, - status: PaymentStatusEnum.Pending, - createdAt: { $lte: cutoff }, - }, - { populate: ['order', 'order.items', 'order.items.food'] }, - ); - - if (!payments || payments.length === 0) { - return; - } - - this.logger.log(`Found ${payments.length} stale pending online payments`); - - for (const p of payments) { - try { - await this.em.transactional(async em => { - // reload inside transaction to avoid concurrency issues - const payment = await em.findOne( - Payment, - { id: p.id }, - { populate: ['order', 'order.items', 'order.items.food'] }, - ); - if (!payment) return; - if (payment.status !== PaymentStatusEnum.Pending) return; - - payment.status = PaymentStatusEnum.Failed; - payment.failedAt = new Date(); - - if (payment.order) { - payment.order.status = OrderStatus.CANCELED; - - // prepare restore payload - const items = (payment.order as any).items || []; - const restorePayload = { - items: items.map((it: any) => ({ foodId: it.food.id, quantity: it.quantity })), - }; - - if (restorePayload.items.length > 0) { - await this.inventoryService.restoreToInventory(em, restorePayload); - } - } - - em.persist(payment); - if (payment.order) em.persist(payment.order); - await em.flush(); - this.logger.log( - `Marked payment ${payment.id} and order ${payment.order?.id} as failed and restored inventory`, - ); - }); - } catch (err) { - this.logger.error(`Error processing payment ${p.id}: ${err.message}`, err.stack); - } - } - } catch (err) { - this.logger.error(`OrdersCrone failed: ${err.message}`, err.stack); - } - } - - // run every 15 minutes to complete orders that have been in shipped/delivered statuses for more than 3 hours - @Cron('*/15 * * * *', { - name: 'completeOldDeliveredOrders', - timeZone: 'UTC', - }) - async completeOldDeliveredOrders() { - try { - const cutoff = new Date(Date.now() - 3 * 60 * 60 * 1000); // 3 hours ago - - this.logger.debug('Searching for orders in shipped/delivered statuses older than 3 hours'); - - const orders = await this.em.find( - Order, - { - status: { - $in: [ - OrderStatus.SHIPPED, - OrderStatus.DELIVERED_TO_WAITER, - OrderStatus.DELIVERED_TO_RECEPTIONIST, - ], - }, - updatedAt: { $lte: cutoff }, - }, - { populate: ['restaurant'] }, - ); - - if (!orders || orders.length === 0) { - return; - } - - this.logger.log(`Found ${orders.length} orders to mark as completed`); - - for (const order of orders) { - try { - await this.em.transactional(async em => { - // reload inside transaction to avoid concurrency issues - const reloadedOrder = await em.findOne( - Order, - { id: order.id }, - { populate: ['restaurant', 'user'] }, - ); - if (!reloadedOrder) return; - if ( - ![ - OrderStatus.SHIPPED, - OrderStatus.DELIVERED_TO_WAITER, - OrderStatus.DELIVERED_TO_RECEPTIONIST, - ].includes(reloadedOrder.status) - ) { - return; - } - - const previousStatus = reloadedOrder.status; - const restaurantId = - typeof reloadedOrder.restaurant === 'string' - ? reloadedOrder.restaurant - : reloadedOrder.restaurant.id; - - // Update order status and history - reloadedOrder.status = OrderStatus.COMPLETED; - reloadedOrder.history.push({ - status: OrderStatus.COMPLETED, - changedAt: new Date(), - desc: 'تکمیل سفارش توسط سیستم', - }); - - em.persist(reloadedOrder); - await em.flush(); - - // // Emit event after transaction completes - this.eventEmitter.emit( - OrderStatusChangedEvent.name, - new OrderStatusChangedEvent( - reloadedOrder.id, - reloadedOrder.user?.id || '', - String(reloadedOrder.orderNumber) || '', - restaurantId, - previousStatus, - OrderStatus.COMPLETED, - 'admin', - ), - ); - - this.logger.log(`Marked order ${reloadedOrder.id} as completed`); - }); - } catch (err) { - this.logger.error(`Error processing order ${order.id}: ${err.message}`, err.stack); - } - } - } catch (err) { - this.logger.error(`completeOldDeliveredOrders cron failed: ${err.message}`, err.stack); - } - } -} diff --git a/src/modules/orders/dto/find-orders.dto.ts b/src/modules/orders/dto/find-orders.dto.ts deleted file mode 100644 index 1ab7404..0000000 --- a/src/modules/orders/dto/find-orders.dto.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - IsOptional, - IsString, - IsNumber, - Min, - IsIn, - IsEnum, - IsDateString, - IsArray, -} from 'class-validator'; -import { Type, Transform } from 'class-transformer'; -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { OrderStatus } from '../interface/order.interface'; -import { PaymentStatusEnum } from '../../payments/interface/payment'; - -const sortOrderOptions = ['asc', 'desc'] as const; -type SortOrder = (typeof sortOrderOptions)[number]; - -export class FindOrdersDto { - @ApiPropertyOptional({ default: 1, minimum: 1 }) - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(1) - page: number = 1; - - @ApiPropertyOptional({ default: 10, minimum: 1 }) - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(1) - limit: number = 10; - - /** - * ?statuses=paid,confirmed - * ?statuses=paid&statuses=confirmed - */ - @ApiPropertyOptional({ - description: 'Filter by order statuses', - enum: OrderStatus, - isArray: true, - }) - @IsOptional() - @Transform(({ value }) => - Array.isArray(value) ? value : value?.split(',') - ) - @IsArray() - @IsEnum(OrderStatus, { each: true }) - statuses?: OrderStatus[]; - - @ApiPropertyOptional({ enum: PaymentStatusEnum }) - @IsOptional() - @IsEnum(PaymentStatusEnum) - paymentStatus?: PaymentStatusEnum; - - @ApiPropertyOptional() - @IsOptional() - @IsString() - search?: string; - - @ApiPropertyOptional({ format: 'date-time' }) - @IsOptional() - @IsDateString() - startDate?: string; - - @ApiPropertyOptional({ format: 'date-time' }) - @IsOptional() - @IsDateString() - endDate?: string; - - @ApiPropertyOptional({ default: 'createdAt' }) - @IsOptional() - @IsString() - orderBy: string = 'createdAt'; - - @ApiPropertyOptional({ - enum: sortOrderOptions, - default: 'desc', - }) - @IsOptional() - @IsIn(sortOrderOptions) - order: SortOrder = 'desc'; -} diff --git a/src/modules/orders/dto/update-order-status.dto.ts b/src/modules/orders/dto/update-order-status.dto.ts deleted file mode 100644 index e4026bf..0000000 --- a/src/modules/orders/dto/update-order-status.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; -export class UpdateOrderStatusDto { - @ApiPropertyOptional({ - description: 'Change Status description', - }) - @IsOptional() - @IsString() - desc?: string; -} diff --git a/src/modules/orders/dto/update-order.dto.ts b/src/modules/orders/dto/update-order.dto.ts deleted file mode 100644 index e64eebc..0000000 --- a/src/modules/orders/dto/update-order.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -// import { PartialType } from '@nestjs/swagger'; -// import { CreateOrderDto } from './create-order.dto'; - -// export class UpdateOrderDto extends PartialType(CreateOrderDto) {} -export class UpdateOrderDto {} diff --git a/src/modules/orders/entities/order-item.entity.ts b/src/modules/orders/entities/order-item.entity.ts deleted file mode 100644 index 8c996ab..0000000 --- a/src/modules/orders/entities/order-item.entity.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity, Index, ManyToOne, Property } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Order } from './order.entity'; -import { Food } from '../../foods/entities/food.entity'; - -@Entity({ tableName: 'order_items' }) -@Index({ properties: ['order'] }) -@Index({ properties: ['food'] }) -export class OrderItem extends BaseEntity { - @ManyToOne(() => Order) - order!: Order; - - @ManyToOne(() => Food) - food!: Food; - - @Property({ type: 'int' }) - quantity!: number; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - unitPrice!: number; - - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - discount: number = 0; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - totalPrice!: number; -} diff --git a/src/modules/orders/entities/order.entity.ts b/src/modules/orders/entities/order.entity.ts deleted file mode 100644 index 592627b..0000000 --- a/src/modules/orders/entities/order.entity.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - Entity, - Index, - ManyToOne, - OneToMany, - Property, - Collection, - Cascade, - Enum, - BeforeCreate, - Unique, - type EventArgs, -} from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { OrderCouponDetail, OrderStatus } from '../interface/order.interface'; -import { User } from '../../users/entities/user.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { PaymentMethod } from '../../payments/entities/payment-method.entity'; -import { OrderItem } from './order-item.entity'; -import { Delivery } from '../../delivery/entities/delivery.entity'; -import { OrderUserAddress, OrderCarAddress } from '../interface/order.interface'; -import { Payment } from 'src/modules/payments/entities/payment.entity'; - -@Entity({ tableName: 'orders' }) -@Unique({ properties: ['restaurant', 'orderNumber'] }) -@Index({ properties: ['restaurant', 'status'] }) -@Index({ properties: ['user', 'status'] }) -@Index({ properties: ['restaurant', 'orderNumber'] }) -@Index({ properties: ['status'] }) -export class Order extends BaseEntity { - @ManyToOne(() => User) - user!: User; - - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - - @ManyToOne(() => Delivery) - deliveryMethod!: Delivery; - - @OneToMany(() => Payment, payment => payment.order, { - cascade: [Cascade.ALL], - orphanRemoval: true, - }) - payments = new Collection(this); - - @OneToMany(() => OrderItem, item => item.order, { - cascade: [Cascade.ALL], - orphanRemoval: true, - }) - items = new Collection(this); - - @Property({ type: 'json', nullable: true }) - userAddress?: OrderUserAddress | null; - // for car delivery - @Property({ type: 'json', nullable: true }) - carAddress?: OrderCarAddress | null; - - @ManyToOne(() => PaymentMethod) - paymentMethod: PaymentMethod; - - @Property({ type: 'int', nullable: true }) - orderNumber?: number; - - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - couponDiscount: number = 0; - - @Property({ type: 'jsonb', nullable: true }) - couponDetail?: OrderCouponDetail | null; - - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - itemsDiscount: number = 0; - - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - totalDiscount: number = 0; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - subTotal!: number; - - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - tax: number = 0; - - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - deliveryFee: number = 0; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - total!: number; - - @Property({ type: 'int', default: 0 }) - totalItems: number = 0; - - @Property({ type: 'text', nullable: true }) - description?: string; - - @Property({ nullable: true }) - tableNumber?: string; - - @Enum(() => OrderStatus) - status!: OrderStatus; - - @Property({ type: 'json', nullable: true }) - history: Array<{ status: OrderStatus; changedAt: Date; desc: string | null }> = []; - - @BeforeCreate() - async generateOrderNumber(args: EventArgs) { - const em = args.em; - const order = args.entity; - - // Ensure restaurant is loaded - if (!order.restaurant) { - throw new Error('Restaurant must be set before creating order'); - } - - // Get the restaurant ID (handle both entity and ID cases) - const restaurantId = typeof order.restaurant === 'string' ? order.restaurant : order.restaurant.id; - - // Query for max orderNumber for this restaurant - // Using findOne with orderBy to get the highest order number - const maxOrder = await em.findOne( - Order, - { restaurant: restaurantId }, - { - orderBy: { orderNumber: 'DESC' }, - fields: ['orderNumber'], - }, - ); - - // Set the next order number (1 if no orders exist, otherwise max + 1) - order.orderNumber = maxOrder?.orderNumber ? maxOrder.orderNumber + 1 : 1; - } -} diff --git a/src/modules/orders/events/order.events.ts b/src/modules/orders/events/order.events.ts deleted file mode 100644 index 5348149..0000000 --- a/src/modules/orders/events/order.events.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { OrderStatus, StatusTransitionRef } from '../interface/order.interface'; - -export class OrderCreatedEvent { - constructor( - public readonly orderId: string, - public readonly restaurantId: string, - public readonly orderNumber: string, - public readonly total: number, - ) {} -} - -export class OrderStatusChangedEvent { - constructor( - public readonly orderId: string, - public readonly userId: string, - public readonly orderNumber: string, - public readonly restaurantId: string, - public readonly previousStatus: OrderStatus, - public readonly newStatus: OrderStatus, - public readonly changedBy: StatusTransitionRef, - ) {} -} diff --git a/src/modules/orders/interface/order.interface.ts b/src/modules/orders/interface/order.interface.ts deleted file mode 100644 index d9d1a6c..0000000 --- a/src/modules/orders/interface/order.interface.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { CouponType } from 'src/modules/coupons/interface/coupon'; - -export interface OrderUserAddress { - address?: string; - latitude?: number; - longitude?: number; - city: string; - province: string; - postalCode?: string; - fullName: string; - phone: string; -} - -export interface OrderCarAddress { - carModel: string; - carColor: string; - plateNumber: string; - phone: string; -} - -export enum OrderStatus { - PENDING_PAYMENT = 'pendingPayment', - PAID = 'paid', - PREPARING = 'preparing', - DELIVERED_TO_WAITER = 'deliveredToWaiter', - DELIVERED_TO_RECEPTIONIST = 'deliveredToReceptionist', - SHIPPED = 'shipped', - COMPLETED = 'completed', - CANCELED = 'canceled', -} - -export interface OrderCouponDetail { - couponId: string; - couponName: string; - couponCode: string; - type: CouponType; - value: number; - - maxDiscount?: number; -} - -export type StatusTransitionRef = 'user' | 'admin'; diff --git a/src/modules/orders/listeners/order.listeners.ts b/src/modules/orders/listeners/order.listeners.ts deleted file mode 100644 index 8a8bfe0..0000000 --- a/src/modules/orders/listeners/order.listeners.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.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'; -import { OrderRepository } from '../repositories/order.repository'; -import { OrderStatus } from '../interface/order.interface'; -import { PaymentMethodEnum } from 'src/modules/payments/interface/payment'; -import { UserService } from 'src/modules/users/providers/user.service'; -import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet'; - -@Injectable() -export class OrderListeners { - private readonly logger = new Logger(OrderListeners.name); - private orderCreatedSmsTemplateId: string; - private orderStatusChangedSmsTemplateId: string; - // private orderCompletedSmsTemplateId: string; - constructor( - private readonly adminService: AdminRepository, - private readonly OrderRepository: OrderRepository, - private readonly notificationService: NotificationService, - private readonly configService: ConfigService, - private readonly userService: UserService, - ) { - this.orderCreatedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_CREATED') ?? '123'; - this.orderStatusChangedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123'; - // this.orderCompletedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_STATUS_COMPLETED') ?? '123'; - } - - private getStatusFarsi(status: OrderStatus): string { - const statusMap: Record = { - [OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت', - [OrderStatus.PAID]: 'پرداخت شده', - [OrderStatus.PREPARING]: 'در حال آماده‌سازی', - [OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش', - [OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون', - [OrderStatus.SHIPPED]: 'ارسال شده', - [OrderStatus.COMPLETED]: 'تکمیل شده', - [OrderStatus.CANCELED]: 'لغو شده', - }; - return statusMap[status] || status; - } - - @OnEvent(OrderCreatedEvent.name) - async handleOrderCreated(event: OrderCreatedEvent) { - try { - this.logger.log( - `Order created event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, - ); - - const order = await this.OrderRepository.findOne(event.orderId); - if (order?.paymentMethod.method === PaymentMethodEnum.Online) { - return; - } - - - // get admnin os restuaraant that have order permissuins - const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.NEW_ORDER_NOTIFICATION); - const recipients = admins.map(admin => ({ - adminId: admin.id, - })); - - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.ORDER_CREATED, - content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, - sms: { - templateId: this.orderCreatedSmsTemplateId, - parameters: { - orderNumber: event.orderNumber, - total: event.total.toString(), - }, - }, - pushNotif: { - title: `سفارش جدید`, - content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, - icon: `/`, - action: { - type: NotifTitleEnum.ORDER_CREATED, - url: `/`, - }, - }, - }, - recipients, - metadata: { - priority: 1, - }, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for order created event: ${event.restaurantId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } - - @OnEvent(OrderStatusChangedEvent.name) - async handleOrderStatusChanged(event: OrderStatusChangedEvent) { - try { - this.logger.log( - `Order status changed event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, - ); - //TODO : REFACTOR to use queue or other way to handle this - const recipients = [ - { - userId: event.userId, - }, - ]; - if (event.newStatus === OrderStatus.COMPLETED) { - - if (!event?.userId) { - this.logger.log( - `User not found for order: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, - ); - } - - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.ORDER_STATUS_CHANGED, - content: `لطفابرای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`, - sms: { - templateId: this.orderCreatedSmsTemplateId, - parameters: { - orderNumber: event.orderNumber, - }, - }, - pushNotif: { - title: `تغییر وضعیت سفارش`, - content: `لطفا برای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`, - icon: `/`, - action: { - type: NotifTitleEnum.ORDER_STATUS_CHANGED, - url: ``, - }, - }, - }, - recipients, - metadata: { - priority: 1, - }, - }); - } else { - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.ORDER_STATUS_CHANGED, - content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`, - sms: { - templateId: this.orderStatusChangedSmsTemplateId, - parameters: { - orderNumber: event.orderNumber, - status: this.getStatusFarsi(event.newStatus), - }, - }, - pushNotif: { - title: `تغییر وضعیت سفارش`, - content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`, - icon: `/`, - action: { - type: NotifTitleEnum.ORDER_STATUS_CHANGED, - url: ``, - }, - }, - }, - recipients, - metadata: { - priority: 1, - }, - }); - } - } catch (error) { - this.logger.error( - `Failed to send notification for order status changed event: ${event.restaurantId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } - -} diff --git a/src/modules/orders/orders.module.ts b/src/modules/orders/orders.module.ts deleted file mode 100644 index b763908..0000000 --- a/src/modules/orders/orders.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { OrdersService } from './providers/orders.service'; -import { OrdersController } from './controllers/orders.controller'; -import { Order } from './entities/order.entity'; -import { OrderItem } from './entities/order-item.entity'; -import { User } from '../users/entities/user.entity'; -import { Restaurant } from '../restaurants/entities/restaurant.entity'; -import { Food } from '../foods/entities/food.entity'; -import { UserAddress } from '../users/entities/user-address.entity'; -import { PaymentMethod } from '../payments/entities/payment-method.entity'; -import { CartModule } from '../cart/cart.module'; -import { UtilsModule } from '../utils/utils.module'; -import { AuthModule } from '../auth/auth.module'; -import { PaymentsModule } from '../payments/payments.module'; -import { JwtModule } from '@nestjs/jwt'; -import { OrderRepository } from './repositories/order.repository'; -import { OrderListeners } from './listeners/order.listeners'; -import { AdminModule } from '../admin/admin.module'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { InventoryModule } from '../inventory/inventory.module'; -import { UserModule } from '../users/user.module'; - -@Module({ - imports: [ - MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]), - CartModule, - UtilsModule, - AuthModule, - forwardRef(() => PaymentsModule), - JwtModule, - AdminModule, - NotificationsModule, - InventoryModule, - forwardRef(() => UserModule) - ], - controllers: [OrdersController], - providers: [OrdersService, OrderRepository, OrderListeners], - exports: [OrderRepository, OrdersService], -}) -export class OrdersModule { } diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts deleted file mode 100644 index 62b4489..0000000 --- a/src/modules/orders/providers/orders.service.ts +++ /dev/null @@ -1,601 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Order } from '../entities/order.entity'; -import { OrderItem } from '../entities/order-item.entity'; -import { User } from '../../users/entities/user.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { Food } from '../../foods/entities/food.entity'; -import { CartService } from '../../cart/providers/cart.service'; -import { OrderStatus, OrderUserAddress, OrderCarAddress } from '../interface/order.interface'; -import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment'; -import { Cart } from '../../cart/interfaces/cart.interface'; -import { PaymentMethod } from '../../payments/entities/payment-method.entity'; -import { PaymentsService } from '../../payments/services/payments.service'; -import { DeliveryMethodEnum } from '../../delivery/interface/delivery'; -import { Delivery } from '../../delivery/entities/delivery.entity'; -import { OrderRepository } from '../repositories/order.repository'; -import { FindOrdersDto } from '../dto/find-orders.dto'; -import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; -import { Payment } from 'src/modules/payments/entities/payment.entity'; -import { InventoryService } from 'src/modules/inventory/inventory.service'; -import { BulkReserveFoodDto } from 'src/modules/inventory/dto/bulk-reserve-food.dto'; -import { StatusTransitionRef } from '../interface/order.interface'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events'; -import { OrderMessage } from 'src/common/enums/message.enum'; -type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number }; - -type ValidatedCartForOrder = { - user: User; - restaurant: Restaurant; - delivery: Delivery; - userAddress: OrderUserAddress | null; - carAddress: OrderCarAddress | null; - paymentMethod: PaymentMethod; - orderItemsData: OrderItemData[]; -}; - -@Injectable() -export class OrdersService { - private readonly logger = new Logger(OrdersService.name); - - private static readonly STATUS_TRANSITIONS: Record = { - [OrderStatus.PENDING_PAYMENT]: [OrderStatus.PAID, OrderStatus.CANCELED, OrderStatus.PREPARING], - [OrderStatus.PAID]: [OrderStatus.PREPARING, OrderStatus.CANCELED], - [OrderStatus.PREPARING]: [OrderStatus.DELIVERED_TO_RECEPTIONIST, OrderStatus.DELIVERED_TO_WAITER, OrderStatus.SHIPPED, OrderStatus.CANCELED], - [OrderStatus.DELIVERED_TO_WAITER]: [OrderStatus.COMPLETED, OrderStatus.CANCELED], - [OrderStatus.DELIVERED_TO_RECEPTIONIST]: [OrderStatus.COMPLETED, OrderStatus.CANCELED], - [OrderStatus.SHIPPED]: [OrderStatus.COMPLETED, OrderStatus.CANCELED], - [OrderStatus.COMPLETED]: [OrderStatus.CANCELED], - [OrderStatus.CANCELED]: [], - }; - - constructor( - private readonly em: EntityManager, - private readonly cartService: CartService, - private readonly orderRepository: OrderRepository, - private readonly paymentsService: PaymentsService, - private readonly inventoryService: InventoryService, - private readonly eventEmitter: EventEmitter2, - ) { } - - async checkout(userId: string, restaurantId: string) { - const cart = await this.cartService.findOneOrFail(userId, restaurantId); - const validated = await this.validateCartForOrder(userId, restaurantId, cart); - - const order = await this.em.transactional(async em => { - const order = em.create(Order, { - user: validated.user, - restaurant: validated.restaurant, - deliveryMethod: validated.delivery, - userAddress: validated.userAddress, - carAddress: validated.carAddress, - paymentMethod: validated.paymentMethod, - couponDiscount: cart.couponDiscount || 0, - couponDetail: cart.coupon, - itemsDiscount: cart.itemsDiscount || 0, - totalDiscount: cart.totalDiscount || 0, - subTotal: cart.subTotal || 0, - tax: cart.tax || 0, - deliveryFee: cart.deliveryFee || 0, - total: cart.total || 0, - totalItems: cart.totalItems || 0, - description: cart.description, - tableNumber: cart.tableNumber, - status: OrderStatus.PENDING_PAYMENT, - history: [{ status: OrderStatus.PENDING_PAYMENT, changedAt: new Date() }], - }); - - em.persist(order); - - for (const itemData of validated.orderItemsData) { - const { food, quantity, unitPrice, discount } = itemData; - - this.assertFoodHasSufficientStock(food, quantity); - - const totalPrice = (unitPrice - discount) * quantity; - - const orderItem = em.create(OrderItem, { - order, - food, - quantity, - unitPrice, - discount, - totalPrice, - }); - - em.persist(orderItem); - } - - const payment = em.create(Payment, { - order, - amount: order.total, - status: PaymentStatusEnum.Pending, - method: order.paymentMethod.method, - gateway: order.paymentMethod.gateway ?? null, - }); - - em.persist(payment); - // reserve stock based on payment method. - const bulkReserveFoodDto: BulkReserveFoodDto = { - items: validated.orderItemsData.map(item => ({ - foodId: item.food.id, - quantity: item.quantity, - })), - }; - await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto); - await em.flush(); - this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`); - return order; - }); - - await this.cartService.clearCart(userId, restaurantId); - - const { paymentUrl } = await this.paymentsService.payOrder(order.id); - this.eventEmitter.emit( - OrderCreatedEvent.name, - new OrderCreatedEvent(order.id, restaurantId, String(order?.orderNumber) || '', order.total), - ); - - return { paymentUrl, order }; - } - - /** - * Validates cart and prepares all required data for order creation - */ - private async validateCartForOrder(userId: string, restaurantId: string, cart: Cart): Promise { - this.assertCartHasItems(cart); - this.assertCartHasDeliveryMethod(cart); - this.assertCartHasPaymentMethod(cart); - - const [user, restaurant, delivery] = await Promise.all([ - this.getUserOrFail(userId), - this.getRestaurantOrFail(restaurantId), - this.getDeliveryOrFail(cart.deliveryMethodId!), - ]); - - this.assertMeetsMinOrderForDelivery(cart, delivery); - this.assertDeliveryMethodRequirements(cart, delivery); - - const paymentMethod = await this.getPaymentMethodOrFail(cart.paymentMethodId!, restaurantId); - this.assertPaymentMethodEnabled(paymentMethod); - - const orderItemsData = await this.buildOrderItemsData(cart, restaurantId); - - return { - user, - restaurant, - delivery, - paymentMethod, - userAddress: delivery.method === DeliveryMethodEnum.DeliveryCourier ? (cart?.userAddress ?? null) : null, - carAddress: delivery.method === DeliveryMethodEnum.DeliveryCar ? (cart?.carAddress ?? null) : null, - orderItemsData, - }; - } - - async findAllForUser(restId: string, dto: FindOrdersDto, userId: string): Promise> { - const result = await this.orderRepository.findAllPaginated(restId, { - page: dto.page, - limit: dto.limit, - statuses: dto.statuses, - paymentStatus: dto.paymentStatus, - search: dto.search, - startDate: dto.startDate, - endDate: dto.endDate, - orderBy: dto.orderBy, - order: dto.order, - userId, - }); - - return result; - } - - async findAllForAdmin(restId: string, dto: FindOrdersDto): Promise> { - const result = await this.orderRepository.findAllPaginated(restId, { - page: dto.page, - limit: dto.limit, - statuses: dto.statuses, - paymentStatus: dto.paymentStatus, - search: dto.search, - startDate: dto.startDate, - endDate: dto.endDate, - orderBy: dto.orderBy, - order: dto.order, - excludeOnlinePendingPayment: true, - }); - - return result; - } - - async findOne(id: string, restId: string): Promise { - const order = await this.orderRepository.findOne( - { id, restaurant: { id: restId } }, - { - populate: [ - 'user', - 'restaurant', - 'deliveryMethod', - 'userAddress', - 'carAddress', - 'paymentMethod', - 'payments', - 'items', - 'items.food', - ], - }, - ); - if (!order) { - throw new NotFoundException(OrderMessage.NOT_FOUND); - } - return order; - } - - - - async changeOrderStatus( - orderId: string, - restId: string, - toStatus: OrderStatus, - ref: StatusTransitionRef, - desc?: string, - ): Promise { - const order = await this.getOrderOrFail(orderId, restId); - - // Store previous status before changing it - const previousStatus = order.status; - - this.assertStatusTransitionAllowed(order, toStatus, ref); - - order.status = toStatus; - order.history.push({ status: toStatus, changedAt: new Date(), desc: desc || null }); - await this.em.persistAndFlush(order); - - this.eventEmitter.emit( - OrderStatusChangedEvent.name, - new OrderStatusChangedEvent( - orderId, - order.user?.id || '', - String(order?.orderNumber) || '', - restId, - previousStatus, - toStatus, - ref, - ), - ); - return order; - } - /* Helpers */ - private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) { - const paymentMethod = order.paymentMethod?.method; - if (!paymentMethod) { - throw new BadRequestException(OrderMessage.PAYMENT_METHOD_MISSING); - } - - if (!this.canTransition(order.status, to, paymentMethod, ref, order.deliveryMethod.method)) { - throw new BadRequestException(OrderMessage.INVALID_STATUS_TRANSITION); - } - } - - private canTransition(from: OrderStatus, to: OrderStatus, paymentMethod: PaymentMethodEnum, ref: 'user' | 'admin', deliveryMethod: DeliveryMethodEnum) { - if (!OrdersService.STATUS_TRANSITIONS[from]?.includes(to)) return false; - - if (to === OrderStatus.CANCELED) { - // only allow orders with status of PENDING_PAYMENT and PAID are allowed to be canceled by user - if (ref === 'user' && ![OrderStatus.PENDING_PAYMENT, OrderStatus.PAID].includes(from)) { - return false; - } else if (ref === 'admin') { - return true; - } - } - // only allow orders with status of PENDING_PAYMENT and payment - // method of cash are allowed to move to CONFIRMED directly - if ( - from == OrderStatus.PENDING_PAYMENT && - to == OrderStatus.PREPARING && - paymentMethod !== PaymentMethodEnum.Cash - ) { - return false; - } - - /** - * Only allow orders with status of PREPARING to be shipped if the delivery method is DeliveryCourier - */ - if ( - from == OrderStatus.PREPARING && - to == OrderStatus.SHIPPED && - deliveryMethod !== DeliveryMethodEnum.DeliveryCourier - ) { - return false; - } - if ( - from == OrderStatus.PREPARING && - to == OrderStatus.DELIVERED_TO_WAITER && - deliveryMethod !== DeliveryMethodEnum.DineIn && - deliveryMethod !== DeliveryMethodEnum.DeliveryCar - ) { - return false; - } - if ( - from == OrderStatus.PREPARING && - to == OrderStatus.DELIVERED_TO_RECEPTIONIST && - deliveryMethod !== DeliveryMethodEnum.CustomerPickup - ) { - return false; - } - - if (paymentMethod === PaymentMethodEnum.Online) { - if (to === OrderStatus.PREPARING && from !== OrderStatus.PAID) return false; - } - - return true; - } - - private async getOrderOrFail(orderId: string, restId: string): Promise { - const order = await this.em.findOne( - Order, - { id: orderId, restaurant: { id: restId } }, - { populate: ['paymentMethod', 'deliveryMethod', 'user'] }, - ); - if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND); - return order; - } - - private assertCartHasItems(cart: Cart) { - if (!cart.items || cart.items.length === 0) { - throw new BadRequestException(OrderMessage.CART_EMPTY); - } - } - - private assertCartHasDeliveryMethod(cart: Cart) { - if (!cart.deliveryMethodId) { - throw new NotFoundException(OrderMessage.DELIVERY_METHOD_NOT_FOUND); - } - } - - private assertCartHasPaymentMethod(cart: Cart) { - if (!cart.paymentMethodId) { - throw new BadRequestException(OrderMessage.PAYMENT_METHOD_REQUIRED); - } - } - - private async getUserOrFail(userId: string): Promise { - const user = await this.em.findOne(User, { id: userId }); - if (!user) throw new NotFoundException(OrderMessage.USER_NOT_FOUND); - return user; - } - - private async getRestaurantOrFail(restaurantId: string): Promise { - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) throw new NotFoundException(OrderMessage.RESTAURANT_NOT_FOUND); - return restaurant; - } - - private async getDeliveryOrFail(deliveryId: string): Promise { - const delivery = await this.em.findOne(Delivery, { id: deliveryId }); - if (!delivery) throw new NotFoundException(OrderMessage.DELIVERY_NOT_FOUND); - return delivery; - } - - private assertMeetsMinOrderForDelivery(cart: Cart, delivery: Delivery) { - const minOrderPrice = Number(delivery.minOrderPrice) || 0; - if (minOrderPrice > 0 && cart.total < minOrderPrice) { - throw new BadRequestException(OrderMessage.MIN_ORDER_AMOUNT_NOT_MET); - } - } - - private assertDeliveryMethodRequirements(cart: Cart, delivery: Delivery) { - if (delivery.method === DeliveryMethodEnum.DineIn) { - if (!cart.tableNumber || cart.tableNumber.trim() === '') { - throw new BadRequestException(OrderMessage.TABLE_NUMBER_REQUIRED); - } - } - - if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.userAddress) { - throw new BadRequestException(OrderMessage.ADDRESS_REQUIRED); - } - - if (delivery.method === DeliveryMethodEnum.DeliveryCar && !cart.carAddress) { - throw new BadRequestException(OrderMessage.CAR_ADDRESS_REQUIRED); - } - } - - private async getPaymentMethodOrFail(paymentMethodId: string, restaurantId: string): Promise { - const paymentMethod = await this.em.findOne( - PaymentMethod, - { - id: paymentMethodId, - restaurant: { id: restaurantId }, - }, - { populate: ['restaurant'] }, - ); - - if (!paymentMethod) { - throw new NotFoundException(`Payment method with ID ${paymentMethodId} not found for restaurant ${restaurantId}`); - } - - return paymentMethod; - } - - private assertPaymentMethodEnabled(paymentMethod: PaymentMethod) { - if (!paymentMethod.enabled) { - throw new BadRequestException(OrderMessage.PAYMENT_METHOD_NOT_ENABLED); - } - } - - private async buildOrderItemsData(cart: Cart, restaurantId: string): Promise { - const orderItemsData: OrderItemData[] = []; - - for (const cartItem of cart.items) { - const food = await this.em.findOne(Food, { id: cartItem.foodId }, { populate: ['restaurant', 'inventory'] }); - if (!food) throw new NotFoundException(OrderMessage.FOOD_NOT_FOUND); - - if (food.restaurant.id !== restaurantId) { - throw new BadRequestException(OrderMessage.FOOD_NOT_BELONGS_TO_RESTAURANT); - } - - this.assertFoodHasSufficientStock(food, cartItem.quantity); - - orderItemsData.push({ - food, - quantity: cartItem.quantity, - unitPrice: food.price || 0, - discount: food.discount || 0, - }); - } - - return orderItemsData; - } - - private assertFoodHasSufficientStock(food: Food, quantity: number) { - const availableStock = food.inventory?.availableStock ?? 0; - if (availableStock < quantity) { - throw new BadRequestException(OrderMessage.FOOD_NOT_FOUND); - } - } - - - async getStats(restId: string) { - return this.em.transactional(async em => { - // 1. Total orders count (excluding pending and canceled) - const totalOrders = await em.count(Order, { - restaurant: { id: restId }, - status: { - $in: [ - OrderStatus.PAID, - OrderStatus.PREPARING, - OrderStatus.DELIVERED_TO_WAITER, - OrderStatus.DELIVERED_TO_RECEPTIONIST, - OrderStatus.SHIPPED, - OrderStatus.COMPLETED, - ], - }, - }); - - // 2. Active foods count - const activeFoods = await em.count(Food, { - restaurant: { id: restId }, - isActive: true, - }); - - // 3. Total clients count (distinct users with orders for this restaurant) - const clientsResult = await em.execute( - ` - SELECT COUNT(DISTINCT o.user_id) as count - FROM orders o - WHERE o.restaurant_id = ? - `, - [restId], - ); - const totalClients = Number(clientsResult[0]?.count || 0); - - // 4. Total revenue (sum of paid payments for orders of this restaurant) - const revenueResult = await em.execute( - ` - SELECT COALESCE(SUM(p.amount), 0)::numeric as total - FROM payments p - INNER JOIN orders o ON p.order_id = o.id - WHERE o.restaurant_id = ? AND p.status = 'paid' - `, - [restId], - ); - const totalRevenue = Number(revenueResult[0]?.total || 0); - - return { - totalOrders, - activeFoods, - totalClients, - totalRevenue, - }; - }); - } - - async getFoodSalesPieChart(restId: string) { - return this.em.transactional(async em => { - // Use last 30 days instead of just last month to be more inclusive - const now = new Date(); - const endDate = new Date(now); - endDate.setHours(23, 59, 59, 999); - const startDate = new Date(now); - startDate.setDate(startDate.getDate() - 30); - startDate.setHours(0, 0, 0, 0); - - // Calculate actual last month for the period display - const firstDayOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - firstDayOfLastMonth.setHours(0, 0, 0, 0); - const lastDayOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); - - this.logger.debug( - `Food sales pie chart query params: restId=${restId}, startDate=${startDate.toISOString()}, endDate=${endDate.toISOString()}`, - ); - - // Diagnostic query to check what orders exist - const diagnosticResult = await em.execute( - ` - SELECT - o.id, - o.status, - o.created_at, - COUNT(oi.id) as item_count - FROM orders o - LEFT JOIN order_items oi ON oi.order_id = o.id - WHERE o.restaurant_id = ? - GROUP BY o.id, o.status, o.created_at - ORDER BY o.created_at DESC - LIMIT 10 - `, - [restId], - ); - this.logger.debug(`Diagnostic: Found ${diagnosticResult.length} recent orders for restaurant ${restId}`); - if (diagnosticResult.length > 0) { - this.logger.debug(`Sample order: status=${diagnosticResult[0].status}, created_at=${diagnosticResult[0].created_at}`); - } - - // Query order items from orders in the date range - // Only include completed orders (excluding canceled and pending) - const result = await em.execute( - ` - SELECT - f.id as food_id, - f.title as food_title, - COALESCE(SUM(oi.quantity), 0)::int as total_quantity, - COALESCE(SUM(oi.total_price), 0)::numeric as total_revenue - FROM order_items oi - INNER JOIN orders o ON oi.order_id = o.id - INNER JOIN foods f ON oi.food_id = f.id - WHERE o.restaurant_id = ? - AND o.created_at >= ? - AND o.created_at <= ? - AND o.status NOT IN ('pendingPayment', 'canceled') - GROUP BY f.id, f.title - ORDER BY total_revenue DESC - `, - [restId, startDate, endDate], - ); - - this.logger.debug(`Food sales pie chart query returned ${result.length} rows`); - - // Calculate total revenue for percentage calculation - const totalRevenue = result.reduce((sum: number, item: any) => { - return sum + Number(item.total_revenue || 0); - }, 0); - - // Format data for pie chart and calculate percentages - const chartData = result.map((item: any) => ({ - foodId: item.food_id, - foodTitle: item.food_title || 'Unknown', - quantity: Number(item.total_quantity || 0), - revenue: Number(item.total_revenue || 0), - percentage: totalRevenue > 0 ? Number(((Number(item.total_revenue || 0) / totalRevenue) * 100).toFixed(2)) : 0, - })); - - return { - period: { - startDate: firstDayOfLastMonth.toISOString(), - endDate: lastDayOfLastMonth.toISOString(), - }, - totalRevenue, - data: chartData, - }; - }); - } -} diff --git a/src/modules/orders/repositories/order.repository.ts b/src/modules/orders/repositories/order.repository.ts deleted file mode 100644 index 130d931..0000000 --- a/src/modules/orders/repositories/order.repository.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { FilterQuery } from '@mikro-orm/core'; -import { Order } from '../entities/order.entity'; -import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; -import { OrderStatus } from '../interface/order.interface'; -import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment'; -import { Review } from '../../review/entities/review.entity'; - -type FindOrdersOpts = { - page?: number; - limit?: number; - statuses?: OrderStatus[]; - paymentStatus?: PaymentStatusEnum; - search?: string; - startDate?: string; - endDate?: string; - orderBy?: string; - order?: 'asc' | 'desc'; - userId?: string; - excludeOnlinePendingPayment?: boolean; -}; - -@Injectable() -export class OrderRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, Order); - } - - /** - * Find orders with pagination and optional filters. - * Supports: statuses, paymentStatus, search (orderNumber), date range, ordering. - */ - async findAllPaginated(restId: string, opts: FindOrdersOpts = {}): Promise> { - const { - page = 1, - limit = 10, - statuses, - search, - startDate, - endDate, - orderBy = 'createdAt', - order = 'desc', - userId, - excludeOnlinePendingPayment = false, - } = opts; - - const offset = (page - 1) * limit; - - const where: FilterQuery = { restaurant: { id: restId } }; - - // Filter by statuses - if (statuses) { - // Ensure statuses is always an array and normalize it - const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses]; - // Filter out any empty values - const validStatuses = normalizedStatuses.filter((s): s is OrderStatus => !!s); - - if (validStatuses.length > 0) { - // Use direct equality for single value, $in for multiple values to avoid SQL generation issues - if (validStatuses.length === 1) { - where.status = validStatuses[0]; - } else { - where.status = { $in: validStatuses }; - } - } - } - - if (userId) { - where.user = { id: userId }; - } - - // Filter by date range - if (startDate || endDate) { - where.createdAt = {}; - if (startDate) { - where.createdAt.$gte = new Date(startDate); - } - if (endDate) { - where.createdAt.$lte = new Date(endDate); - } - } - - // Search by order number or user information - if (search) { - const searchPattern = `%${search}%`; - const searchConditions: any[] = [ - { user: { firstName: { $ilike: searchPattern } } }, - { user: { lastName: { $ilike: searchPattern } } }, - { user: { phone: { $ilike: searchPattern } } }, - ]; - - // If search is numeric, also search by orderNumber - const numericSearch = Number(search); - if (!isNaN(numericSearch) && isFinite(numericSearch)) { - searchConditions.push({ orderNumber: numericSearch }); - } - - where.$or = searchConditions; - } - - // Filter: Exclude orders with payment method Online and status pending_payment - if (excludeOnlinePendingPayment) { - const existingConditions = where.$and || []; - where.$and = [ - ...existingConditions, - { - $or: [ - { paymentMethod: { method: { $ne: PaymentMethodEnum.Online } } }, - { status: { $ne: OrderStatus.PENDING_PAYMENT } }, - ], - }, - ]; - } - - // First, fetch orders without reviews - const [data, total] = await this.findAndCount(where, { - limit, - offset, - orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['user', 'restaurant', 'deliveryMethod', 'paymentMethod', 'items', 'items.food'] as never, - }); - - // Collect all (orderId, foodId) pairs for efficient review lookup - const orderFoodPairs: Array<{ orderId: string; foodId: string }> = []; - for (const order of data) { - for (const item of order.items.getItems()) { - if (item.food?.id) { - orderFoodPairs.push({ orderId: order.id, foodId: item.food.id }); - } - } - } - - // Fetch all relevant reviews in a single query - const reviewsMap: Map = new Map(); - if (orderFoodPairs.length > 0) { - const orderIds = [...new Set(orderFoodPairs.map(p => p.orderId))]; - const foodIds = [...new Set(orderFoodPairs.map(p => p.foodId))]; - - const reviews = await this.em.find( - Review, - { - order: { id: { $in: orderIds } }, - food: { id: { $in: foodIds } }, - }, - { - fields: ['id', 'order', 'food'], - populate: ['order', 'food'], - }, - ); - - // Create a map: key = `${orderId}-${foodId}`, value = reviewId - for (const review of reviews) { - const key = `${review.order.id}-${review.food.id}`; - reviewsMap.set(key, review.id); - } - } - - // Map reviewIds to foods - for (const order of data) { - for (const item of order.items.getItems()) { - if (item.food) { - const key = `${order.id}-${item.food.id}`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const food = item.food as any; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - food.reviewId = reviewsMap.get(key) || null; - } - } - } - - const totalPages = Math.ceil(total / limit); - - return { - data, - meta: { - total, - page, - limit, - totalPages, - }, - }; - } -} diff --git a/src/modules/pager/controllers/pager.controller.ts b/src/modules/pager/controllers/pager.controller.ts deleted file mode 100644 index c5a10e9..0000000 --- a/src/modules/pager/controllers/pager.controller.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Controller, Get, Post, Body, UseGuards, Res, Req, Patch, Param } from '@nestjs/common'; -import { PagerService } from '../providers/pager.service'; -import { CreatePagerDto } from '../dto/create-pager.dto'; -import { OptionalAuthGuard } from '../../auth/guards/optinalAuth.guard'; -import { RestId } from 'src/common/decorators/rest-id.decorator'; -import { UserId } from 'src/common/decorators/user-id.decorator'; -import { RestSlug } from 'src/common/decorators/rest-slug.decorator'; -import { ApiTags, ApiOperation, ApiBody, ApiHeader, ApiBearerAuth, ApiParam } from '@nestjs/swagger'; -import type { FastifyReply, FastifyRequest } from 'fastify'; -import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard'; -import { AdminId } from 'src/common/decorators/admin-id.decorator'; -import { UpdatePagerStatusDto } from '../dto/update-pager.dto'; -import { PagerMessage } from 'src/common/enums/message.enum'; -import { Permissions } from 'src/common/decorators/permissions.decorator'; -import { Permission } from 'src/common/enums/permission.enum'; -import { API_HEADER_SLUG } from 'src/common/constants'; - -@ApiTags('pager') -@Controller() -export class PagerController { - constructor(private readonly pagerService: PagerService) { } - - @UseGuards(OptionalAuthGuard) - @Post('public/pager') - @ApiOperation({ summary: 'Create a new pager request' }) - @ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier' }) - @ApiHeader({ name: 'Authorization', required: false, description: 'Bearer token (optional authentication)' }) - @ApiBody({ type: CreatePagerDto }) - async create( - @Body() createPagerDto: CreatePagerDto, - @Req() request: FastifyRequest, - @Res() reply: FastifyReply, - @RestSlug() slug: string, - @RestId() restId?: string, - @UserId() userId?: string, - ) { - // Convert empty strings to undefined for cleaner validation - const normalizedRestId = restId || undefined; - const normalizedUserId = userId || undefined; - const userCookieId = request.cookies['pagerId'] || undefined; - - const pager = await this.pagerService.create(createPagerDto, { - restId: normalizedRestId, - userId: normalizedUserId, - userCookieId, - slug, - }); - reply.setCookie('pagerId', pager.cookieId, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 1000 * 60 * 60 * 24 * 1, // 1 days - path: '/', - }); - return reply.code(200).send({ - success: true, - message: PagerMessage.CREATED_SUCCESS, - data: null, - }); - } - - - @UseGuards(OptionalAuthGuard) - @Get('public/pager') - @ApiOperation({ summary: 'Get all pager requests for the current session' }) - @ApiHeader(API_HEADER_SLUG) - @ApiHeader({ name: 'Authorization', required: false, description: 'Bearer token (optional authentication)' }) - async findAll(@Req() request: FastifyRequest) { - const cookieId = request.cookies['pagerId'] || undefined; - if (!cookieId) { - return []; - } - return this.pagerService.findAll(cookieId); - } - - /******************** Admin Routes **********************/ - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAGER) - @ApiBearerAuth() - @Get('admin/pager') - @ApiOperation({ summary: 'Get all pager requests for the restaurant' }) - async findAllAdmin(@RestId() restId: string) { - return this.pagerService.findAllByRestaurantId(restId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAGER) - @ApiBearerAuth() - @Patch('admin/pager/:pagerId/status') - @ApiParam({ name: 'pagerId', required: true, type: String }) - @ApiOperation({ summary: 'Update the status of a pager request' }) - @ApiBody({ type: UpdatePagerStatusDto }) - async updateStatus( - @Param('pagerId') pagerId: string, - @RestId() restId: string, - @AdminId() adminId: string, - @Body() dto: UpdatePagerStatusDto, - ) { - return this.pagerService.updateStatus(pagerId, restId, adminId, dto.status); - } -} diff --git a/src/modules/pager/decorators/ws-admin-id.decorator.ts b/src/modules/pager/decorators/ws-admin-id.decorator.ts deleted file mode 100644 index 18ed9c3..0000000 --- a/src/modules/pager/decorators/ws-admin-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 admin ID from authenticated WebSocket client - * Use this decorator in WebSocket handlers to get the adminId from the authenticated admin - * - * @example - * ```typescript - * @SubscribeMessage('some:event') - * handleEvent(@WsAdminId() adminId: string) { - * // adminId is automatically extracted from authenticated admin - * } - * ``` - */ -export const WsAdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => { - const client = ctx.switchToWs().getClient(); - const adminId = client.adminId; - - if (!adminId) { - throw new Error('Admin ID not found. Ensure WsAdminAuthGuard is applied.'); - } - - return adminId; -}); diff --git a/src/modules/pager/decorators/ws-rest-id.decorator.ts b/src/modules/pager/decorators/ws-rest-id.decorator.ts deleted file mode 100644 index bf556ef..0000000 --- a/src/modules/pager/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('join:restaurant') - * handleJoinRestaurant(@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/pager/dto/create-pager.dto.ts b/src/modules/pager/dto/create-pager.dto.ts deleted file mode 100644 index af5d45f..0000000 --- a/src/modules/pager/dto/create-pager.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -export class CreatePagerDto { - @IsString() - @IsNotEmpty() - @ApiProperty({ description: 'Table number' }) - tableNumber!: string; - - @IsString() - @IsOptional() - @ApiPropertyOptional({ description: 'Message' }) - message?: string; -} diff --git a/src/modules/pager/dto/pager-socket-connection.dto.ts b/src/modules/pager/dto/pager-socket-connection.dto.ts deleted file mode 100644 index 49ed5c9..0000000 --- a/src/modules/pager/dto/pager-socket-connection.dto.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { PagerStatus } from '../interface/pager'; - -/** - * Socket connection configuration for admin pager module - * Used by frontend to establish WebSocket connection - */ -export interface PagerSocketConnectionConfig { - /** - * WebSocket server URL - * Example: 'http://localhost:3000' or 'https://api.example.com' - */ - serverUrl: string; - - /** - * Socket namespace (default: '/pager') - * The gateway is configured to use '/pager' namespace - */ - namespace?: string; - - /** - * JWT authentication token for admin - * Token should contain adminId and restId in payload - * Can be passed via auth.token, auth.authorization, query.token, or headers.authorization - * Example: { auth: { token: 'your-jwt-token' } } - */ - token: string; - - /** - * Optional: Additional connection options - */ - options?: { - /** - * Enable auto-reconnection - * @default true - */ - autoConnect?: boolean; - - /** - * Connection timeout in milliseconds - * @default 5000 - */ - timeout?: number; - - /** - * Enable transports (websocket, polling) - * @default ['websocket', 'polling'] - */ - transports?: ('websocket' | 'polling')[]; - }; -} - -/** - * Socket event names for pager module - */ -export enum PagerSocketEvents { - // Client to Server Events - JOIN_RESTAURANT = 'join:restaurant', - LEAVE_ROOM = 'leave:room', - GET_PAGERS = 'get:pagers', - UPDATE_PAGER_STATUS = 'update:pager:status', - - // Server to Client Events - JOINED = 'joined', - LEFT = 'left', - PAGER_CREATED = 'pager:created', - PAGER_STATUS_UPDATED = 'pager:status:updated', - PAGERS_LIST = 'pagers:list', - PAGER_STATUS_UPDATED_RESPONSE = 'pager:status:updated:response', - ERROR = 'error', - CONNECT = 'connect', - DISCONNECT = 'disconnect', -} - -/** - * Payload for joining restaurant room - * Note: restId is automatically extracted from JWT token, no need to send it - */ -export type JoinRestaurantPayload = Record; - -/** - * Payload for leaving a room - */ -export interface LeaveRoomPayload { - room: string; -} - -/** - * Payload for getting pagers list - * Note: restId is automatically extracted from JWT token, no need to send it - */ -export type GetPagersPayload = Record; - -/** - * Payload for updating pager status - * Note: restId and adminId are automatically extracted from JWT token - */ -export interface UpdatePagerStatusPayload { - pagerId: string; - status: PagerStatus; -} - -/** - * Response when successfully joined a room - */ -export interface JoinedResponse { - room: string; - message: string; -} - -/** - * Response when successfully left a room - */ -export interface LeftResponse { - room: string; - message: string; -} - -/** - * User information in pager response - */ -export interface PagerUserInfo { - id: string; - firstName: string; - lastName?: string; -} - -/** - * Staff information in pager response - */ -export interface PagerStaffInfo { - id: string; - firstName: string; - lastName?: string; -} - -/** - * Pager data structure emitted by socket events - */ -export interface PagerSocketData { - id: string; - tableNumber: string; - message?: string; - status: PagerStatus; - cookieId?: string; - createdAt?: Date | string; - updatedAt?: Date | string; - user?: PagerUserInfo | null; - staff?: PagerStaffInfo | null; -} - -/** - * Payload for pager:created event - */ -export interface PagerCreatedPayload { - pager: PagerSocketData; -} - -/** - * Payload for pager:status:updated event - */ -export interface PagerStatusUpdatedPayload { - pager: PagerSocketData; -} - -/** - * Payload for pagers:list event - */ -export interface PagersListPayload { - pagers: PagerSocketData[]; -} - -/** - * Payload for pager:status:updated:response event (response to update request) - */ -export interface PagerStatusUpdatedResponsePayload { - pager: PagerSocketData; -} - -/** - * Error response from socket - */ -export interface SocketErrorResponse { - message: string; - code?: string; - details?: unknown; -} - -/** - * Complete socket event map for type safety - */ -export interface PagerSocketEventMap { - // Client emits - [PagerSocketEvents.JOIN_RESTAURANT]: (payload?: JoinRestaurantPayload) => void; - [PagerSocketEvents.LEAVE_ROOM]: (payload: LeaveRoomPayload) => void; - [PagerSocketEvents.GET_PAGERS]: (payload?: GetPagersPayload) => void; - [PagerSocketEvents.UPDATE_PAGER_STATUS]: (payload: UpdatePagerStatusPayload) => void; - - // Server emits - [PagerSocketEvents.JOINED]: (response: JoinedResponse) => void; - [PagerSocketEvents.LEFT]: (response: LeftResponse) => void; - [PagerSocketEvents.PAGER_CREATED]: (payload: PagerCreatedPayload) => void; - [PagerSocketEvents.PAGER_STATUS_UPDATED]: (payload: PagerStatusUpdatedPayload) => void; - [PagerSocketEvents.PAGERS_LIST]: (payload: PagersListPayload) => void; - [PagerSocketEvents.PAGER_STATUS_UPDATED_RESPONSE]: (payload: PagerStatusUpdatedResponsePayload) => void; - [PagerSocketEvents.ERROR]: (error: SocketErrorResponse) => void; - [PagerSocketEvents.CONNECT]: () => void; - [PagerSocketEvents.DISCONNECT]: (reason: string) => void; -} diff --git a/src/modules/pager/dto/update-pager.dto.ts b/src/modules/pager/dto/update-pager.dto.ts deleted file mode 100644 index e66b14b..0000000 --- a/src/modules/pager/dto/update-pager.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty } from 'class-validator'; -import { PagerStatus } from '../interface/pager'; - -export class UpdatePagerStatusDto { - @ApiProperty({ description: 'Status of the pager', enum: PagerStatus }) - @IsEnum(PagerStatus) - @IsNotEmpty() - status!: PagerStatus; -} diff --git a/src/modules/pager/entities/pager.entity.ts b/src/modules/pager/entities/pager.entity.ts deleted file mode 100644 index 3c29bb2..0000000 --- a/src/modules/pager/entities/pager.entity.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Entity, ManyToOne, Property, Enum } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { User } from '../../users/entities/user.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { PagerStatus } from '../interface/pager'; -import { Admin } from 'src/modules/admin/entities/admin.entity'; - -@Entity({ tableName: 'pagers' }) -export class Pager extends BaseEntity { - @Property({ type: 'string' }) - cookieId!: string; - - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - - @Property({ type: 'string' }) - tableNumber!: string; - - @ManyToOne(() => User, { nullable: true }) - user?: User; - - @ManyToOne(() => Admin, { nullable: true }) - staff?: Admin; - - @Property({ type: 'string', nullable: true }) - message?: string; - - @Enum(() => PagerStatus) - status: PagerStatus = PagerStatus.Pending; -} diff --git a/src/modules/pager/events/pager.events.ts b/src/modules/pager/events/pager.events.ts deleted file mode 100644 index a5236a6..0000000 --- a/src/modules/pager/events/pager.events.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class PagerCreatedEvent { - constructor( - public readonly restaurantId: string, - public readonly tableNumber: string, - ) {} -} diff --git a/src/modules/pager/guards/ws-admin-auth.guard.ts b/src/modules/pager/guards/ws-admin-auth.guard.ts deleted file mode 100644 index 2e15939..0000000 --- a/src/modules/pager/guards/ws-admin-auth.guard.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable, Logger, Inject } from '@nestjs/common'; -import { WsException } from '@nestjs/websockets'; -import { Socket } from 'socket.io'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; -import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload'; - -export interface AuthenticatedSocket extends Socket { - adminId?: string; - restId?: string; -} - -@Injectable() -export class WsAdminAuthGuard implements CanActivate { - private readonly logger = new Logger(WsAdminAuthGuard.name); - - constructor( - @Inject(JwtService) - private readonly jwtService: JwtService, - @Inject(ConfigService) - private readonly configService: ConfigService, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const client: Socket = context.switchToWs().getClient(); - - try { - const token = this.extractToken(client); - - if (!token) { - this.logger.warn('No token provided in WebSocket connection', { - socketId: client.id, - auth: client.handshake.auth, - query: client.handshake.query, - }); - throw new WsException('Authentication required'); - } - - const secret = this.configService.getOrThrow('JWT_SECRET'); - const payload = await this.jwtService.verifyAsync(token, { - secret, - }); - - if (!payload.adminId || !payload.restId) { - this.logger.error('Invalid token payload structure', payload); - throw new WsException('Invalid token payload'); - } - - // Attach admin info to socket - (client as AuthenticatedSocket).adminId = payload.adminId; - (client as AuthenticatedSocket).restId = payload.restId; - - this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, restId: ${payload.restId}`); - - return true; - } catch (error) { - if (error instanceof WsException) { - throw error; - } - this.logger.error('WebSocket authentication error', { - error: error instanceof Error ? error.message : 'Unknown error', - socketId: client.id, - }); - throw new WsException('Authentication failed'); - } - } - - private extractToken(client: Socket): string | undefined { - // Try to get token from handshake auth (recommended for socket.io) - const auth = client.handshake.auth; - const authToken = (auth?.token as string | undefined) || (auth?.authorization as string | undefined); - - if (authToken) { - // If it's "Bearer ", extract just the token - if (typeof authToken === 'string' && authToken.startsWith('Bearer ')) { - return authToken.substring(7); - } - return authToken; - } - - // Try to get from query parameters (fallback) - const queryToken = client.handshake.query?.token || client.handshake.query?.authorization; - - if (queryToken) { - if (typeof queryToken === 'string' && queryToken.startsWith('Bearer ')) { - return queryToken.substring(7); - } - return queryToken as string; - } - - // Try to get from headers (if available) - const headers = client.handshake.headers; - const authHeader = headers.authorization || headers['authorization']; - - if (authHeader && typeof authHeader === 'string') { - const [type, token] = authHeader.split(' '); - if (type?.toLowerCase() === 'bearer' && token) { - return token; - } - } - - return undefined; - } -} diff --git a/src/modules/pager/interface/pager.ts b/src/modules/pager/interface/pager.ts deleted file mode 100644 index afe6fdc..0000000 --- a/src/modules/pager/interface/pager.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum PagerStatus { - Pending = 'pending', - Acknowledged = 'acknowledged', - Rejected = 'rejected', - Completed = 'completed', -} diff --git a/src/modules/pager/listeners/notification.listeners.ts b/src/modules/pager/listeners/notification.listeners.ts deleted file mode 100644 index c1960c6..0000000 --- a/src/modules/pager/listeners/notification.listeners.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -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) { - try { - this.logger.log(`Pager created event received: ${event.restaurantId}`); - // get admnin os restuaraant that have pager permissuins - const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.PAGER_NOTIFICATION); - const recipients = admins.map(admin => ({ - adminId: admin.id, - })); - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.PAGER_CREATED, - content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`, - sms: { - templateId: this.smsPatternPagerCreated, - parameters: { - tableNumber: event.tableNumber.toString(), - }, - }, - pushNotif: { - title: ` پیج جدید`, - content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`, - icon: `/assets/images/logo.png`, - action: { - type: NotifTitleEnum.PAGER_CREATED, - url: `/restaurants/${event.restaurantId}/pagers`, - }, - }, - }, - recipients, - metadata: { - priority: 1, - }, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for pager created event: ${event.restaurantId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } -} diff --git a/src/modules/pager/pager.module.ts b/src/modules/pager/pager.module.ts deleted file mode 100644 index 486fadf..0000000 --- a/src/modules/pager/pager.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PagerService } from './providers/pager.service'; -import { PagerController } from './controllers/pager.controller'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { Pager } from './entities/pager.entity'; -import { JwtModule } from '@nestjs/jwt'; -import { RolesModule } from '../roles/roles.module'; -import { PagerListeners } from './listeners/notification.listeners'; -import { AdminModule } from '../admin/admin.module'; -import { NotificationsModule } from '../notifications/notifications.module'; - -@Module({ - imports: [MikroOrmModule.forFeature([Pager]), JwtModule, RolesModule, AdminModule, NotificationsModule], - controllers: [PagerController], - providers: [PagerService, PagerListeners], -}) -export class PagerModule {} diff --git a/src/modules/pager/providers/pager.service.ts b/src/modules/pager/providers/pager.service.ts deleted file mode 100644 index 1210551..0000000 --- a/src/modules/pager/providers/pager.service.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { BadRequestException, Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common'; -import { CreatePagerDto } from '../dto/create-pager.dto'; -import { User } from '../../users/entities/user.entity'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { Pager } from '../entities/pager.entity'; -import { PagerStatus } from '../interface/pager'; -import { ulid } from 'ulid'; -import { Admin } from '../../admin/entities/admin.entity'; -import { PagerCreatedEvent } from '../events/pager.events'; -import { EventEmitter2 } from '@nestjs/event-emitter'; - -@Injectable() -export class PagerService { - constructor( - private readonly em: EntityManager, - // @Inject(forwardRef(() => PagerGateway)) - // private readonly pagerGateway: PagerGateway, - private readonly eventEmitter: EventEmitter2, - ) {} - - async create( - createPagerDto: CreatePagerDto, - params: { restId?: string; userId?: string; slug: string; userCookieId?: string }, - ) { - const { restId, userId, slug, userCookieId } = params; - let restaurant: Restaurant | null = null; - let user: User | null = null; - if (!restId && !userId && !slug) { - throw new BadRequestException('Invalid parameters'); - } - if (restId) { - restaurant = await this.em.findOne(Restaurant, { id: restId }); - if (!restaurant) { - throw new NotFoundException('Restaurant not found'); - } - } - if (userId) { - user = await this.em.findOne(User, { id: userId }); - if (!user) { - throw new NotFoundException('User not found'); - } - } - if (!restaurant && slug) { - restaurant = await this.em.findOne(Restaurant, { slug }); - if (!restaurant) { - throw new NotFoundException('Restaurant not found'); - } - } - const cookieId = userCookieId || ulid().toString(); - // const existingPager = await this.em.findOne(Pager, { - // restaurant: { id: restaurant!.id }, - // status: PagerStatus.Pending, - // cookieId, - // }); - // if (existingPager) { - // throw new BadRequestException('Pager already exists'); - // } - - const pager = this.em.create(Pager, { - ...createPagerDto, - restaurant: restaurant!, - user, - cookieId: ulid().toString(), - status: PagerStatus.Pending, - }); - - await this.em.persistAndFlush(pager); - - // Populate relations before emitting - await this.em.populate(pager, ['restaurant']); - // Emit socket event for new pager - this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id, pager.tableNumber)); - - return pager; - } - - async findAll(cookieId: string) { - const pagers = await this.em.find(Pager, { cookieId }, { populate: ['user'] }); - return pagers; - } - - async findAllByRestaurantId(restId: string) { - const pagers = await this.em.find(Pager, { restaurant: { id: restId } }, { populate: ['user'] }); - return pagers; - } - - async updateStatus(pagerId: string, restId: string, staffId: string, status: PagerStatus) { - const pager = await this.em.findOne( - Pager, - { id: pagerId, restaurant: { id: restId } }, - { populate: ['restaurant'] }, - ); - if (!pager) { - throw new NotFoundException('Pager not found *'); - } - const staff = await this.em.findOne(Admin, { id: staffId }); - if (!staff) { - throw new NotFoundException('Staff not found'); - } - pager.status = status; - if (status === PagerStatus.Acknowledged) { - pager.staff = staff; - } - await this.em.persistAndFlush(pager); - - // Populate relations before emitting - await this.em.populate(pager, ['restaurant', 'staff', 'user']); - - // Emit socket event for status update - // this.pagerGateway.emitPagerStatusUpdated(pager); - - return pager; - } -} diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts deleted file mode 100644 index a9d208e..0000000 --- a/src/modules/payments/controllers/payments.controller.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common'; -import { PaymentsService } from '../services/payments.service'; -import { - ApiTags, - ApiOperation, - ApiNotFoundResponse, - ApiBody, - ApiParam, - ApiBearerAuth, - ApiHeader, -} from '@nestjs/swagger'; -import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; -import { PaymentMethodService } from '../services/payment-method.service'; -import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto'; -import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto'; -import { VerifyPaymentDto } from '../dto/verify-payment.dto'; -import { PaymentChartDto } from '../dto/payment-chart.dto'; -import { RestId, UserId } from 'src/common/decorators'; -import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; -import { API_HEADER_SLUG } from 'src/common/constants'; -import { Permissions } from 'src/common/decorators/permissions.decorator'; -import { Permission } from 'src/common/enums/permission.enum'; - -@ApiTags('payments') -@Controller() -export class PaymentsController { - constructor( - private readonly paymentsService: PaymentsService, - private readonly paymentMethodService: PaymentMethodService, - ) { } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Get('public/payments/methods/restaurant') - @ApiOperation({ summary: 'Get the restaurant payment methods' }) - @ApiHeader(API_HEADER_SLUG) - @ApiNotFoundResponse({ description: 'Restaurant payment methods not found' }) - getTheRestaurantPaymentMethods(@RestId() restId: string) { - return this.paymentMethodService.findByRestaurant(restId); - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Get('public/payments') - @ApiOperation({ summary: 'Get all the restaurant payments for user' }) - @ApiHeader(API_HEADER_SLUG) - findAllByRestaurant(@UserId() userId: string, @RestId() restId: string) { - return this.paymentsService.findAllPaymentsByRestaurantId(restId, userId); - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Get('public/payments/pay-order/:orderId') - @ApiOperation({ summary: 'Pay for an order' }) - @ApiParam({ name: 'orderId', type: 'string', description: 'Order ID' }) - @ApiHeader(API_HEADER_SLUG) - payAnOrder(@Param('orderId') orderId: string) { - return this.paymentsService.payOrder(orderId); - } - - @Post('public/payments/verify') - @ApiOperation({ summary: 'Verify a payment' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: VerifyPaymentDto }) - @ApiNotFoundResponse({ description: 'Payment not found' }) - verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) { - return this.paymentsService.verifyOnlinePayment(verifyPaymentDto.authority, verifyPaymentDto.orderId); - } - - /** admin routes */ - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAYMENTS) - @ApiBearerAuth() - @Get('admin/payments/methods') - @ApiOperation({ summary: 'Get restaurant all payment methods' }) - findByRestaurant(@RestId() restId: string) { - return this.paymentMethodService.findByRestaurant(restId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAYMENTS) - @ApiBearerAuth() - @Post('admin/payments/methods') - @ApiOperation({ summary: 'Create a new restaurant payment method' }) - @ApiBody({ type: CreatePaymentMethodDto }) - createRestaurantPaymentMethod(@RestId() restId: string, @Body() createPaymentMethodDto: CreatePaymentMethodDto) { - return this.paymentMethodService.create(restId, createPaymentMethodDto); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAYMENTS) - @ApiBearerAuth() - @Get('admin/payments/methods/:paymentMethodId') - @ApiOperation({ summary: 'Get restaurant payment method by ID' }) - @ApiParam({ name: 'paymentMethodId', type: 'string', description: 'Payment method ID' }) - findOneRestaurantPaymentMethod(@Param('paymentMethodId') paymentMethodId: string) { - return this.paymentMethodService.findOne(paymentMethodId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAYMENTS) - @ApiBearerAuth() - @Patch('admin/payments/methods/:paymentMethodId') - @ApiOperation({ summary: 'Update a restaurant payment method' }) - @ApiParam({ name: 'paymentMethodId', type: 'string', description: 'Payment method ID' }) - @ApiBody({ type: UpdatePaymentMethodDto }) - updateRestaurantPaymentMethod( - @Param('paymentMethodId') paymentMethodId: string, - @Body() updatePaymentMethodDto: UpdatePaymentMethodDto, - ) { - return this.paymentMethodService.update(paymentMethodId, updatePaymentMethodDto); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAYMENTS) - @ApiBearerAuth() - @Delete('admin/payments/methods/:paymentMethodId') - @ApiOperation({ summary: 'Delete a restaurant payment method' }) - @ApiParam({ name: 'paymentMethodId', type: 'string', description: 'Payment method ID' }) - removeRestaurantPaymentMethod(@Param('paymentMethodId') paymentMethodId: string) { - return this.paymentMethodService.remove(paymentMethodId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_PAYMENTS) - @ApiBearerAuth() - @Post('admin/payments/verify-cash-method/:paymentId') - @ApiOperation({ summary: 'Verify cash payment ' }) - @ApiParam({ name: 'paymentId', type: 'string', description: 'Payment ID' }) - verifyCashPayment(@Param('paymentId') paymentId: string) { - return this.paymentsService.verifyCashPayment(paymentId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.VIEW_REPORTS) - @ApiBearerAuth() - @Get('admin/payments/chart') - @ApiOperation({ summary: 'Get payment chart data with date and period filters' }) - @ApiHeader(API_HEADER_SLUG) - getPaymentChart(@Query() query: PaymentChartDto, @RestId() restId: string) { - return this.paymentsService.getChartData(query, restId); - } -} diff --git a/src/modules/payments/dto/create-payment-method.dto.ts b/src/modules/payments/dto/create-payment-method.dto.ts deleted file mode 100644 index 24acc82..0000000 --- a/src/modules/payments/dto/create-payment-method.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IsString, IsOptional, IsBoolean, IsNumber, IsEnum } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { PaymentMethodEnum, PaymentGatewayEnum } from '../interface/payment'; - -export class CreatePaymentMethodDto { - - @ApiProperty({ description: 'Payment method', enum: PaymentMethodEnum }) - @IsEnum(PaymentMethodEnum) - method!: PaymentMethodEnum; - - @ApiProperty({ description: 'Payment gateway', enum: PaymentGatewayEnum, required: false }) - @IsOptional() - @IsEnum(PaymentGatewayEnum) - gateway?: PaymentGatewayEnum | null; - - @ApiProperty({ description: 'Payment method description', required: false }) - @IsOptional() - @IsString() - description?: string; - - @ApiProperty({ description: 'Is payment method enabled', required: false, default: true }) - @IsOptional() - @IsBoolean() - enabled?: boolean; - - @ApiProperty({ description: 'Display order', required: false, default: 0 }) - @IsOptional() - @IsNumber() - order?: number; - - @ApiProperty({ description: 'Merchant ID', required: false }) - @IsOptional() - @IsString() - merchantId?: string; -} diff --git a/src/modules/payments/dto/payment-chart.dto.ts b/src/modules/payments/dto/payment-chart.dto.ts deleted file mode 100644 index ae1f87b..0000000 --- a/src/modules/payments/dto/payment-chart.dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { IsOptional, IsString, IsEnum, IsDateString } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export enum ChartPeriodEnum { - Daily = 'daily', - Weekly = 'weekly', - Monthly = 'monthly', -} - -export class PaymentChartDto { - @ApiPropertyOptional({ - description: 'Start date for filtering (ISO date string)', - type: String, - format: 'date-time', - example: '2024-01-01T00:00:00Z', - }) - @IsOptional() - @IsDateString() - startDate?: string; - - @ApiPropertyOptional({ - description: 'End date for filtering (ISO date string)', - type: String, - format: 'date-time', - example: '2024-12-31T23:59:59Z', - }) - @IsOptional() - @IsDateString() - endDate?: string; - - @ApiPropertyOptional({ - description: 'Period type for grouping data', - enum: ChartPeriodEnum, - default: ChartPeriodEnum.Daily, - example: ChartPeriodEnum.Daily, - }) - @IsOptional() - @IsEnum(ChartPeriodEnum) - type?: ChartPeriodEnum = ChartPeriodEnum.Daily; -} - diff --git a/src/modules/payments/dto/update-payment-method.dto.ts b/src/modules/payments/dto/update-payment-method.dto.ts deleted file mode 100644 index 7151364..0000000 --- a/src/modules/payments/dto/update-payment-method.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreatePaymentMethodDto } from './create-payment-method.dto'; - -export class UpdatePaymentMethodDto extends PartialType(CreatePaymentMethodDto) {} diff --git a/src/modules/payments/dto/update-payment.dto.ts b/src/modules/payments/dto/update-payment.dto.ts deleted file mode 100644 index a3d0fd2..0000000 --- a/src/modules/payments/dto/update-payment.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsNumber } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdatePaymentDto { - @ApiProperty({ description: 'Payment ID' }) - @IsNumber() - id: number; -} diff --git a/src/modules/payments/dto/verify-payment.dto.ts b/src/modules/payments/dto/verify-payment.dto.ts deleted file mode 100644 index 06d5ecb..0000000 --- a/src/modules/payments/dto/verify-payment.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class VerifyPaymentDto { - @ApiProperty({ description: 'Payment authority token' }) - @IsString() - authority: string; - - @ApiProperty({ description: 'Payment order id' }) - @IsString() - orderId: string; -} diff --git a/src/modules/payments/entities/payment-method.entity.ts b/src/modules/payments/entities/payment-method.entity.ts deleted file mode 100644 index 63f5ad9..0000000 --- a/src/modules/payments/entities/payment-method.entity.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Entity, Enum, Index, ManyToOne, Property, Unique } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; -import { PaymentGatewayEnum, PaymentMethodEnum } from '../interface/payment'; - -@Entity({ tableName: 'payment_methods' }) -@Unique({ properties: ['restaurant', 'method'] }) -@Index({ properties: ['restaurant', 'enabled'] }) -export class PaymentMethod extends BaseEntity { - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - - @Enum(() => PaymentMethodEnum) - method!: PaymentMethodEnum; - - @Enum(() => PaymentGatewayEnum) - gateway: PaymentGatewayEnum | null = null; - - @Property({ nullable: true }) - description?: string; - - @Property({ default: true }) - enabled: boolean = true; - - @Property({ type: 'integer', default: 0 }) - order: number = 0; - - @Property({ nullable: true }) - merchantId?: string; -} diff --git a/src/modules/payments/entities/payment.entity.ts b/src/modules/payments/entities/payment.entity.ts deleted file mode 100644 index 5f2827e..0000000 --- a/src/modules/payments/entities/payment.entity.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Entity, ManyToOne, Property, Enum, Index } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Order } from '../../orders/entities/order.entity'; -import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; - -@Entity({ tableName: 'payments' }) -export class Payment extends BaseEntity { - @ManyToOne(() => Order) - @Index() - order!: Order; - - @Property({ type: 'decimal', precision: 10, scale: 0 }) - amount!: number; - - @Property({ unique: true, nullable: true }) - referenceId?: string | null; - - @Enum(() => PaymentMethodEnum) - method!: PaymentMethodEnum; - - @Enum(() => PaymentGatewayEnum) - gateway?: PaymentGatewayEnum | null = null; - - @Property({ nullable: true }) - transactionId?: string | null = null; - - @Enum(() => PaymentStatusEnum) - status!: PaymentStatusEnum; - - @Property({ nullable: true }) - cardPan?: string | null = null; - - @Property({ type: 'json', nullable: true }) - verifyResponse?: Record | null = null; - - @Property({ nullable: true }) - paidAt?: Date | null = null; - - @Property({ nullable: true }) - failedAt?: Date | null = null; - - @Property({ nullable: true }) - description?: string | null = null; -} diff --git a/src/modules/payments/events/payment.events.ts b/src/modules/payments/events/payment.events.ts deleted file mode 100644 index 3fbc443..0000000 --- a/src/modules/payments/events/payment.events.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PaymentMethodEnum } from "../interface/payment"; - -export class paymentSucceedEvent { - constructor( - public readonly orderId: string, - public readonly restaurantId: string, - public readonly orderNumber: string, - public readonly paymentMethod: PaymentMethodEnum, - public readonly total: number - ) { } -} - - - -export class onlinePaymentSucceedEvent { - constructor( - public readonly paymentId: string, - public readonly orderId: string, - public readonly restaurantId: string, - public readonly orderNumber: string, - public readonly total: number, - ) { } -} \ No newline at end of file diff --git a/src/modules/payments/gateways/gateway.manager.ts b/src/modules/payments/gateways/gateway.manager.ts deleted file mode 100644 index b441fe8..0000000 --- a/src/modules/payments/gateways/gateway.manager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; -import { PaymentGatewayEnum } from '../interface/payment'; -import { ZarinpalGateway } from './zarinpal.gateway'; -import { IPaymentGateway } from '../interface/gateway'; - -@Injectable() -export class GatewayManager { - constructor( - private readonly zarinpal: ZarinpalGateway, - ) { } - - get(gateway: PaymentGatewayEnum): IPaymentGateway { - switch (gateway) { - case PaymentGatewayEnum.ZarinPal: - return this.zarinpal; - default: - throw new BadRequestException(`Unsupported gateway: ${gateway}`); - } - } -} diff --git a/src/modules/payments/gateways/zarinpal.gateway.ts b/src/modules/payments/gateways/zarinpal.gateway.ts deleted file mode 100755 index 5b26c6e..0000000 --- a/src/modules/payments/gateways/zarinpal.gateway.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Injectable, Logger, BadRequestException } from '@nestjs/common'; -import axios from 'axios'; -import { - IPaymentGateway, - IPaymentVerifyParams, - IRequestPaymentData, - IRequestPaymentParams, - IPaymentVerifyData, - IZarinpalPaymentResponse, - IZarinpalRequestPayment, - IZarinpalVerifyRequest, - IZarinpalVerifyResponse, -} from '../interface/gateway'; -import { ConfigService } from '@nestjs/config'; -import { PaymentMessage } from 'src/common/enums/message.enum'; - -@Injectable() -export class ZarinpalGateway implements IPaymentGateway { - private readonly logger = new Logger(ZarinpalGateway.name); - private readonly zarinpalRequestUrl: string; - private readonly zarinpalVerifyUrl: string; - private readonly zarinpalPaymentBaseUrl: string; - private readonly axiosConfig = { - timeout: 10_000, - headers: { - 'Content-Type': 'application/json', - }, - }; - - constructor(private readonly configService: ConfigService) { - const zarinpalBaseUrl = this.configService.get('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com'; - this.zarinpalPaymentBaseUrl = zarinpalBaseUrl; - this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`; - this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`; - } - - private getAxiosErrorData(error: unknown): { status?: number; data?: unknown } | null { - if (!axios.isAxiosError(error) || !error.response) return null; - return { status: error.response.status, data: error.response.data }; - } - - async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): Promise { - // Transform camelCase to snake_case for Zarinpal API v4 - const callbackUrl = `${domain}/verify/${orderId}`; - const zarinpalRequest: IZarinpalRequestPayment = { - amount, - merchant_id: merchantId, - description: `Payment for order #${orderId}`, - callback_url: callbackUrl, - currency: 'IRT', - metadata: { - order_id: orderId, - }, - }; - - try { - const res = await axios.post( - this.zarinpalRequestUrl, - zarinpalRequest, - this.axiosConfig, - ); - const { data, errors } = res.data ?? {}; - const code = data?.code; - const message = data?.message; - const authority = data?.authority; - - if (!data) { - this.logger.error('Invalid response from Zarinpal API (missing data)', JSON.stringify(res.data)); - throw new BadRequestException(PaymentMessage.ZARINPAL_INVALID_RESPONSE); - } - - if ((Array.isArray(errors) && errors.length > 0) || code !== 100 || !authority) { - this.logger.error( - 'Zarinpal payment request failed', - JSON.stringify({ - code, - message, - errors, - callbackUrl, - amount, - orderId, - }), - ); - throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); - } - - return { transactionId: authority }; - } catch (error) { - if (error instanceof BadRequestException) throw error; - - const axiosErr = this.getAxiosErrorData(error); - if (axiosErr) { - this.logger.error( - 'Zarinpal payment request axios error', - JSON.stringify({ - status: axiosErr.status, - data: axiosErr.data, - callbackUrl, - amount, - orderId, - }), - ); - throw new BadRequestException(PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); - } - - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Zarinpal payment request error', errorMessage); - throw new BadRequestException(PaymentMessage.GATEWAY_ERROR); - } - } - - async verifyPayment({ merchantId, amount, transactionId }: IPaymentVerifyParams): Promise { - try { - // Transform camelCase to snake_case for Zarinpal API v4 - const zarinpalVerifyRequest: IZarinpalVerifyRequest = { - merchant_id: merchantId, - amount, - authority: transactionId, - }; - - const res = await axios.post( - this.zarinpalVerifyUrl, - zarinpalVerifyRequest, - this.axiosConfig, - ); - - // Check if response data exists - if (!res.data || !res.data.data) { - throw new BadRequestException(PaymentMessage.ZARINPAL_INVALID_RESPONSE); - } - - const { code, message, ref_id, card_pan } = res.data.data; - - // Check if there are errors in the response - if (code !== 100 && code !== 0) { - throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_VERIFY_ERROR); - } - - return { - success: code === 100, - referenceId: ref_id ? ref_id.toString() : '', - cardPan: card_pan, - raw: res.data, - }; - } catch (error) { - if (axios.isAxiosError(error) && error.response) { - this.logger.error('Zarinpal verify failed', error.response.data); - throw new BadRequestException(error.response.data); - } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new BadRequestException(PaymentMessage.GATEWAY_ERROR); - } - } - - getPaymentUrl(authority: string): string { - return `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`; - } -} diff --git a/src/modules/payments/interface/gateway.ts b/src/modules/payments/interface/gateway.ts deleted file mode 100644 index f336f2c..0000000 --- a/src/modules/payments/interface/gateway.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface IPaymentGateway { - requestPayment(requestPaymentParams: IRequestPaymentParams): Promise; - verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; - getPaymentUrl(authority: string): string; -} - -export interface IRequestPaymentParams { - amount: number; - merchantId: string; - domain: string; - orderId: string; -} - -export interface IRequestPaymentData { - transactionId: string; -} - -export interface IPaymentVerifyParams { - amount: number; - merchantId: string; - transactionId: string; -} - -export interface IPaymentVerifyData { - success: boolean; - referenceId: string; - cardPan?: string; - raw: Record; -} -////////////////////////// Zarinpal API v4 ////////////////////////// -export interface IZarinpalRequestPayment { - amount: number; - merchant_id: string; - description: string; - callback_url: string; - metadata?: { - order_id?: string; - mobile?: string; - email?: string; - }; - referrer_id?: string; - currency: 'IRT'; -} -export interface IZarinpalPaymentResponse { - data: { - code: number; - message: string; - authority: string; - fee_type: string; - fee: number; - }; - errors: unknown[]; -} -export interface IZarinpalVerifyRequest { - merchant_id: string; - amount: number; - authority: string; -} -export interface IZarinpalVerifyResponse { - data: { - code: number; - message: string; - card_hash: string; - card_pan: string; - ref_id: number; - fee_type: string; - fee: number; - }; - errors: unknown[]; -} diff --git a/src/modules/payments/interface/payment.ts b/src/modules/payments/interface/payment.ts deleted file mode 100644 index 6fd70c2..0000000 --- a/src/modules/payments/interface/payment.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Order } from 'src/modules/orders/entities/order.entity'; - -export enum PaymentMethodEnum { - Online = 'Online', - Cash = 'Cash', - Wallet = 'Wallet', -} -export enum PaymentStatusEnum { - Pending = 'pending', - Paid = 'paid', - Failed = 'failed', - Refunded = 'refunded', -} -export enum PaymentGatewayEnum { - ZarinPal = 'zarinpal', -} - -export interface ICreatePayment { - orderId: string; - amount: number; - method: PaymentMethodEnum; - transactionId: string; - gateway?: PaymentGatewayEnum | null; -} - -export interface OrderPaymentContext { - order: Order; - amount: number; - method: PaymentMethodEnum; - gateway: PaymentGatewayEnum | null; - merchantId: string | null; - restaurantDomain: string | null; -} diff --git a/src/modules/payments/listeners/payment.listeners.ts b/src/modules/payments/listeners/payment.listeners.ts deleted file mode 100644 index ed60ca7..0000000 --- a/src/modules/payments/listeners/payment.listeners.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.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'; -import { OrdersService } from 'src/modules/orders/providers/orders.service'; - -@Injectable() -export class PaymentListeners { - private readonly logger = new Logger(PaymentListeners.name); - private orderCreatedSmsTemplateId: string; - private paymentSucceedSmsTemplateId: string; - constructor( - private readonly adminService: AdminRepository, - private readonly notificationService: NotificationService, - private readonly configService: ConfigService, - private readonly orderService: OrdersService, - ) { - this.orderCreatedSmsTemplateId = this.configService.get('SMS_PATTERN_ORDER_CREATED') ?? '123'; - this.paymentSucceedSmsTemplateId = this.configService.get('SMS_PATTERN_ONLINE_PAYMENT_SUCCEED') ?? '123'; - } - - private getPaymentMethodFarsi(method: string): string { - const methodMap: Record = { - Cash: 'نقدی', - Wallet: 'کیف پول', - Online: 'آنلاین', - }; - return methodMap[method] || method; - } - - @OnEvent(paymentSucceedEvent.name) - async handlePaymentSucceed(event: paymentSucceedEvent) { - try { - this.logger.log( - `Payment paid event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, - ); - // get admnin os restuaraant that have order permissuins - const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS); - // const order=await - const recipients = admins.map(admin => ({ - adminId: admin.id, - })); - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.PAYMENT_SUCCESS, - content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`, - sms: { - templateId: this.orderCreatedSmsTemplateId, - parameters: { - orderNumber: event.orderNumber, - - }, - }, - pushNotif: { - title: `پرداخت موفق`, - content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`, - icon: ``, - action: { - type: NotifTitleEnum.PAYMENT_SUCCESS, - url: `/`, - }, - }, - }, - recipients, - metadata: { - priority: 1, - }, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for order created event: ${event.restaurantId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } - - @OnEvent(onlinePaymentSucceedEvent.name) - async handleOnlinePaymentSucceed(event: onlinePaymentSucceedEvent) { - try { - - this.logger.log( - `Online payment succeed event received: ${event.paymentId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, - ); - const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS); - const recipients = admins.map(admin => ({ - adminId: admin.id, - })); - - // admin notifs - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.ORDER_CREATED, - content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, - sms: { - templateId: this.orderCreatedSmsTemplateId, - parameters: { - orderNumber: event.orderNumber, - total: event.total.toString(), - }, - }, - pushNotif: { - title: `سفارش جدید`, - content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, - icon: `/`, - action: { - type: NotifTitleEnum.ORDER_CREATED, - url: `/`, - }, - }, - }, - recipients, - metadata: { - priority: 1, - }, - }); - - const order = await this.orderService.findOne(event.orderId, event.restaurantId); - if (!order) { - this.logger.error( - `Order not found: ${event.orderId} for restaurant: ${event.restaurantId}`, - ); - return; - } - const userRecipients = [ - { - userId: order.user.id, - }, - ]; - // user notif - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - message: { - title: NotifTitleEnum.PAYMENT_SUCCESS, - content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${order.total} تومان با موفقیت انجام شد`, - sms: { - templateId: this.paymentSucceedSmsTemplateId, - parameters: { - orderNumber: event.orderNumber, - total: event.total.toString(), - }, - }, - pushNotif: { - title: `پرداخت موفق`, - content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${order.total} تومان با موفقیت انجام شد`, - icon: `/`, - action: { - type: NotifTitleEnum.PAYMENT_SUCCESS, - url: `/`, - }, - }, - }, - recipients: userRecipients, - metadata: { - priority: 1, - }, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for online payment succeed event: ${event.restaurantId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } -} diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts deleted file mode 100644 index a429d7a..0000000 --- a/src/modules/payments/payments.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { PaymentsService } from './services/payments.service'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { PaymentMethod } from './entities/payment-method.entity'; -import { PaymentMethodRepository } from './repositories/payment-method.repository'; -import { PaymentMethodService } from './services/payment-method.service'; -import { Restaurant } from '../restaurants/entities/restaurant.entity'; -import { PaymentsController } from './controllers/payments.controller'; -import { AuthModule } from '../auth/auth.module'; -import { JwtModule } from '@nestjs/jwt'; -import { Payment } from './entities/payment.entity'; -import { ZarinpalGateway } from './gateways/zarinpal.gateway'; -import { GatewayManager } from './gateways/gateway.manager'; -import { PaymentRepository } from './repositories/payment.repository'; -import { InventoryModule } from '../inventory/inventory.module'; -import { PaymentListeners } from './listeners/payment.listeners'; -import { AdminModule } from '../admin/admin.module'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { OrdersModule } from '../orders/orders.module'; - -@Module({ - imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), - AuthModule, JwtModule, InventoryModule, AdminModule, NotificationsModule, - forwardRef(() => OrdersModule)], - controllers: [PaymentsController], - providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, - PaymentRepository, ZarinpalGateway, GatewayManager, PaymentListeners], - exports: [ - PaymentMethodRepository, - PaymentRepository, - PaymentMethodService, - PaymentsService, - // PaymentGatewayService, - ], -}) -export class PaymentsModule { } diff --git a/src/modules/payments/repositories/payment-method.repository.ts b/src/modules/payments/repositories/payment-method.repository.ts deleted file mode 100644 index 22bf7fa..0000000 --- a/src/modules/payments/repositories/payment-method.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { PaymentMethod } from '../entities/payment-method.entity'; - -@Injectable() -export class PaymentMethodRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, PaymentMethod); - } -} diff --git a/src/modules/payments/repositories/payment.repository.ts b/src/modules/payments/repositories/payment.repository.ts deleted file mode 100644 index f157dd3..0000000 --- a/src/modules/payments/repositories/payment.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { Payment } from '../entities/payment.entity'; - -@Injectable() -export class PaymentRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, Payment); - } -} diff --git a/src/modules/payments/services/payment-method.service.ts b/src/modules/payments/services/payment-method.service.ts deleted file mode 100644 index cb40dcf..0000000 --- a/src/modules/payments/services/payment-method.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { PaymentMethod } from '../entities/payment-method.entity'; -import { PaymentMethodRepository } from '../repositories/payment-method.repository'; -import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto'; -import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto'; -import { RequiredEntityData } from '@mikro-orm/core'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; -import { RestMessage, PaymentMessage } from 'src/common/enums/message.enum'; - -@Injectable() -export class PaymentMethodService { - constructor( - private readonly paymentMethodRepository: PaymentMethodRepository, - private readonly em: EntityManager, - ) {} - - async create(restId: string, createPaymentMethodDto: CreatePaymentMethodDto): Promise { - // Check if restaurant exists - const restaurant = await this.em.findOne(Restaurant, { id: restId }); - if (!restaurant) { - throw new NotFoundException(RestMessage.NOT_FOUND); - } - - const paymentMethod = this.paymentMethodRepository.create({ - restaurant, - ...createPaymentMethodDto, - } as unknown as RequiredEntityData); - await this.em.persistAndFlush(paymentMethod); - return paymentMethod; - } - - async findAll(): Promise { - return this.paymentMethodRepository.findAll({ populate: ['restaurant'] }); - } - - async findOne(id: string): Promise { - const paymentMethod = await this.paymentMethodRepository.findOne({ id }, { populate: ['restaurant'] }); - if (!paymentMethod) { - throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND); - } - return paymentMethod; - } - - async findByRestaurant(restaurantId: string): Promise { - return this.paymentMethodRepository.find({ restaurant: { id: restaurantId } }, { populate: ['restaurant'] }); - } - - async update(id: string, updatePaymentMethodDto: UpdatePaymentMethodDto): Promise { - const paymentMethod = await this.findOne(id); - this.em.assign(paymentMethod, updatePaymentMethodDto); - await this.em.flush(); - return paymentMethod; - } - - async remove(id: string): Promise { - const paymentMethod = await this.findOne(id); - await this.em.removeAndFlush(paymentMethod); - return paymentMethod; - } - - -} diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts deleted file mode 100644 index 1733710..0000000 --- a/src/modules/payments/services/payments.service.ts +++ /dev/null @@ -1,442 +0,0 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; -import { Payment } from '../entities/payment.entity'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Order } from '../../orders/entities/order.entity'; -import { Logger } from '@nestjs/common'; -import { GatewayManager } from '../gateways/gateway.manager'; -import { WalletTransaction } from 'src/modules/users/entities/wallet-transaction.entity'; -import { OrderPaymentContext } from '../interface/payment'; -import { OrderStatus } from 'src/modules/orders/interface/order.interface'; -import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto'; -import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events'; -import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet'; - -@Injectable() -export class PaymentsService { - private readonly logger = new Logger(PaymentsService.name); - - constructor( - private readonly em: EntityManager, - private readonly gatewayManager: GatewayManager, - private readonly eventEmitter: EventEmitter2, - ) { } - - async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> { - const ctx = await this.loadAndValidateOrder(orderId); - - // Idempotency: avoid creating/charging again for already-paid orders - if (ctx.order.status === OrderStatus.PAID) { - return { paymentUrl: null }; - } - - switch (ctx.method) { - case PaymentMethodEnum.Cash: - await this.handleCashPayment(ctx); - return { paymentUrl: null }; - - case PaymentMethodEnum.Wallet: - await this.handleWalletPayment(ctx); - return { paymentUrl: null }; - - case PaymentMethodEnum.Online: - return this.handleOnlinePayment(ctx); - - default: - throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD); - } - } - - private async loadAndValidateOrder(orderId: string): Promise { - const order = await this.em.findOne( - Order, - { id: orderId }, - { populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant'] }, - ); - - if (!order) { - throw new NotFoundException(OrderMessage.NOT_FOUND); - } - - if (order.total <= 0) { - throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO); - } - - const pm = order.paymentMethod; - if (!pm) { - throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND); - } - - if (pm.method === PaymentMethodEnum.Online) { - if (!pm.gateway) throw new BadRequestException(PaymentMessage.GATEWAY_REQUIRED); - if (!pm.merchantId) throw new BadRequestException(PaymentMessage.MERCHANT_ID_REQUIRED); - if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED); - } - - return { - order, - amount: order.total, - method: pm.method, - gateway: pm.gateway ?? null, - merchantId: pm.merchantId ?? null, - restaurantDomain: pm.restaurant.domain ?? null, - }; - } - - private async handleCashPayment(ctx: OrderPaymentContext): Promise { - await this.getOrCreateLatestPendingPayment(ctx.order.id, { - amount: ctx.amount, - method: PaymentMethodEnum.Cash, - gateway: null, - }); - } - - // TODO clean this code and refactor it - private async handleWalletPayment(ctx: OrderPaymentContext): Promise { - await this.em.transactional(async em => { - const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] }); - if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND); - - if (order.status === OrderStatus.PAID) { - return; - } - - const walletTransaction = await em.findOne(WalletTransaction, { - user: { id: order.user.id }, - restaurant: { id: order.restaurant.id }, - }, { - orderBy: { createdAt: 'DESC' } - }); - - if (!walletTransaction) { - throw new NotFoundException('User wallet not found'); - } - - if (walletTransaction.balance < ctx.amount) { - throw new BadRequestException('Insufficient wallet balance'); - } - - const newBalance = walletTransaction.balance - ctx.amount; - - - const payment = await this.getOrCreateLatestPendingPayment(order.id, { - em, - amount: ctx.amount, - method: PaymentMethodEnum.Wallet, - gateway: null, - }); - - const newWalletTransaction = em.create(WalletTransaction, { - user: order.user, - restaurant: order.restaurant, - amount: ctx.amount, - type: WalletTransactionType.DEBIT, - reason: WalletTransactionReason.ORDER_PAYMENT, - balance: newBalance, - }); - - payment.status = PaymentStatusEnum.Paid; - payment.paidAt = new Date(); - order.status = OrderStatus.PAID; - - em.persist([ payment, order, newWalletTransaction]); - await em.flush(); - }); - this.eventEmitter.emit( - paymentSucceedEvent.name, - new paymentSucceedEvent(ctx.order.id, ctx.order.restaurant.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount), - ); - } - - private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> { - const gateway = this.gatewayManager.get(ctx.gateway!); - - const payment = await this.getOrCreateLatestPendingPayment(ctx.order.id, { - amount: ctx.amount, - method: PaymentMethodEnum.Online, - gateway: ctx.gateway!, - }); - - // If we already requested a gateway transaction, just return the same URL (idempotent) - if (payment.transactionId) { - return { paymentUrl: gateway.getPaymentUrl(payment.transactionId) }; - } - - const { transactionId } = await gateway.requestPayment({ - amount: ctx.amount, - orderId: ctx.order.id, - merchantId: ctx.merchantId!, - domain: ctx.restaurantDomain!, - }); - - payment.gateway = ctx.gateway; - payment.transactionId = transactionId; - payment.status = PaymentStatusEnum.Pending; - - await this.em.persistAndFlush(payment); - - return { - paymentUrl: gateway.getPaymentUrl(transactionId), - }; - } - - async verifyOnlinePayment(transactionId: string, orderId: string): Promise { - const payment = await this.em.transactional(async em => { - const payment = await em.findOne( - Payment, - { transactionId, order: { id: orderId } }, - { populate: ['order', 'order.paymentMethod'] }, - ); - - if (!payment) { - throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); - } - - if (payment.status === PaymentStatusEnum.Paid) { - return payment; - } - - const pm = payment.order.paymentMethod; - if (!pm?.merchantId || !payment.gateway) { - throw new BadRequestException(PaymentMessage.INVALID_PAYMENT_CONFIGURATION); - } - - const gateway = this.gatewayManager.get(payment.gateway); - - const result = await gateway.verifyPayment({ - merchantId: pm.merchantId, - amount: payment.amount, - transactionId: payment.transactionId!, - }); - - payment.verifyResponse = result.raw; - - if (!result.success) { - this.failPayment(payment); - return payment; - } - - this.markPaid(payment, result.referenceId); - if (payment.order.status === OrderStatus.PENDING_PAYMENT) { - payment.order.status = OrderStatus.PAID; - } - - await em.flush(); - return payment; - }); - this.eventEmitter.emit( - onlinePaymentSucceedEvent.name, - new onlinePaymentSucceedEvent(payment.id, payment.order.id, payment.order.restaurant.id, String(payment.order?.orderNumber) || '', payment.amount), - ); - return payment; - } - - findAllPaymentsByRestaurantId(restId: string, userId: string) { - return this.em.find( - Payment, - { order: { restaurant: { id: restId }, user: { id: userId } } }, - { populate: ['order', 'order.paymentMethod'] }, - ); - } - - async verifyCashPayment(id: string): Promise { - const payment = await this.em.transactional(async em => { - const payment = await em.findOne(Payment, id, { populate: ['order'] }); - if (!payment) { - throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); - } - if (payment.method !== PaymentMethodEnum.Cash) { - throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH); - } - if (payment.status === PaymentStatusEnum.Paid) { - throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID); - } - if (payment.order.status === OrderStatus.PENDING_PAYMENT) { - payment.order.status = OrderStatus.PAID; - } - payment.status = PaymentStatusEnum.Paid; - payment.paidAt = new Date(); - em.persist(payment); - em.persist(payment.order); - await em.flush(); - return payment; - }); - return payment; - } - - private markPaid(payment: Payment, referenceId: string) { - payment.status = PaymentStatusEnum.Paid; - payment.paidAt = new Date(); - payment.referenceId = referenceId; - } - - private failPayment(payment: Payment) { - payment.status = PaymentStatusEnum.Failed; - payment.failedAt = new Date(); - payment.order.status = OrderStatus.CANCELED; - } - - - private async getOrCreateLatestPendingPayment( - orderId: string, - params: { - amount: number; - method: PaymentMethodEnum; - gateway: PaymentGatewayEnum | null; - em?: EntityManager; - }, - ): Promise { - const em = params.em ?? this.em; - - const existing = await em.findOne( - Payment, - { order: { id: orderId }, status: PaymentStatusEnum.Pending }, - { orderBy: { createdAt: 'DESC' } }, - ); - - if (existing) { - if (existing.method !== params.method) { - throw new BadRequestException(PaymentMessage.EXISTING_PENDING_PAYMENT_MISMATCH); - } - - return existing; - } - - const orderRef = em.getReference(Order, orderId); - const payment = em.create(Payment, { - amount: params.amount, - order: orderRef, - method: params.method, - gateway: params.gateway, - transactionId: null, - status: PaymentStatusEnum.Pending, - }); - - em.persist(payment); - await em.flush(); - return payment; - } - - async getChartData(dto: PaymentChartDto, restaurantId: string): Promise> { - const { startDate, endDate, type = ChartPeriodEnum.Daily } = dto; - - const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - const end = endDate ? new Date(endDate) : new Date(); - - const startOfDay = new Date(start); - startOfDay.setHours(0, 0, 0, 0); - - const endOfDay = new Date(end); - endOfDay.setHours(23, 59, 59, 999); - - // 1. Map your Enum to valid Postgres date_trunc units - // 'Daily' is not valid in PG, it must be 'day' - const pgPeriod = { - [ChartPeriodEnum.Daily]: 'day', - [ChartPeriodEnum.Weekly]: 'week', - [ChartPeriodEnum.Monthly]: 'month', - }[type] || 'day'; - - const params: any[] = [startOfDay, endOfDay]; - let restaurantFilter = ''; - - if (restaurantId) { - params.push(restaurantId); - restaurantFilter = `AND o.restaurant_id = ?`; - } - - // MikroORM uses ? placeholders for parameter binding - // Ensure paid_at is not NULL and handle timestamp casting properly - const query = ` - SELECT - TO_CHAR(DATE_TRUNC('${pgPeriod}', CAST(p.paid_at AS timestamp)), 'YYYY-MM-DD') as "date", - COALESCE(SUM(CASE WHEN p.method = '${PaymentMethodEnum.Cash}' THEN p.amount ELSE 0 END), 0)::numeric as cash, - COALESCE(SUM(CASE WHEN p.method = '${PaymentMethodEnum.Online}' THEN p.amount ELSE 0 END), 0)::numeric as online - FROM payments p - INNER JOIN orders o ON p.order_id = o.id - WHERE p.status = '${PaymentStatusEnum.Paid}' - AND p.paid_at IS NOT NULL - AND p.paid_at >= ? - AND p.paid_at <= ? - ${restaurantFilter} - GROUP BY DATE_TRUNC('${pgPeriod}', CAST(p.paid_at AS timestamp)) - ORDER BY DATE_TRUNC('${pgPeriod}', CAST(p.paid_at AS timestamp)) ASC - `; - - this.logger.debug(`Chart query params: startOfDay=${startOfDay.toISOString()}, endOfDay=${endOfDay.toISOString()}, restaurantId=${restaurantId}`); - const result = await this.em.execute(query, params); - this.logger.debug(`Chart query returned ${result.length} rows`); - - const dataMap = new Map(); - result.forEach((row: any) => { - dataMap.set(row.date, { - cash: Number(row.cash), - online: Number(row.online), - }); - }); - - const allDates = this.generateDateRange(startOfDay, endOfDay, type); - - return allDates.map(date => ({ - date, - cash: dataMap.get(date)?.cash ?? 0, - online: dataMap.get(date)?.online ?? 0, - })); - } - - private generateDateRange(start: Date, end: Date, type: ChartPeriodEnum): string[] { - const dates: string[] = []; - const current = new Date(start); - const endDate = new Date(end); - - // Normalize start date based on period type - switch (type) { - case ChartPeriodEnum.Weekly: - // Start of week (Monday) - PostgreSQL DATE_TRUNC('week') uses ISO week (Monday as first day) - const dayOfWeek = current.getDay(); - const diff = current.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); - current.setDate(diff); - current.setHours(0, 0, 0, 0); - // Also normalize end date to include the week containing it - const endDayOfWeek = endDate.getDay(); - const endDiff = endDate.getDate() - endDayOfWeek + (endDayOfWeek === 0 ? -6 : 1); - endDate.setDate(endDiff); - endDate.setHours(23, 59, 59, 999); - break; - case ChartPeriodEnum.Monthly: - // Start of month - current.setDate(1); - current.setHours(0, 0, 0, 0); - // Also normalize end date to include the month containing it - endDate.setDate(1); - endDate.setHours(23, 59, 59, 999); - break; - case ChartPeriodEnum.Daily: - default: - current.setHours(0, 0, 0, 0); - endDate.setHours(23, 59, 59, 999); - } - - while (current <= endDate) { - const dateStr = current.toISOString().split('T')[0]; - dates.push(dateStr); - - // Increment based on period type - switch (type) { - case ChartPeriodEnum.Weekly: - current.setDate(current.getDate() + 7); - break; - case ChartPeriodEnum.Monthly: - current.setMonth(current.getMonth() + 1); - break; - case ChartPeriodEnum.Daily: - default: - current.setDate(current.getDate() + 1); - } - } - - return dates; - } -} diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index c75bfc6..64804cc 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -16,13 +16,10 @@ 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 { Order } from '../../orders/entities/order.entity'; import { Coupon } from '../../coupons/entities/coupon.entity'; import { Category } from '../../foods/entities/category.entity'; import { Food } from '../../foods/entities/food.entity'; import { Delivery } from '../../delivery/entities/delivery.entity'; -import { PaymentMethod } from '../../payments/entities/payment-method.entity'; -import { Pager } from '../../pager/entities/pager.entity'; import { Schedule } from '../entities/schedule.entity'; import { WalletTransaction } from '../../users/entities/wallet-transaction.entity'; import { SmsLog } from '../../notifications/entities/smsLogs.entity'; @@ -234,9 +231,6 @@ export class RestaurantsService { throw new NotFoundException(RestMessage.NOT_FOUND); } - // Delete orders first (will cascade to order items and payments due to cascade settings) - await em.nativeDelete(Order, { restaurant: id }); - // Delete coupons await em.nativeDelete(Coupon, { restaurant: id }); @@ -264,11 +258,6 @@ export class RestaurantsService { // Delete admin roles for this restaurant await em.nativeDelete(AdminRole, { restaurant: id }); - // Delete payment methods - await em.nativeDelete(PaymentMethod, { restaurant: id }); - - // Delete pagers - await em.nativeDelete(Pager, { restaurant: id }); // Delete wallet transactions await em.nativeDelete(WalletTransaction, { restaurant: id }); diff --git a/src/modules/review/controllers/review.controller.ts b/src/modules/review/controllers/review.controller.ts deleted file mode 100644 index 36ad8fc..0000000 --- a/src/modules/review/controllers/review.controller.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common'; -import { ReviewService } from '../providers/review.service'; -import { CreateReviewDto } from '../dto/create-review.dto'; -import { UpdateReviewDto } from '../dto/update-review.dto'; -import { FindReviewsDto, FindRestuarantReviewsDto } from '../dto/find-reviews.dto'; -import { ChangeStatusDto } from '../dto/change-status.dto'; -import { - ApiTags, - ApiOperation, - ApiCreatedResponse, - ApiOkResponse, - ApiQuery, - ApiBody, - ApiParam, - ApiBearerAuth, - ApiHeader, -} from '@nestjs/swagger'; -import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; -import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; -import { UserId } from 'src/common/decorators/user-id.decorator'; -import { RestId } from 'src/common/decorators/rest-id.decorator'; -import { ReviewStatus } from '../enums/review-status.enum'; -import { Permissions } from 'src/common/decorators/permissions.decorator'; -import { Permission } from 'src/common/enums/permission.enum'; -import { API_HEADER_SLUG } from 'src/common/constants'; - -@ApiTags('review') -@Controller() -export class ReviewController { - constructor(private readonly reviewService: ReviewService) {} - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Post('public/reviews') - @ApiOperation({ summary: 'Create a new review and rating' }) - @ApiHeader(API_HEADER_SLUG) - @ApiCreatedResponse({ description: 'The review has been successfully created.' }) - @ApiBody({ type: CreateReviewDto }) - create(@Body() createReviewDto: CreateReviewDto, @UserId() userId: string) { - return this.reviewService.create(userId, createReviewDto); - } - - @Get('public/reviews/:foodId') - @ApiOperation({ summary: 'Get all reviews of food by id(public - only approved)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiOkResponse({ description: 'List of approved reviews' }) - @ApiParam({ name: 'foodId', required: true, type: String }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'orderBy', required: false, type: String }) - @ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] }) - findAll(@Query() dto: FindReviewsDto, @Param('foodId') foodId: string) { - // Only show approved reviews for public endpoint - return this.reviewService.findAll({ ...dto, status: ReviewStatus.APPROVED, foodId }); - } - - @Get('public/reviews/restuarant/:restuarantSlug') - @ApiOperation({ summary: 'Get all reviews of restuarant by slug(public - only approved)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiParam({ name: 'restuarantSlug', required: true, type: String }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'orderBy', required: false, type: String }) - @ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] }) - findRestuarantAllReviews(@Query() dto: FindRestuarantReviewsDto, @Param('restuarantSlug') restuarantSlug: string) { - // Only show approved reviews for public endpoint - return this.reviewService.findRestuarantAllReviews({ ...dto, status: ReviewStatus.APPROVED, restuarantSlug }); - } - - // @Get('public/reviews/:id') - // @ApiOperation({ summary: 'Get a review by id' }) - // @ApiParam({ name: 'id', required: true }) - // @ApiOkResponse({ description: 'The review' }) - // @ApiNotFoundResponse({ description: 'Review not found' }) - // findById(@Param('id') id: string) { - // return this.reviewService.findById(id); - // } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Patch('public/reviews/:reviewId') - @ApiOperation({ summary: 'Update a review (own reviews only)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiParam({ name: 'reviewId', required: true }) - @ApiBody({ type: UpdateReviewDto }) - @ApiOkResponse({ description: 'The updated review' }) - update(@Param('reviewId') reviewId: string, @Body() updateReviewDto: UpdateReviewDto, @UserId() userId: string) { - return this.reviewService.update(reviewId, userId, updateReviewDto, false); - } - - @UseGuards(AuthGuard) - @ApiBearerAuth() - @Delete('public/reviews/:id') - @ApiOperation({ summary: 'Delete a review (own reviews only)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiParam({ name: 'id', required: true }) - remove(@Param('id') id: string, @UserId() userId: string) { - return this.reviewService.remove(id, userId, false); - } - - /******************** Admin Routes **********************/ - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_REVIEWS) - @Get('admin/reviews') - @ApiOperation({ summary: 'Get all reviews (admin - including unapproved)' }) - @ApiOkResponse({ description: 'List of all reviews' }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'foodId', required: false, type: String }) - @ApiQuery({ name: 'userId', required: false, type: String }) - @ApiQuery({ name: 'status', required: false, enum: ReviewStatus }) - @ApiQuery({ name: 'orderBy', required: false, type: String }) - @ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] }) - findAllAdmin(@Query() dto: FindReviewsDto, @RestId() restId: string) { - return this.reviewService.findAll({ ...dto, restId: restId }); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_REVIEWS) - @Patch('admin/reviews/:id') - @ApiOperation({ summary: 'Update a review (admin - can change approval status)' }) - @ApiParam({ name: 'id', required: true }) - @ApiBody({ type: UpdateReviewDto }) - updateAdmin(@Param('id') id: string, @Body() updateReviewDto: UpdateReviewDto, @UserId() userId: string) { - return this.reviewService.update(id, userId, updateReviewDto, true); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_REVIEWS) - @Get('admin/reviews/:id') - @ApiOperation({ summary: 'review detail' }) - @ApiParam({ name: 'id', required: true }) - reviewDetail(@Param('id') reviewId: string, @RestId() restaurantId: string) { - return this.reviewService.findById(reviewId, restaurantId); - } - - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Permissions(Permission.MANAGE_REVIEWS) - @Patch('admin/reviews/:id/status') - @ApiOperation({ summary: 'Change review status (pending, approved, rejected)' }) - @ApiParam({ name: 'id', required: true }) - @ApiBody({ type: ChangeStatusDto }) - changeStatus( - @Param('id') reviewId: string, - @Body() changeStatusDto: ChangeStatusDto, - @RestId() restaurantId: string, - ) { - return this.reviewService.changeStatus(reviewId, changeStatusDto.status, restaurantId); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_REVIEWS) - @ApiBearerAuth() - @Delete('admin/reviews/:id') - @ApiOperation({ summary: 'Delete a review (admin)' }) - @ApiParam({ name: 'id', required: true }) - removeAdmin(@Param('id') id: string, @UserId() userId: string) { - return this.reviewService.remove(id, userId, true); - } -} diff --git a/src/modules/review/dto/change-status.dto.ts b/src/modules/review/dto/change-status.dto.ts deleted file mode 100644 index 3be9bd7..0000000 --- a/src/modules/review/dto/change-status.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsEnum } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { ReviewStatus } from '../enums/review-status.enum'; - -export class ChangeStatusDto { - @IsEnum(ReviewStatus) - @ApiProperty({ - enum: ReviewStatus, - description: 'Review status: pending, approved, or rejected', - example: ReviewStatus.APPROVED, - }) - status: ReviewStatus; -} diff --git a/src/modules/review/dto/create-review.dto.ts b/src/modules/review/dto/create-review.dto.ts deleted file mode 100644 index 0304d0b..0000000 --- a/src/modules/review/dto/create-review.dto.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min, IsArray, IsEnum } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { PositivePoint, NegativePoint } from '../enums/review-point.enum'; - -export class CreateReviewDto { - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Food ID' }) - foodId: string; - - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Order ID' }) - orderId: string; - - @IsNotEmpty() - @IsNumber() - @Min(1) - @Max(5) - @Type(() => Number) - @ApiProperty({ description: 'Rating from 1 to 5', example: 5, minimum: 1, maximum: 5 }) - rating: number; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Comment text', example: 'Very delicious food!' }) - comment?: string; - - @IsOptional() - @IsArray() - @IsEnum(PositivePoint, { each: true }) - @ApiPropertyOptional({ - description: 'Positive points about the food', - example: [PositivePoint.GREAT_TASTE, PositivePoint.FAST_DELIVERY], - enum: PositivePoint, - isArray: true, - }) - positivePoints?: PositivePoint[]; - - @IsOptional() - @IsArray() - @IsEnum(NegativePoint, { each: true }) - @ApiPropertyOptional({ - description: 'Negative points about the food', - example: [NegativePoint.SMALL_PORTION], - enum: NegativePoint, - isArray: true, - }) - negativePoints?: NegativePoint[]; -} diff --git a/src/modules/review/dto/find-reviews.dto.ts b/src/modules/review/dto/find-reviews.dto.ts deleted file mode 100644 index b4a0377..0000000 --- a/src/modules/review/dto/find-reviews.dto.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ReviewStatus } from '../enums/review-status.enum'; - -export class FindReviewsDto { - @IsOptional() - @IsNumber() - @Type(() => Number) - @ApiPropertyOptional({ description: 'Page number', example: 1 }) - page?: number; - - @IsOptional() - @IsNumber() - @Type(() => Number) - @ApiPropertyOptional({ description: 'Items per page', example: 10 }) - limit?: number; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Food ID to filter comments' }) - foodId?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Restaurant ID to filter comments' }) - restId?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'User ID to filter comments' }) - userId?: string; - - @IsOptional() - @IsEnum(ReviewStatus) - @ApiPropertyOptional({ description: 'Filter by review status', enum: ReviewStatus }) - status?: ReviewStatus; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Order by field', example: 'createdAt' }) - orderBy?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Order direction', enum: ['asc', 'desc'], example: 'desc' }) - order?: 'asc' | 'desc'; -} - -export class FindRestuarantReviewsDto extends FindReviewsDto { - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Restaurant slug to filter comments' }) - restuarantSlug?: string; -} diff --git a/src/modules/review/dto/update-review.dto.ts b/src/modules/review/dto/update-review.dto.ts deleted file mode 100644 index a377045..0000000 --- a/src/modules/review/dto/update-review.dto.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IsNumber, IsOptional, IsString, Max, Min, IsArray, IsEnum } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { PositivePoint, NegativePoint } from '../enums/review-point.enum'; -import { ReviewStatus } from '../enums/review-status.enum'; - -export class UpdateReviewDto { - @IsOptional() - @IsNumber() - @Min(1) - @Max(5) - @Type(() => Number) - @ApiPropertyOptional({ description: 'Rating from 1 to 5', minimum: 1, maximum: 5 }) - rating?: number; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ description: 'Comment text' }) - comment?: string; - - @IsOptional() - @IsArray() - @IsEnum(PositivePoint, { each: true }) - @ApiPropertyOptional({ - description: 'Positive points about the food', - enum: PositivePoint, - isArray: true, - }) - positivePoints?: PositivePoint[]; - - @IsOptional() - @IsArray() - @IsEnum(NegativePoint, { each: true }) - @ApiPropertyOptional({ - description: 'Negative points about the food', - enum: NegativePoint, - isArray: true, - }) - negativePoints?: NegativePoint[]; - - @IsOptional() - @IsEnum(ReviewStatus) - @ApiPropertyOptional({ description: 'Review status (admin only)', enum: ReviewStatus }) - status?: ReviewStatus; -} diff --git a/src/modules/review/entities/review.entity.ts b/src/modules/review/entities/review.entity.ts deleted file mode 100644 index aec0853..0000000 --- a/src/modules/review/entities/review.entity.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Entity, Index, ManyToOne, Property, Unique, Enum } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Food } from '../../foods/entities/food.entity'; -import { User } from '../../users/entities/user.entity'; -import { Order } from 'src/modules/orders/entities/order.entity'; -import { PositivePoint, NegativePoint } from '../enums/review-point.enum'; -import { ReviewStatus } from '../enums/review-status.enum'; - -@Entity({ tableName: 'reviews' }) -@Unique({ properties: ['order', 'food', 'user'] }) -@Index({ properties: ['food', 'status'] }) -@Index({ properties: ['user'] }) -@Index({ properties: ['order'] }) -export class Review extends BaseEntity { - @ManyToOne(() => Order) - order: Order; - - @ManyToOne(() => Food) - food: Food; - - @ManyToOne(() => User) - user: User; - - @Property({ type: 'text', nullable: true }) - comment?: string; - - @Property({ type: 'int', nullable: false }) - rating: number = 0; - - @Property({ type: 'json', nullable: true }) - positivePoints?: PositivePoint[]; - - @Property({ type: 'json', nullable: true }) - negativePoints?: NegativePoint[]; - - @Enum(() => ReviewStatus) - @Property({ type: 'string', default: ReviewStatus.PENDING }) - status: ReviewStatus = ReviewStatus.PENDING; -} diff --git a/src/modules/review/enums/review-point.enum.ts b/src/modules/review/enums/review-point.enum.ts deleted file mode 100644 index 05f4058..0000000 --- a/src/modules/review/enums/review-point.enum.ts +++ /dev/null @@ -1,15 +0,0 @@ -export enum PositivePoint { - GREAT_TASTE = 'great_taste', - FAST_DELIVERY = 'fast_delivery', - GOOD_QUALITY = 'good_quality', - GOOD_PORTION_SIZE = 'good_portion_size', - FRIENDLY_SERVICE = 'friendly_service', -} - -export enum NegativePoint { - SMALL_PORTION = 'small_portion', - SLOW_DELIVERY = 'slow_delivery', - POOR_QUALITY = 'poor_quality', - OVERPRICED = 'overpriced', - UNFRIENDLY_SERVICE = 'unfriendly_service', -} diff --git a/src/modules/review/enums/review-status.enum.ts b/src/modules/review/enums/review-status.enum.ts deleted file mode 100644 index b46c121..0000000 --- a/src/modules/review/enums/review-status.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum ReviewStatus { - PENDING = 'pending', - REJECTED = 'rejected', - APPROVED = 'approved', -} diff --git a/src/modules/review/events/review.events.ts b/src/modules/review/events/review.events.ts deleted file mode 100644 index 4d6f08d..0000000 --- a/src/modules/review/events/review.events.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 69c6a40..0000000 --- a/src/modules/review/listeners/review.listeners.ts +++ /dev/null @@ -1,77 +0,0 @@ -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/food-rating-cron.service.ts b/src/modules/review/providers/food-rating-cron.service.ts deleted file mode 100644 index 2053326..0000000 --- a/src/modules/review/providers/food-rating-cron.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { FoodRepository } from '../../foods/repositories/food.repository'; -import { ReviewRepository } from '../repositories/review.repository'; -import { ReviewStatus } from '../enums/review-status.enum'; - -@Injectable() -export class FoodRatingCronService { - private readonly logger = new Logger(FoodRatingCronService.name); - - constructor( - private readonly em: EntityManager, - private readonly foodRepository: FoodRepository, - private readonly reviewRepository: ReviewRepository, - ) { } - - /** - * Cron job that runs at midnight every day (00:00:00 UTC) - * Calculates and updates the average rating for each restaurant food - */ - @Cron('0 0 * * *', { - name: 'calculateFoodRatings', - timeZone: 'UTC', - }) - async handleCron() { - this.logger.log('Starting daily food rating calculation cron job...'); - - try { - // Get all foods from all restaurants - const foods = await this.foodRepository.find({}); - - this.logger.log(`Found ${foods.length} foods to process`); - - let updatedCount = 0; - let errorCount = 0; - - // Process each food item - for (const food of foods) { - try { - // Get all approved reviews for this food that are not deleted - const reviews = await this.reviewRepository.find( - { - food: { id: food.id }, - status: ReviewStatus.APPROVED, - deletedAt: null, - }, - { fields: ['rating'] }, - ); - - // Calculate average rating - let averageRating = 0; - if (reviews.length > 0) { - const sum = reviews.reduce((acc, review) => acc + review.rating, 0); - averageRating = sum / reviews.length; - } - - // Update food score - food.score = parseFloat(averageRating.toFixed(2)); - this.em.persist(food); - - updatedCount++; - } catch (error) { - this.logger.error(`Error processing food ${food.id}: ${error.message}`, error.stack); - errorCount++; - } - } - - // Flush all updates at once for better performance - await this.em.flush(); - - this.logger.log(`Food rating calculation completed. Updated: ${updatedCount}, Errors: ${errorCount}`); - } catch (error) { - this.logger.error(`Error in food rating cron job: ${error.message}`, error.stack); - } - } -} diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts deleted file mode 100644 index f498176..0000000 --- a/src/modules/review/providers/review.service.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { CreateReviewDto } from '../dto/create-review.dto'; -import { UpdateReviewDto } from '../dto/update-review.dto'; -import { FindRestuarantReviewsDto, FindReviewsDto } from '../dto/find-reviews.dto'; -import { ReviewRepository } from '../repositories/review.repository'; -import { FoodRepository } from '../../foods/repositories/food.repository'; -import { UserRepository } from '../../users/repositories/user.repository'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { RequiredEntityData } from '@mikro-orm/core'; -import { Review } from '../entities/review.entity'; -import { FoodMessage, UserMessage, ReviewMessage } from 'src/common/enums/message.enum'; -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'; -import { sanitizeText } from '../../utils/sanitize.util'; - -@Injectable() -export class ReviewService { - constructor( - private readonly reviewRepository: ReviewRepository, - private readonly foodRepository: FoodRepository, - private readonly userRepository: UserRepository, - private readonly em: EntityManager, - private readonly eventEmitter: EventEmitter2, - ) { } - - async create(userId: string, createReviewDto: CreateReviewDto): Promise { - const { foodId, orderId, rating, comment, positivePoints, negativePoints } = createReviewDto; - - const food = await this.foodRepository.findOne({ id: foodId }); - if (!food) { - throw new NotFoundException(FoodMessage.NOT_FOUND); - } - - const user = await this.userRepository.findOne({ id: userId }); - if (!user) { - throw new NotFoundException(UserMessage.USER_NOT_FOUND); - } - - // 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'); - } - - // Verify the food is in the order - const orderItem = await this.em.findOne(OrderItem, { - order: { id: orderId }, - food: { id: foodId }, - }); - if (!orderItem) { - throw new BadRequestException('Food is not in the specified order'); - } - - // Check if user already commented on this food for this order - const existingReview = await this.reviewRepository.findOne({ - order: { id: orderId }, - food: { id: foodId }, - user: { id: userId }, - }); - - if (existingReview) { - throw new BadRequestException(ReviewMessage.ALREADY_COMMENTED); - } - - const data: RequiredEntityData = { - order, - food, - user, - rating, - comment: sanitizeText(comment) || undefined, - positivePoints: positivePoints || undefined, - negativePoints: negativePoints || undefined, - status: ReviewStatus.PENDING, - }; - - const review = this.reviewRepository.create(data); - if (!review) { - throw new BadRequestException('Failed to create review entity'); - } - - await this.em.persistAndFlush(review); - - // 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; - } - - async findAll(dto: FindReviewsDto) { - return this.reviewRepository.findAllPaginated(dto); - } - - async findRestuarantAllReviews(dto: FindRestuarantReviewsDto) { - const { restuarantSlug, ...restDto } = dto; - const restaurant = await this.em.findOne(Restaurant, { slug: restuarantSlug }); - if (!restaurant) { - throw new NotFoundException('RestaurantMessage.NOT_FOUND'); - } - return this.reviewRepository.findAllPaginated({ ...restDto, restId: restaurant.id }); - } - - async findById(id: string, restaurantId: string): Promise { - const review = await this.reviewRepository.findOne( - { id, order: { restaurant: { id: restaurantId } } }, - { populate: ['food', 'user', 'order'] }, - ); - if (!review) { - throw new NotFoundException(ReviewMessage.NOT_FOUND); - } - return review; - } - - async update(id: string, userId: string, dto: UpdateReviewDto, isAdmin: boolean = false): Promise { - const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] }); - if (!review) { - throw new NotFoundException(ReviewMessage.NOT_FOUND); - } - - // Only allow user to update their own reviews (unless admin) - if (!isAdmin && review.user.id !== userId) { - throw new BadRequestException(ReviewMessage.CAN_ONLY_UPDATE_OWN); - } - - // Users can only update rating and comment, not status - if (!isAdmin && dto.status !== undefined) { - throw new BadRequestException('Only admins can change review status'); - } - - const oldRating = review.rating; - - // 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 - if (dto.rating !== undefined && dto.rating !== oldRating) { - await this.updateFoodRating(review.food.id); - } - - return review; - } - - async changeStatus(reviewId: string, status: ReviewStatus, restaurantId: string): Promise { - const review = await this.reviewRepository.findOne({ id: reviewId, order: { restaurant: { id: restaurantId } } }); - if (!review) { - throw new NotFoundException(ReviewMessage.NOT_FOUND); - } - review.status = status; - await this.em.persistAndFlush(review); - return review; - } - - async remove(id: string, userId: string, isAdmin: boolean = false) { - const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] }); - if (!review) { - throw new NotFoundException(ReviewMessage.NOT_FOUND); - } - - // Only allow user to delete their own reviews (unless admin) - if (!isAdmin && review.user.id !== userId) { - throw new BadRequestException(ReviewMessage.CAN_ONLY_DELETE_OWN); - } - - const foodId = review.food.id; - review.deletedAt = new Date(); - await this.em.persistAndFlush(review); - - // Update food rating average - await this.updateFoodRating(foodId); - } - - private async updateFoodRating(foodId: string): Promise { - const reviews = await this.reviewRepository.find( - { food: { id: foodId }, status: ReviewStatus.APPROVED, deletedAt: null }, - { fields: ['rating'] }, - ); - - if (reviews.length === 0) { - const food = await this.foodRepository.findOne({ id: foodId }); - if (food) { - food.score = 0; - await this.em.persistAndFlush(food); - } - return; - } - - const averageRating = reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length; - - const food = await this.foodRepository.findOne({ id: foodId }); - if (food) { - food.score = parseFloat(averageRating.toFixed(2)); - await this.em.persistAndFlush(food); - } - } -} diff --git a/src/modules/review/repositories/review.repository.ts b/src/modules/review/repositories/review.repository.ts deleted file mode 100644 index 5114e1e..0000000 --- a/src/modules/review/repositories/review.repository.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { FilterQuery } from '@mikro-orm/core'; -import { Review } from '../entities/review.entity'; -import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; -import { ReviewStatus } from '../enums/review-status.enum'; - -type FindReviewsOpts = { - page?: number; - limit?: number; - foodId?: string; - userId?: string; - status?: ReviewStatus; - orderBy?: string; - restId?: string; - order?: 'asc' | 'desc'; -}; - -@Injectable() -export class ReviewRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, Review); - } - - /** - * Find reviews with pagination and optional filters. - * Supports: foodId, userId, status, ordering. - */ - async findAllPaginated(opts: FindReviewsOpts = {}): Promise> { - const { page = 1, limit = 10, foodId, restId, userId, status, orderBy = 'createdAt', order = 'desc' } = opts; - - const offset = (page - 1) * limit; - - const where: FilterQuery = {}; - - if (foodId) { - where.food = { id: foodId }; - } - - if (restId) { - where.food = { restaurant: { id: restId } }; - } - - if (userId) { - where.user = { id: userId }; - } - - if (status) { - where.status = status; - } - - const [data, total] = await this.findAndCount(where, { - limit, - offset, - orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['food', 'user'], - }); - - const totalPages = Math.ceil(total / limit); - - return { - data, - meta: { - total, - page, - limit, - totalPages, - }, - }; - } -} diff --git a/src/modules/review/review.module.ts b/src/modules/review/review.module.ts deleted file mode 100644 index c76b69d..0000000 --- a/src/modules/review/review.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ReviewService } from './providers/review.service'; -import { ReviewController } from './controllers/review.controller'; -import { ReviewRepository } from './repositories/review.repository'; -import { FoodRatingCronService } from './providers/food-rating-cron.service'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { Review } from './entities/review.entity'; -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, AdminModule, NotificationsModule], - controllers: [ReviewController], - providers: [ReviewService, ReviewRepository, FoodRatingCronService, ReviewListeners], - exports: [ReviewRepository], -}) -export class ReviewModule {} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 794b103..f0b4614 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,14 +1,12 @@ 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 { Order } from 'src/modules/orders/entities/order.entity'; -import { normalizePhone } from '../../utils/phone.util'; + import { normalizePhone } from '../../utils/phone.util'; @Entity({ tableName: 'users' }) @Index({ properties: ['isActive'] }) export class User extends BaseEntity { - @OneToMany(() => Order, order => order.user) - orders = new Collection(this); + @Property() firstName!: string; diff --git a/src/modules/users/providers/user.service.ts b/src/modules/users/providers/user.service.ts index bbc2972..3f84b94 100644 --- a/src/modules/users/providers/user.service.ts +++ b/src/modules/users/providers/user.service.ts @@ -133,11 +133,11 @@ export class UserService { // 2. Build the 'where' filter query const where: FilterQuery = { - orders: { - restaurant: { - id: restId, - }, - }, + // orders: { + // restaurant: { + // id: restId, + // }, + // }, }; // 4. Add 'search' logic (case-insensitive) @@ -147,12 +147,12 @@ export class UserService { 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 } }] : []), - ]; + // where.$or = [ + // { firstName: { $ilike: searchPattern } }, + // { lastName: { $ilike: searchPattern } }, + // { phone: { $ilike: searchPattern } }, + // ...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []), + // ]; } // 5. Execute the query using findAndCount diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index dc3442a..923aefb 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -4,17 +4,11 @@ import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import { PermissionsSeeder } from './permissions.seeder'; import { RolesSeeder } from './roles.seeder'; import { RestaurantsSeeder } from './restaurants.seeder'; -import { PaymentMethodsSeeder } from './payment-methods.seeder'; -import { DeliveryMethodsSeeder } from './delivery-methods.seeder'; import { SchedulesSeeder } from './schedules.seeder'; -import { CategoriesSeeder } from './categories.seeder'; import { FoodsSeeder } from './foods.seeder'; import { AdminsSeeder } from './admins.seeder'; import { UsersSeeder } from './users.seeder'; -import { CouponsSeeder } from './coupons.seeder'; -import { NotificationPreferencesSeeder } from './notification-preferences.seeder'; import { UserWalletsSeeder } from './user-wallets.seeder'; -import { InventorySeeder } from './inventory.seeder'; // import { NotificationsSeeder } from './notifications.seeder'; export class DatabaseSeeder extends Seeder { @@ -39,54 +33,21 @@ export class DatabaseSeeder extends Seeder { const restaurantsMap = await restaurantsSeeder.run(em); console.info(`[Seeder] Step 3/${TOTAL_STEPS}: Done`); - // 4. Create Payment Methods for all Restaurants - console.info(`[Seeder] Step 4/${TOTAL_STEPS}: Creating payment methods`); - const allRestaurants = await em.find(Restaurant, {}); - const paymentMethodsSeeder = new PaymentMethodsSeeder(); - await paymentMethodsSeeder.run(em, allRestaurants); - console.info(`[Seeder] Step 4/${TOTAL_STEPS}: Done`); - // 5. Create Delivery Methods for all Restaurants - console.info(`[Seeder] Step 5/${TOTAL_STEPS}: Creating delivery methods`); - const deliveryMethodsSeeder = new DeliveryMethodsSeeder(); - await deliveryMethodsSeeder.run(em, allRestaurants); - console.info(`[Seeder] Step 5/${TOTAL_STEPS}: Done`); - - // 6. Create Schedules for all Restaurants - console.info(`[Seeder] Step 6/${TOTAL_STEPS}: Creating schedules`); - const schedulesSeeder = new SchedulesSeeder(); - await schedulesSeeder.run(em, allRestaurants); - console.info(`[Seeder] Step 6/${TOTAL_STEPS}: Done`); - - // 7. Create Notification Preferences for all Restaurants - console.info(`[Seeder] Step 7/${TOTAL_STEPS}: Creating notification preferences`); - const notificationPreferencesSeeder = new NotificationPreferencesSeeder(); - await notificationPreferencesSeeder.run(em, allRestaurants); - console.info(`[Seeder] Step 7/${TOTAL_STEPS}: Done`); // 8. Create Categories - console.info(`[Seeder] Step 8/${TOTAL_STEPS}: Creating categories`); - const categoriesSeeder = new CategoriesSeeder(); - const categoriesMap = await categoriesSeeder.run(em, restaurantsMap); - console.info(`[Seeder] Step 8/${TOTAL_STEPS}: Done`); + // 9. Create Foods - console.info(`[Seeder] Step 9/${TOTAL_STEPS}: Creating foods`); - const foodsSeeder = new FoodsSeeder(); - await foodsSeeder.run(em, restaurantsMap, categoriesMap); - console.info(`[Seeder] Step 9/${TOTAL_STEPS}: Done`); + // console.info(`[Seeder] Step 9/${TOTAL_STEPS}: Creating foods`); + // const foodsSeeder = new FoodsSeeder(); + // await foodsSeeder.run(em, restaurantsMap, categoriesMap); + // console.info(`[Seeder] Step 9/${TOTAL_STEPS}: Done`); // 10. Create Inventory for all Foods - console.info(`[Seeder] Step 10/${TOTAL_STEPS}: Creating inventory`); - const inventorySeeder = new InventorySeeder(); - await inventorySeeder.run(em); - console.info(`[Seeder] Step 10/${TOTAL_STEPS}: Done`); + - // 11. Create Coupons - console.info(`[Seeder] Step 11/${TOTAL_STEPS}: Creating coupons`); - const couponsSeeder = new CouponsSeeder(); - await couponsSeeder.run(em, restaurantsMap); - console.info(`[Seeder] Step 11/${TOTAL_STEPS}: Done`); + // 12. Create Admins console.info(`[Seeder] Step 12/${TOTAL_STEPS}: Creating admins`); diff --git a/src/seeders/categories.seeder.ts b/src/seeders/categories.seeder.ts deleted file mode 100644 index 53d3a32..0000000 --- a/src/seeders/categories.seeder.ts +++ /dev/null @@ -1,395 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { Category } from '../modules/foods/entities/category.entity'; -import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; -import { categoriesData } from './data/categories.data'; -import * as fs from 'fs'; -import * as path from 'path'; - -interface IconData { - index: number; - name: string; - url: string; -} - -export class CategoriesSeeder { - private icons: IconData[] = []; - private iconIndex = 0; - - private loadIcons(): void { - if (this.icons.length > 0) return; - - const iconsPath = path.join(process.cwd(), 'dump', 'named_icons.csv'); - const csvContent = fs.readFileSync(iconsPath, 'utf-8'); - const lines = csvContent.split('\n').slice(1); // Skip header - - this.icons = lines - .filter(line => line.trim()) - .map(line => { - const parts = line.split(','); - return { - index: parseInt(parts[0].replace(/"/g, '')), - name: parts[1].replace(/"/g, ''), - url: parts[2].replace(/"/g, '') - }; - }); - } - - private getIconForCategory(categoryTitle: string): string | undefined { - this.loadIcons(); - - if (this.icons.length === 0) return undefined; - - // Create a mapping of keywords to icon names - const iconMapping: Record = { - // Pizza - 'پیتزا': ['pizza'], - - // Pasta - 'پاستا': ['fast-food'], - - // Steak - 'استیک': ['meat', 'meat 2'], - - // Burger - 'برگر': ['hanburger'], - - // Salad - 'سالاد': ['broccoli'], - - // Appetizer/Appetizers - 'پیش غذا': ['chicken', 'fast-food'], - - // Main Course - 'غذای اصلی': ['hanburger', 'meat'], - 'خوراک': ['hanburger', 'meat'], - 'ناهار': ['hanburger', 'meat'], - - // Sandwich - 'ساندویچ': ['bread'], - - // Fried foods - 'سوخاری': ['chicken', 'fast-food'], - - // Cake/Dessert - 'کیک': ['cookie1', 'cookie2'], - 'دسر': ['cookie1', 'cookie2'], - - // Ice Cream - 'بستنی': ['ice cream', 'ice cream 3'], - 'آیس': ['ice cream'], - - // Breakfast - 'صبحانه': ['bread', 'egg'], - - // Drinks - general - 'نوشیدنی': ['drink', 'drink2', 'bottle1', 'bottle2'], - - // Coffee - 'قهوه': ['drink', 'drink2'], - 'اسپرسو': ['drink'], - 'کافی': ['drink'], - - // Tea - 'چای': ['drink', 'drink2'], - 'دمنوش': ['drink', 'drink2'], - - // Cocktail/Mocktail - 'ماکتل': ['wine glass', 'wine glass 2'], - 'موهیتو': ['wine glass'], - - // Shake/Smoothie - 'شیک': ['ice cream'], - 'اسموتی': ['ice cream'], - - // Juice - 'آبمیوه': ['juice', 'juice2', 'juice3'], - 'ویتامینه': ['juice', 'juice2', 'juice3'], - - // Hot Dog - 'هات داگ': ['hot dog'], - - // BBQ - 'باربیکیو': ['barbecue', 'barbeque2'], - - // BBQ Skewers - 'کباب': ['skewer'], - - // Soup - 'آش': ['jar', 'dish'], - - // Bread - 'نان': ['bread'], - - // Donut - 'دونات': ['donuts'], - - // Fish - 'دریایی': ['fish'], - 'ماهی': ['fish'], - - // Chicken - 'مرغ': ['chicken'], - - // Egg - 'تخم': ['egg', 'fried egg'], - - // Vegetable - 'سبزی': ['broccoli'], - - // Fruit - 'میوه': ['strawberry', 'cherry', 'grape', 'watermelon'], - - // Milk - 'شیر': ['bottle1'], // Note: there's no milk icon, using a generic one - - // Cookie - 'کوکی': ['cookie1', 'cookie2'], - - // French Fries - 'سیب زمینی': ['french fries'], - - // Fast Food - 'فست فود': ['fast-food'], - - // Bakery - 'نانوایی': ['bread'], - - // Ice Tea - 'آیس تی': ['drink', 'drink2'], - - // Hot Pot - 'حلیم': ['jar', 'dish'], - - // Taco - 'تاکو': ['taco'], - - // Nachos - 'ناچو': ['fast-food'], - - // Sushi - 'سوشی': ['fish'], - - // Dumpling - 'غالوش': ['basket'], - - // Tempura - 'تمپورا': ['fish'], - - // Tofu - 'توفو': ['jar'], - - // Bibimbap - 'بیبیمباب': ['fast-food'], - - // Boba - 'بوبا': ['drink', 'drink2'], - - // Fish and Chips - 'فیش اند چیپس': ['fish', 'french fries'], - - // Focaccia - 'فوکاچیا': ['bread'], - - // Martini - 'مارتینی': ['wine glass'], - - // Mochi - 'موچی': ['donuts'], - - // Pancake - 'پن کیک': ['bread'], - - // Quesadilla - 'کوئزادیا': ['quesadilla'], - - // Origami - 'اوريگامي': ['basket'], - - // Pudding - 'پودینگ': ['jar'], - - // Sweet - 'شیرین': ['candy'], - - // Crossant - 'کروسان': ['bread'], - - // Bio - 'بیو': ['broccoli'], - - // Lemon - 'لیمو': ['lemon'], - - // Candy - 'شیرینی': ['candy'], - - // Lollipop - 'آب نبات': ['lollipop'], - - // Cherry - 'گیلاس': ['cherry'], - - // Strawberry - 'توت فرنگی': ['strawberry'], - - // Watermelon - 'هندوانه': ['watermelon'], - - // Avocado - 'آووکادو': ['avocado'], - - // Banana - 'موز': ['banana'], - - // Carrot - 'هویج': ['carrot'], - - // Cheese - 'پنیر': ['cheese', 'cheese2'], - - // Tomato - 'گوجه': ['tomato'], // Note: no tomato icon - - // Onion - 'پیاز': ['onion'], - - // Olive - 'زیتون': ['olive'], - - // Nuts - 'آجیل': ['nuts'], - - // Scales (for weighing) - 'ترازو': ['scales'], - - // Whisk - 'همزن': ['whisk', 'whisk'], - - // Mortar - 'هاون': ['mortar'], - - // Rolling pin - 'والت': ['rolling pin'], - - // Knife - 'چاقو': ['knife'], - - // Baby food - 'کودک': ['baby bottle', 'baby bottle', 'baby pacifier'], - - // Hat (chef hat) - 'کلاه': ['hat robe', 'hat robe 2'], - - - - // Candle (birthday) - 'شمع': ['candle'], - - // Fire - 'آتش': ['fire'], - - // Refrigerator - 'یخچال': ['refrigerator'], - - // Kitchen stove - 'اجاق': ['kitchen stove'], - - // Mixer - 'میکسر': ['mixer'], - - // Jar - 'شیشه': ['jar'], - - // Jelly - 'ژله': ['jelly'], - - // Soap - 'صابون': ['soap'], - - // Dish - 'ظرف': ['dish'], - - // Basket - 'سبد': ['basket'], - - // Salt - 'نمک': ['solt'], - - // Eggplant - 'بادمجان': ['egg plant'], - - // Wine - 'شراب': ['wine glass', 'wine glass 2', 'wine glass 3'], - - // Plate - 'بشقاب': ['dish'], - - // Fork and knife - 'چنگال': ['spoon and fork 1', 'spoon and fork 2', 'spoon and knife', 'fork and knife'], - - // Toast - 'تست': ['toast'] - }; - - // Find matching icon based on category title - for (const [keyword, iconNames] of Object.entries(iconMapping)) { - if (categoryTitle.includes(keyword)) { - // Find the first available icon from the list - for (const iconName of iconNames) { - const icon = this.icons.find(i => i.name.toLowerCase() === iconName.toLowerCase()); - if (icon) { - return icon.url; - } - } - } - } - - // Fallback: try to find any icon that might be related by partial name match - for (const icon of this.icons) { - const lowerTitle = categoryTitle.toLowerCase(); - const lowerIconName = icon.name.toLowerCase(); - - // Check if any word in the category title matches part of the icon name - const titleWords = lowerTitle.split(/\s+/); - for (const word of titleWords) { - if (word.length > 2 && lowerIconName.includes(word)) { - return icon.url; - } - } - } - - // Final fallback: return a random icon - const randomIndex = Math.floor(Math.random() * this.icons.length); - return this.icons[randomIndex]?.url; - } - async run(em: EntityManager, restaurantsMap: Map): Promise> { - const categoriesMap = new Map(); - - for (const catData of categoriesData) { - const restaurant = restaurantsMap.get(catData.restaurantSlug); - if (!restaurant) continue; - - const existing = await em.findOne(Category, { - title: catData.title, - restaurant, - }); - - if (!existing) { - const avatarUrl = this.getIconForCategory(catData.title); - const category = em.create(Category, { - title: catData.title, - restaurant, - isActive: true, - avatarUrl, - }); - em.persist(category); - categoriesMap.set(`${catData.restaurantSlug}-${catData.title}`, category); - } else { - categoriesMap.set(`${catData.restaurantSlug}-${catData.title}`, existing); - } - } - - await em.flush(); - return categoriesMap; - } -} diff --git a/src/seeders/coupons.seeder.ts b/src/seeders/coupons.seeder.ts deleted file mode 100644 index 024d01b..0000000 --- a/src/seeders/coupons.seeder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { Coupon } from '../modules/coupons/entities/coupon.entity'; -import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; -import { couponsData } from './data/coupons.data'; - -export class CouponsSeeder { - async run(em: EntityManager, restaurantsMap: Map): Promise { - for (const couponData of couponsData) { - const restaurant = restaurantsMap.get(couponData.restaurantSlug); - - if (!restaurant) continue; - - const existing = await em.findOne(Coupon, { - code: couponData.code, - restaurant: { id: restaurant.id }, - }); - - if (!existing) { - const coupon = em.create(Coupon, { - restaurant, - code: couponData.code, - name: couponData.name, - description: couponData.description, - type: couponData.type, - value: couponData.value, - maxDiscount: couponData.maxDiscount, - minOrderAmount: couponData.minOrderAmount, - maxUses: couponData.maxUses, - maxUsesPerUser: couponData.maxUsesPerUser, - startDate: couponData.startDate ? new Date(couponData.startDate) : undefined, - endDate: couponData.endDate ? new Date(couponData.endDate) : undefined, - isActive: couponData.isActive, - usedCount: 0, - }); - - em.persist(coupon); - } - } - - await em.flush(); - } -} diff --git a/src/seeders/data/payment-methods.data.ts b/src/seeders/data/payment-methods.data.ts deleted file mode 100644 index 673a1fc..0000000 --- a/src/seeders/data/payment-methods.data.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PaymentMethodEnum, PaymentGatewayEnum } from '../../modules/payments/interface/payment'; - -export interface PaymentMethodData { - method: PaymentMethodEnum; - description: string; - enabled: boolean; - order: number; - gateway: PaymentGatewayEnum | null; - merchantId?: string; -} - -export const paymentMethodsData: PaymentMethodData[] = [ - { - method: PaymentMethodEnum.Online, - description: 'درگاه پرداخت زرین‌پال', - enabled: true, - order: 1, - gateway: PaymentGatewayEnum.ZarinPal, - merchantId: 'b6f55bd0-6eae-4045-aeb8-07d084fa8f73', - }, - { - method: PaymentMethodEnum.Cash, - description: 'پرداخت نقدی', - enabled: true, - order: 2, - gateway: null, - }, - { - method: PaymentMethodEnum.Wallet, - description: 'پرداخت از کیف پول', - enabled: true, - order: 3, - gateway: null, - }, -]; diff --git a/src/seeders/inventory.seeder.ts b/src/seeders/inventory.seeder.ts deleted file mode 100644 index 716715e..0000000 --- a/src/seeders/inventory.seeder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { Food } from '../modules/foods/entities/food.entity'; -import { Inventory } from '../modules/inventory/entities/inventory.entity'; - -export class InventorySeeder { - async run(em: EntityManager): Promise { - const foods = await em.find(Food, {}); - - for (const food of foods) { - // Check if inventory already exists for this food - const existingInventory = await em.findOne(Inventory, { - food: { id: food.id }, - }); - - if (!existingInventory) { - const inventory = em.create(Inventory, { - food, - totalStock: 50, - availableStock: 50, - }); - - em.persist(inventory); - } - } - - await em.flush(); - } -} - diff --git a/src/seeders/payment-methods.seeder.ts b/src/seeders/payment-methods.seeder.ts deleted file mode 100644 index 1765ce0..0000000 --- a/src/seeders/payment-methods.seeder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { EntityManager } from '@mikro-orm/core'; -import { PaymentMethod } from '../modules/payments/entities/payment-method.entity'; -import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; -import { paymentMethodsData } from './data/payment-methods.data'; - -export class PaymentMethodsSeeder { - async run(em: EntityManager, restaurants: Restaurant[]): Promise { - for (const restaurant of restaurants) { - if (!restaurant) continue; - - for (const methodData of paymentMethodsData) { - const existing = await em.findOne(PaymentMethod, { - restaurant: { id: restaurant.id }, - method: methodData.method, - }); - - if (!existing) { - const paymentMethod = em.create(PaymentMethod, { - restaurant, - method: methodData.method, - description: methodData.description, - enabled: methodData.enabled, - order: methodData.order, - gateway: methodData.gateway, - merchantId: methodData.merchantId, - }); - em.persist(paymentMethod); - } else { - // Update existing payment method if needed - if ( - existing.description !== methodData.description || - existing.enabled !== methodData.enabled || - existing.order !== methodData.order - ) { - existing.description = methodData.description; - existing.enabled = methodData.enabled; - existing.order = methodData.order; - if (methodData.gateway !== undefined) { - existing.gateway = methodData.gateway; - } - if (methodData.merchantId !== undefined) { - existing.merchantId = methodData.merchantId; - } - em.persist(existing); - } - } - } - } - - await em.flush(); - } -}