diff --git a/src/discount/controllers/coupon.controller.ts b/src/discount/controllers/coupon.controller.ts index 039929e..b09b0bd 100644 --- a/src/discount/controllers/coupon.controller.ts +++ b/src/discount/controllers/coupon.controller.ts @@ -18,6 +18,8 @@ import { UpdateCouponDTO, ResponseCouponDTO, CouponQueryDTO, + CouponListDeleteDTO, + CouponListUpdateDTO, } from '../dto/coupon.dto'; import { AuthGuard } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; @@ -115,6 +117,36 @@ export class CouponController { return { data, total }; } + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('bulk') + @ApiOperation({ summary: 'Bulk delete coupons' }) + @ApiResponse({ + description: 'Successful bulk deletion of coupons', + status: HttpStatus.NO_CONTENT, + }) + async bulkDelete( + @Body() couponListDeleteDTO: CouponListDeleteDTO, + ): Promise { + await this.couponService.bulkDelete(couponListDeleteDTO.ids); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Patch('bulk') + @ApiOperation({ summary: 'Bulk update coupons' }) + @ApiResponse({ + description: 'Successful bulk update of coupons', + status: HttpStatus.NO_CONTENT, + }) + async bulkUpdate( + @Body() couponListUpdateDTO: CouponListUpdateDTO, + ): Promise { + await this.couponService.bulkUpdate( + couponListUpdateDTO.ids, + couponListUpdateDTO.maxUses, + couponListUpdateDTO.expirationDate, + ); + } + @Get(':code') @ApiOperation({ summary: 'Get coupon by code' }) @ApiResponse({ diff --git a/src/discount/controllers/promo.controller.ts b/src/discount/controllers/promo.controller.ts index cd45416..9cde9a1 100644 --- a/src/discount/controllers/promo.controller.ts +++ b/src/discount/controllers/promo.controller.ts @@ -29,6 +29,8 @@ import { UpdatePromoDTO, ResponsePromoDTO, PromoQueryDTO, + PromoListDeleteDTO, + PromoListUpdateDTO, } from '../dto/promo.dto'; import { PromoService } from '../services/promo.service'; import { UserRole } from 'src/user/entities/user.entity'; @@ -115,6 +117,35 @@ export class PromoController { return { data, total }; } + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('bulk') + @ApiOperation({ summary: 'Bulk delete promos' }) + @ApiResponse({ + description: 'Successful bulk deletion of promos', + status: HttpStatus.NO_CONTENT, + }) + async bulkDelete( + @Body() promoListDeleteDTO: PromoListDeleteDTO, + ): Promise { + await this.promoService.bulkDelete(promoListDeleteDTO.ids); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Patch('bulk') + @ApiOperation({ summary: 'Bulk update promos' }) + @ApiResponse({ + description: 'Successful bulk update of promos', + status: HttpStatus.NO_CONTENT, + }) + async bulkUpdate( + @Body() promoListUpdateDTO: PromoListUpdateDTO, + ): Promise { + await this.promoService.bulkUpdate( + promoListUpdateDTO.ids, + promoListUpdateDTO.expiredAt, + ); + } + @Get(':id') @ApiOperation({ summary: 'Get promo by ID' }) @ApiResponse({ diff --git a/src/discount/dto/coupon.dto.ts b/src/discount/dto/coupon.dto.ts index 0454d38..5f0048a 100644 --- a/src/discount/dto/coupon.dto.ts +++ b/src/discount/dto/coupon.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, PartialType, IntersectionType } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; +import { Transform, Expose } from 'class-transformer'; import { IsNotEmpty, IsString, @@ -8,6 +8,7 @@ import { Min, IsDateString, IsOptional, + IsUUID, } from 'class-validator'; import { BaseDTO } from 'src/utils/dto/base.dto'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; @@ -73,3 +74,33 @@ export class CouponQueryDTO extends PaginationQueryDTO { this.expirationBetween = expirationBetween ? expirationBetween : []; } } + +export class CouponListDeleteDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of coupon ids to be deleted', + type: [String], + }) + ids: string[]; +} + +export class CouponListUpdateDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of coupon ids to be updated', + type: [String], + }) + ids: string[]; + + @Expose() + @IsOptional() + @ApiProperty({ description: 'The new expiration date of the coupons' }) + expirationDate: Date; + + @Expose() + @ApiProperty({ description: 'Maximum number of coupon uses' }) + @IsOptional() + @IsInt() + @Min(0) + maxUses: number; +} diff --git a/src/discount/dto/promo.dto.ts b/src/discount/dto/promo.dto.ts index f32c012..6e1bdbc 100644 --- a/src/discount/dto/promo.dto.ts +++ b/src/discount/dto/promo.dto.ts @@ -6,6 +6,7 @@ import { IsString, IsDateString, IsOptional, + IsUUID, } from 'class-validator'; import { BaseDTO } from 'src/utils/dto/base.dto'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; @@ -58,3 +59,25 @@ export class PromoQueryDTO extends PaginationQueryDTO { this.expirationBetween = expirationBetween ? expirationBetween : []; } } + +export class PromoListDeleteDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of promo ids to be deleted', + type: [String], + }) + ids: string[]; +} + +export class PromoListUpdateDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of promo ids to be updated', + type: [String], + }) + ids: string[]; + + @IsNotEmpty() + @ApiProperty({ description: 'The new expiration date of the promos' }) + expiredAt: Date; +} diff --git a/src/discount/services/coupon.service.ts b/src/discount/services/coupon.service.ts index 1b6536b..79cdf70 100644 --- a/src/discount/services/coupon.service.ts +++ b/src/discount/services/coupon.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, IsNull, Repository } from 'typeorm'; import { Coupon } from '../entities/coupon.entity'; import { CouponDTO, UpdateCouponDTO } from '../dto/coupon.dto'; @@ -76,4 +76,29 @@ export class CouponService { } return true; } + + async bulkDelete(ids: string[]) { + const coupons = await this.couponRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (coupons.length === 0) { + throw new NotFoundException(`No coupons found with the given IDs`); + } + await this.couponRepository.softDelete({ id: In(ids) }); + } + + async bulkUpdate(ids: string[], maxUses?: number, expirationDate?: Date) { + const coupons = await this.couponRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (coupons.length === 0) { + throw new NotFoundException(`No coupons found with the given IDs`); + } + const couponsToUpdate = coupons.map((coupon) => { + return { ...coupon, maxUses, expirationDate }; + }); + await this.couponRepository.save(couponsToUpdate); + } } diff --git a/src/discount/services/promo.service.ts b/src/discount/services/promo.service.ts index edbea56..b07b655 100644 --- a/src/discount/services/promo.service.ts +++ b/src/discount/services/promo.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Repository } from 'typeorm'; +import { In, IsNull, Repository } from 'typeorm'; import { PromoDTO, UpdatePromoDTO } from '../dto/promo.dto'; import { Promo } from '../entities/promo.entity'; @@ -69,4 +69,29 @@ export class PromoService { }); return result.affected === 1; } + + async bulkDelete(ids: string[]) { + const promos = await this.promoRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (promos.length === 0) { + throw new NotFoundException(`No promos found with the given IDs`); + } + await this.promoRepository.softDelete({ id: In(ids) }); + } + + async bulkUpdate(ids: string[], expiredAt: Date) { + const promos = await this.promoRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (promos.length === 0) { + throw new NotFoundException(`No promos found with the given IDs`); + } + await this.promoRepository.update( + { id: In(ids) }, + { expiredAt, updatedAt: new Date() }, + ); + } } diff --git a/src/order/controllers/order.controller.ts b/src/order/controllers/order.controller.ts index e6094b8..3aefbd3 100644 --- a/src/order/controllers/order.controller.ts +++ b/src/order/controllers/order.controller.ts @@ -17,6 +17,7 @@ import { import { OrderService } from '../order.service'; import { CreateOrderDTO, + OrderListUpdateDTO, OrderQueryDTO, ResponseOrderDetailedDTO, ResponseOrderDTO, @@ -162,6 +163,22 @@ export class OrderController { }; } + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update orders' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Orders updated successfully', + }) + async bulkUpdate(@Body() updateOrderDto: OrderListUpdateDTO): Promise { + await this.orderService.bulkUpdate( + updateOrderDto.orders, + updateOrderDto.status, + ); + } + @Get(':id') @UseGuards(AuthGuard) @ApiBearerAuth() diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index 1096e04..eb5eb5e 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -3,6 +3,7 @@ import { IsArray, IsEnum, IsInt, + IsNotEmpty, IsOptional, IsPositive, IsString, @@ -213,6 +214,16 @@ export class UpdateOrderStatusWsDTO { status: OrderStatus; } +export class OrderListUpdateDTO { + @ArrayNotEmpty() + @IsUUID(undefined, { each: true }) + orders: string[]; + + @IsNotEmpty() + @IsEnum(OrderStatus) + status: OrderStatus; +} + export class SalesReportDTO { @Expose() @ApiProperty({ description: 'ID of the order' }) diff --git a/src/order/order.service.ts b/src/order/order.service.ts index f15e652..bc41728 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -7,7 +7,7 @@ import { CreateOrderDTO, SalesReportDTO } from './dto/order'; import { User, UserRole } from 'src/user/entities/user.entity'; import { ProductPresentationService } from 'src/products/services/product-presentation.service'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { Order, OrderDetail, @@ -276,6 +276,22 @@ export class OrderService { return await this.orderRepository.save(order); } + async bulkUpdate(ordersIds: string[], status: OrderStatus) { + const orders = await this.orderRepository.findBy({ + id: In(ordersIds), + }); + if (orders.length === 0) { + throw new NotFoundException('No orders found'); + } + const updatedOrders = orders.map((order) => { + if (order.status !== OrderStatus.COMPLETED) { + order.status = status; + return order; + } else return order; + }); + return await this.orderRepository.save(updatedOrders); + } + async findAllOD( user: User, page: number, diff --git a/src/products/dto/product-presentation.dto.ts b/src/products/dto/product-presentation.dto.ts index 43b01fb..0d54193 100644 --- a/src/products/dto/product-presentation.dto.ts +++ b/src/products/dto/product-presentation.dto.ts @@ -93,3 +93,16 @@ export class ResponseOrderProductPresentationDetailDTO extends IntersectionType( @ApiProperty({ type: ResponsePromoDTO }) promo: ResponsePromoDTO; } + +export class ProductPresentationListUpdateDTO { + @ApiProperty({ description: 'The IDs of the product presentation' }) + @IsUUID(undefined, { each: true }) + ids: string[]; + + @ApiProperty({ + description: 'Indicates if the product presentation is visible', + default: true, + }) + @IsBoolean() + isVisible: boolean; +} diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 819a971..a3978ac 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -1,6 +1,9 @@ import { + Body, Controller, Get, + HttpStatus, + Patch, Query, Req, UseGuards, @@ -8,10 +11,12 @@ import { } from '@nestjs/common'; import { ProductsService } from './products.service'; import { + ApiBearerAuth, ApiExtraModels, ApiOkResponse, ApiOperation, ApiQuery, + ApiResponse, getSchemaPath, } from '@nestjs/swagger'; import { ProductPresentationDTO, ProductQueryDTO } from './dto/product.dto'; @@ -20,6 +25,11 @@ import { PaginationInterceptor } from 'src/utils/pagination.interceptor'; import { plainToInstance } from 'class-transformer'; import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { RecommendationService } from 'src/recommendation/recommendation.service'; +import { RolesGuard } from 'src/auth/roles.guard'; +import { Roles } from 'src/auth/roles.decorador'; +import { UserRole } from 'src/user/entities/user.entity'; +import { ProductPresentationListUpdateDTO } from './dto/product-presentation.dto'; +import { ProductPresentationService } from './services/product-presentation.service'; @Controller('product') @ApiExtraModels(PaginationDTO, ProductPresentationDTO) @@ -27,6 +37,7 @@ export class ProductsController { constructor( private productsServices: ProductsService, private recommendationService: RecommendationService, + private productPresentationService: ProductPresentationService, ) {} @Get() @UseInterceptors(PaginationInterceptor) @@ -187,4 +198,22 @@ export class ProductsController { total: products.total, }; } + + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('presentation/bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update orders' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Orders updated successfully', + }) + async bulkUpdate( + @Body() updateProductPresentationDto: ProductPresentationListUpdateDTO, + ): Promise { + await this.productPresentationService.bulkUpdate( + updateProductPresentationDto.ids, + updateProductPresentationDto.isVisible, + ); + } } diff --git a/src/products/services/product-presentation.service.ts b/src/products/services/product-presentation.service.ts index 1921bee..4af0ba7 100644 --- a/src/products/services/product-presentation.service.ts +++ b/src/products/services/product-presentation.service.ts @@ -115,6 +115,22 @@ export class ProductPresentationService { return await this.repository.save(updatedProductPresentation); } + async bulkUpdate(productPresentationIds: string[], isVisible: boolean) { + const productPresentations = await this.repository.findBy({ + id: In(productPresentationIds), + }); + if (productPresentations.length === 0) { + throw new NotFoundException('No product presentations found'); + } + const updatedProductPresentations = productPresentations.map( + (productPresentation) => { + productPresentation.isVisible = isVisible; + return productPresentation; + }, + ); + return await this.repository.save(updatedProductPresentations); + } + async remove(productId: string, presentationId: string): Promise { const productPresentation = await this.findOneProductPresentation( productId, diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index fbf5c83..d24ca4f 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -18,6 +18,7 @@ import { IsString, IsUUID, IsBoolean, + ArrayNotEmpty, } from 'class-validator'; import { UserGender } from '../entities/profile.entity'; import { IsOlderThan } from 'src/utils/is-older-than-validator'; @@ -102,6 +103,27 @@ export class BaseUserDTO { export class UserDTO extends IntersectionType(BaseUserDTO, PasswordDTO) {} +export class UserListUpdateDTO { + @ApiProperty({ + description: 'List of user IDs to be updated', + type: [String], + }) + @ArrayNotEmpty() + @IsUUID(undefined, { each: true }) + users: string[]; + + @Expose() + @IsOptional() + @ApiProperty({ description: 'If the user has validated the email' }) + isValidated: boolean; + + @Expose() + @IsOptional() + @IsEnum(UserRole) + @ApiProperty({ description: 'Role of the user', enum: UserRole }) + role: UserRole; +} + export class UserAdminDTO extends BaseUserDTO { @ApiProperty({ description: 'the role of the user' }) @IsNotEmpty() diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 8d7ee8b..ea9e56f 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -36,6 +36,7 @@ import { UserAdminDTO, UpdateUserDTO, UserMotoDTO, + UserListUpdateDTO, } from './dto/user.dto'; import { PaginationDTO, UserQueryDTO } from 'src/utils/dto/pagination.dto'; import { plainToInstance } from 'class-transformer'; @@ -75,6 +76,23 @@ export class UserController { await this.userService.validateEmail(userOtp); } + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update users' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Users updated successfully', + }) + async bulkUpdate(@Body() updateUserDto: UserListUpdateDTO): Promise { + await this.userService.bulkUpdate( + updateUserDto.users, + updateUserDto.isValidated, + updateUserDto.role, + ); + } + @HttpCode(HttpStatus.OK) @Get(':userId') @UseGuards(AuthGuard, UserOrAdminGuard) diff --git a/src/user/user.service.ts b/src/user/user.service.ts index a2849ed..b24d5c2 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -5,7 +5,7 @@ import { BadRequestException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { UserAdminDTO, UserDTO, UpdateUserDTO } from './dto/user.dto'; import { User, UserRole } from './entities/user.entity'; import { UserOTP } from './entities/user-otp.entity'; @@ -412,4 +412,21 @@ export class UserService { } await this.userRepository.update(user.id, { wsId: '' }); } + + async bulkUpdate( + userIds: string[], + isValidated?: boolean, + UserRole?: UserRole, + ): Promise { + const users = await this.userRepository.findBy({ id: In(userIds) }); + if (!users.length) { + throw new NotFoundException('No users found'); + } + + const updatedUsers = users.map((user) => { + return { ...user, isValidated, role: UserRole }; + }); + + return await this.userRepository.save(updatedUsers); + } }