diff --git a/src/modules/request/dto/find-requests.dto.ts b/src/modules/request/dto/find-requests.dto.ts new file mode 100644 index 0000000..dbf64ad --- /dev/null +++ b/src/modules/request/dto/find-requests.dto.ts @@ -0,0 +1,73 @@ +import { + IsOptional, + IsString, + IsNumber, + Min, + IsIn, + IsEnum, + IsDateString, + IsArray, +} from 'class-validator'; +import { Type, Transform } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { RequestStatusEnum } from '../enum/request'; + +const sortOrderOptions = ['asc', 'desc'] as const; +type SortOrder = (typeof sortOrderOptions)[number]; + +export class FindRequestsDto { + @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; + + @ApiPropertyOptional({ + description: 'Filter by request statuses', + enum: RequestStatusEnum, + isArray: true, + }) + @IsOptional() + @Transform(({ value }) => (Array.isArray(value) ? value : value?.split(','))) + @IsArray() + @IsEnum(RequestStatusEnum, { each: true }) + statuses?: RequestStatusEnum[]; + + @ApiPropertyOptional({ + description: 'Search by request number or user info', + }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ format: 'date-time' }) + @IsOptional() + @IsDateString() + from?: string; + + @ApiPropertyOptional({ format: 'date-time' }) + @IsOptional() + @IsDateString() + to?: 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/request/repositories/request.repository.ts b/src/modules/request/repositories/request.repository.ts new file mode 100644 index 0000000..e51f3f8 --- /dev/null +++ b/src/modules/request/repositories/request.repository.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { FilterQuery } from '@mikro-orm/core'; +import { Request } from '../entities/request.entity'; +import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; +import { FindRequestsDto } from '../dto/find-requests.dto'; +import { RequestStatusEnum } from '../enum/request'; + +class FindRequestsOpts extends FindRequestsDto { + userId?: string; +} + +@Injectable() +export class RequestRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, Request); + } + + async findAllPaginated(opts: FindRequestsOpts): Promise> { + const { + page = 1, + limit = 10, + statuses, + search, + orderBy = 'createdAt', + order = 'desc', + userId, + from, + to, + } = opts; + + const offset = (page - 1) * limit; + + const where: FilterQuery = {}; + + if (userId) { + where.user = { id: userId }; + } + + if (statuses) { + const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses]; + const validStatuses = normalizedStatuses.filter((s): s is RequestStatusEnum => !!s); + + if (validStatuses.length > 0) { + if (validStatuses.length === 1) { + where.status = validStatuses[0]; + } else { + where.status = { $in: validStatuses }; + } + } + } + + if (from || to) { + where.createdAt = {}; + if (from) { + where.createdAt.$gte = new Date(from); + } + if (to) { + where.createdAt.$lte = new Date(to); + } + } + + if (search) { + const searchPattern = `%${search}%`; + const searchConditions: FilterQuery[] = [ + { user: { firstName: { $ilike: searchPattern } } }, + { user: { lastName: { $ilike: searchPattern } } }, + { user: { phone: { $ilike: searchPattern } } } as FilterQuery, + ]; + + const numericSearch = Number(search); + if (!isNaN(numericSearch) && isFinite(numericSearch)) { + searchConditions.push({ requestNumber: numericSearch } as FilterQuery); + } + + where.$or = searchConditions; + } + + const [data, total] = await this.findAndCount(where, { + limit, + offset, + orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, + populate: ['items', 'items.product', 'user'], + }); + + const totalPages = Math.ceil(total / limit); + + return { + data, + meta: { + total, + page, + limit, + totalPages, + }, + }; + } +} diff --git a/src/modules/request/request.controller.ts b/src/modules/request/request.controller.ts index ce85899..7a3bee5 100644 --- a/src/modules/request/request.controller.ts +++ b/src/modules/request/request.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common'; import { RequestService } from './request.service'; import { CreateRequestDto } from './dto/create-request.dto'; import { UpdateRequestDto } from './dto/update-request.dto'; import { AuthGuard } from '../auth/guards/auth.guard'; import { UserId } from 'src/common/decorators'; +import { FindRequestsDto } from './dto/find-requests.dto'; @Controller() export class RequestController { @@ -17,25 +18,25 @@ export class RequestController { @Get('public/request') @UseGuards(AuthGuard) - findAll() { - return this.requestService.findAll(); + findAll(@Query() dto: FindRequestsDto, @UserId() userId: string) { + return this.requestService.findAll(userId, dto); } - @Get('public/request:id') + @Get('public/request/:id') @UseGuards(AuthGuard) - findOne(@Param('id') id: string) { - return this.requestService.findOne(+id); + findOne(@Param('id') id: string, @UserId() userId: string) { + return this.requestService.findOne(id, userId); } - @Patch('public/request:id') + @Patch('public/request/:id') @UseGuards(AuthGuard) - update(@Param('id') id: string, @Body() updateRequestDto: UpdateRequestDto) { - return this.requestService.update(+id, updateRequestDto); + update(@Param('id') id: string, @Body() updateRequestDto: UpdateRequestDto, @UserId() userId: string) { + return this.requestService.update(id, updateRequestDto, userId); } - @Delete('public/request:id') + @Delete('public/request/:id') @UseGuards(AuthGuard) - remove(@Param('id') id: string) { - return this.requestService.remove(+id); + remove(@Param('id') id: string, @UserId() userId: string) { + return this.requestService.remove(id, userId); } } diff --git a/src/modules/request/request.module.ts b/src/modules/request/request.module.ts index d764812..8a37898 100644 --- a/src/modules/request/request.module.ts +++ b/src/modules/request/request.module.ts @@ -4,6 +4,7 @@ import { RequestService } from './request.service'; import { RequestController } from './request.controller'; import { Request } from './entities/request.entity'; import { RequestItem } from './entities/request-item.entity'; +import { RequestRepository } from './repositories/request.repository'; import { UserModule } from '../user/user.module'; import { ProductModule } from '../product/product.module'; @@ -14,6 +15,6 @@ import { ProductModule } from '../product/product.module'; ProductModule, ], controllers: [RequestController], - providers: [RequestService], + providers: [RequestService, RequestRepository], }) export class RequestModule {} diff --git a/src/modules/request/request.service.ts b/src/modules/request/request.service.ts index f59e82e..a5ff5d6 100644 --- a/src/modules/request/request.service.ts +++ b/src/modules/request/request.service.ts @@ -1,17 +1,20 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; import { CreateRequestDto } from './dto/create-request.dto'; import { UpdateRequestDto } from './dto/update-request.dto'; +import { FindRequestsDto } from './dto/find-requests.dto'; import { Request } from './entities/request.entity'; import { RequestItem } from './entities/request-item.entity'; import { RequestStatusEnum } from './enum/request'; import { UserService } from '../user/providers/user.service'; import { ProductService } from '../product/providers/product.service'; +import { RequestRepository } from './repositories/request.repository'; @Injectable() export class RequestService { constructor( private readonly em: EntityManager, + private readonly requestRepository: RequestRepository, private readonly userService: UserService, private readonly productService: ProductService, ) {} @@ -46,19 +49,69 @@ export class RequestService { }); } - findAll() { - return `This action returns all request`; + findAll(userId: string, dto: FindRequestsDto) { + return this.requestRepository.findAllPaginated({ userId, ...dto }); } - findOne(id: number) { - return `This action returns a #${id} request`; + async findOne(id: string, userId: string) { + const request = await this.requestRepository.findOne( + { id, user: { id: userId } }, + { populate: ['items', 'items.product', 'user'] }, + ); + if (!request) { + throw new NotFoundException('Request not found'); + } + return request; } - update(id: number, updateRequestDto: UpdateRequestDto) { - return `This action updates a #${id} request`; + async update(id: string, updateRequestDto: UpdateRequestDto, userId: string) { + const request = await this.requestRepository.findOne( + { id, user: { id: userId } }, + { populate: ['items', 'items.product', 'user'] }, + ); + if (!request) { + throw new NotFoundException('Request not found'); + } + + const { items } = updateRequestDto; + if (items?.length) { + return this.em.transactional(async (em) => { + request.items.removeAll(); + await em.flush(); + + for (const item of items) { + const product = await this.productService.findOneOrFail(item.productId); + const requestItem = em.create(RequestItem, { + request, + product, + quantity: item.quantity, + attributes: item.attributes, + description: item.description, + attachments: item.attachments, + }); + request.items.add(requestItem); + } + + await em.flush(); + return request; + }); + } + + return request; } - remove(id: number) { - return `This action removes a #${id} request`; + async remove(id: string, userId: string) { + const request = await this.requestRepository.findOne( + { id, user: { id: userId } }, + ); + if (!request) { + throw new NotFoundException('Request not found'); + } + if (request.status !== RequestStatusEnum.PENDING) { + throw new BadRequestException('Only pending requests can be deleted'); + } + request.deletedAt = new Date(); + await this.em.flush(); + return request; } }