From 079cd7628c081840367e9f15fa42c26778bbd554 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Mon, 17 Mar 2025 22:41:12 -0400 Subject: [PATCH 1/5] add createProductImage --- src/products/dto/create-product.dto.ts | 18 +++++++++++++- src/products/products.controller.ts | 32 ++++++++++++++++++++++++- src/products/products.module.ts | 10 +++++++- src/products/products.service.ts | 33 +++++++++++++++++++++++++- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/products/dto/create-product.dto.ts b/src/products/dto/create-product.dto.ts index 226ea1f..51a4d61 100644 --- a/src/products/dto/create-product.dto.ts +++ b/src/products/dto/create-product.dto.ts @@ -1,19 +1,35 @@ -import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsInt, IsOptional, IsString, IsUUID } from 'class-validator'; export class CreateProductDTO { + @ApiProperty() @IsString() name: string; + @ApiProperty() @IsString() genericName: string; + @ApiProperty() @IsOptional() @IsString() description?: string; + @ApiProperty() @IsInt() priority: number; + @ApiProperty() @IsUUID() manufacturer: string; + + @ApiProperty() + @IsArray() + @IsString({ each: true }) + categoryIds: string[]; + + @ApiProperty() + @IsArray() + @IsString({ each: true }) + imageUrls: string[]; } diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 8d29bee..7614c62 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -12,9 +12,13 @@ import { } from '@nestjs/common'; import { ProductsService } from './products.service'; import { + ApiBearerAuth, + ApiBody, + ApiCreatedResponse, ApiExtraModels, ApiOkResponse, ApiOperation, + ApiUnauthorizedResponse, getSchemaPath, } from '@nestjs/swagger'; import { ProductPresentationDTO } from './dto/find-products.dto'; @@ -71,6 +75,17 @@ export class ProductsController { @Post() @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Create a new product', + description: 'Only ADMIN and BRANCH_ADMIN can create a product.', + }) + @ApiBody({ type: CreateProductDTO }) + @ApiCreatedResponse({ + description: 'Product successfully created.', + type: Product, + }) + @ApiUnauthorizedResponse({ description: 'User is not authorized.' }) async createProduct( @Body() createProductDto: CreateProductDTO, @Req() request: CustomRequest, @@ -83,6 +98,21 @@ export class ProductsController { createProductDto.manufacturer, ); - return this.productsServices.createProduct(createProductDto, manufacturer); + const categories = await this.productsServices.findCategories( + createProductDto.categoryIds, + ); + + const newProduct = await this.productsServices.createProduct( + createProductDto, + manufacturer, + categories, + ); + + await this.productsServices.createProductImage( + newProduct, + createProductDto.imageUrls, + ); + + return newProduct; } } diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 9891bda..dc6404e 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -6,10 +6,18 @@ import { Product } from './entities/product.entity'; import { ProductPresentation } from './entities/product-presentation.entity'; import { Manufacturer } from './entities/manufacturer.entity'; import { AuthModule } from 'src/auth/auth.module'; +import { Category } from './entities/category.entity'; +import { ProductImage } from './entities/product-image.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Product, ProductPresentation, Manufacturer]), + TypeOrmModule.forFeature([ + Product, + ProductPresentation, + Manufacturer, + Category, + ProductImage, + ]), AuthModule, ], controllers: [ProductsController], diff --git a/src/products/products.service.ts b/src/products/products.service.ts index e7050d7..bd66b73 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -1,10 +1,12 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Product } from './entities/product.entity'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { ProductPresentation } from './entities/product-presentation.entity'; import { CreateProductDTO } from './dto/create-product.dto'; import { Manufacturer } from './entities/manufacturer.entity'; +import { Category } from './entities/category.entity'; +import { ProductImage } from './entities/product-image.entity'; @Injectable() export class ProductsService { @@ -17,6 +19,12 @@ export class ProductsService { @InjectRepository(Manufacturer) private manufacturerRepository: Repository, + + @InjectRepository(Category) + private categoryRepository: Repository, + + @InjectRepository(ProductImage) + private productImageRepository: Repository, ) {} async countProducts(): Promise { @@ -61,16 +69,39 @@ export class ProductsService { return manufacturer; } + async findCategories(ids: string[]): Promise { + const categories = await this.categoryRepository.findBy({ + id: In(ids), + }); + + if (categories.length !== ids.length) { + throw new NotFoundException('One or more categories not found'); + } + + return categories; + } + async createProduct( createProductDto: CreateProductDTO, manufacturer: Manufacturer, + categories: Category[], ): Promise { const newProduct = this.productRepository.create({ ...createProductDto, manufacturer, + categories, }); const savedProduct = await this.productRepository.save(newProduct); return savedProduct; } + + async createProductImage(product: Product, images: string[]): Promise { + if (images.length) { + const productImages = images.map((url) => + this.productImageRepository.create({ url, product }), + ); + await this.productImageRepository.save(productImages); + } + } } From edc1eff107326df8156f9cece1048583cbf0839b Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Mon, 17 Mar 2025 23:49:59 -0400 Subject: [PATCH 2/5] categories, images and presentations as optional --- src/products/dto/create-product.dto.ts | 26 ++++++++++++++++++++++++-- src/products/products.controller.ts | 10 ++++++---- src/products/products.service.ts | 14 ++++++++------ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/products/dto/create-product.dto.ts b/src/products/dto/create-product.dto.ts index 51a4d61..777a5f6 100644 --- a/src/products/dto/create-product.dto.ts +++ b/src/products/dto/create-product.dto.ts @@ -1,6 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsInt, IsOptional, IsString, IsUUID } from 'class-validator'; +export class CreateProductPresentationDTO { + @ApiProperty() + presentationId: string; + + @ApiProperty() + price: number; +} + export class CreateProductDTO { @ApiProperty() @IsString() @@ -23,13 +31,27 @@ export class CreateProductDTO { @IsUUID() manufacturer: string; - @ApiProperty() + @ApiProperty({ + required: false, + }) @IsArray() + @IsOptional() @IsString({ each: true }) categoryIds: string[]; - @ApiProperty() + @ApiProperty({ + required: false, + }) @IsArray() + @IsOptional() @IsString({ each: true }) imageUrls: string[]; + + @ApiProperty({ + type: [CreateProductPresentationDTO], + required: false, + }) + @IsArray() + @IsOptional() + presentations: CreateProductPresentationDTO[]; } diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 7614c62..7d0225e 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -108,10 +108,12 @@ export class ProductsController { categories, ); - await this.productsServices.createProductImage( - newProduct, - createProductDto.imageUrls, - ); + if (createProductDto.imageUrls) { + await this.productsServices.createProductImage( + newProduct, + createProductDto.imageUrls, + ); + } return newProduct; } diff --git a/src/products/products.service.ts b/src/products/products.service.ts index bd66b73..216f913 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -70,6 +70,10 @@ export class ProductsService { } async findCategories(ids: string[]): Promise { + if (!ids || ids.length === 0) { + return []; + } + const categories = await this.categoryRepository.findBy({ id: In(ids), }); @@ -97,11 +101,9 @@ export class ProductsService { } async createProductImage(product: Product, images: string[]): Promise { - if (images.length) { - const productImages = images.map((url) => - this.productImageRepository.create({ url, product }), - ); - await this.productImageRepository.save(productImages); - } + const productImages = images.map((url) => + this.productImageRepository.create({ url, product }), + ); + await this.productImageRepository.save(productImages); } } From fcb41253c2f796f57ff118f7a1c1ab72ff048e49 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Wed, 19 Mar 2025 15:08:26 -0400 Subject: [PATCH 3/5] implement roles decorator --- src/products/products.controller.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 7d0225e..02cd196 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -7,7 +7,6 @@ import { Post, Query, Req, - UnauthorizedException, UseGuards, } from '@nestjs/common'; import { ProductsService } from './products.service'; @@ -28,8 +27,10 @@ import { getPaginationUrl } from 'src/utils/pagination-urls'; import { PaginationDTO } from 'src/utils/dto/pagination.dto'; import { CreateProductDTO } from './dto/create-product.dto'; import { Product } from './entities/product.entity'; -import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; -import { UserRole } from 'src/user/entities/user.entity'; +import { Role } from 'src/auth/rol.enum'; +import { Roles } from 'src/auth/roles.decorador'; +import { AuthGuard } from 'src/auth/auth.guard'; +import { RolesGuard } from 'src/auth/roles.guard'; @Controller('product') @ApiExtraModels(PaginationDTO, ProductPresentationDTO) @@ -74,7 +75,8 @@ export class ProductsController { } @Post() - @UseGuards(AuthGuard) + @UseGuards(AuthGuard, RolesGuard) + @Roles(Role.ADMIN, Role.BRANCH_ADMIN) @ApiBearerAuth() @ApiOperation({ summary: 'Create a new product', @@ -88,12 +90,7 @@ export class ProductsController { @ApiUnauthorizedResponse({ description: 'User is not authorized.' }) async createProduct( @Body() createProductDto: CreateProductDTO, - @Req() request: CustomRequest, ): Promise { - if (![UserRole.ADMIN, UserRole.BRANCH_ADMIN].includes(request.user.role)) { - throw new UnauthorizedException(); - } - const manufacturer = await this.productsServices.findManufacturer( createProductDto.manufacturer, ); From 64a5a23e81e088c988ccb6396d6cbbb3dc098488 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Fri, 21 Mar 2025 17:55:26 -0400 Subject: [PATCH 4/5] refactor add categories to product --- src/products/products.controller.ts | 18 ++++++++++++------ src/products/products.service.ts | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 02cd196..37dbaa9 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -95,23 +95,29 @@ export class ProductsController { createProductDto.manufacturer, ); - const categories = await this.productsServices.findCategories( - createProductDto.categoryIds, - ); - const newProduct = await this.productsServices.createProduct( createProductDto, manufacturer, - categories, ); - if (createProductDto.imageUrls) { + if (createProductDto.imageUrls && createProductDto.imageUrls.length) { await this.productsServices.createProductImage( newProduct, createProductDto.imageUrls, ); } + if (createProductDto.categoryIds && createProductDto.categoryIds.length) { + const categories = await this.productsServices.findCategories( + createProductDto.categoryIds, + ); + + await this.productsServices.addCategoriesToProduct( + newProduct, + categories, + ); + } + return newProduct; } } diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 216f913..2a1b414 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -88,12 +88,10 @@ export class ProductsService { async createProduct( createProductDto: CreateProductDTO, manufacturer: Manufacturer, - categories: Category[], ): Promise { const newProduct = this.productRepository.create({ ...createProductDto, manufacturer, - categories, }); const savedProduct = await this.productRepository.save(newProduct); @@ -106,4 +104,21 @@ export class ProductsService { ); await this.productImageRepository.save(productImages); } + + async addCategoriesToProduct( + product: Product, + categoriesToAdd: Category[], + ): Promise { + if (categoriesToAdd.length === 0) { + return; + } + + if (!product.categories) { + product.categories = []; + } + + product.categories = [...product.categories, ...categoriesToAdd]; + + await this.productRepository.save(product); + } } From aaf242fb2b765cec7b8c89057437305c2ad9b7c1 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Fri, 21 Mar 2025 20:37:19 -0400 Subject: [PATCH 5/5] add productPresentation --- src/products/products.controller.ts | 14 ++++++++ src/products/products.module.ts | 2 ++ src/products/products.service.ts | 55 ++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 37dbaa9..33ac1d6 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -118,6 +118,20 @@ export class ProductsController { ); } + if ( + createProductDto.presentations && + createProductDto.presentations.length + ) { + const ids = createProductDto.presentations.map((p) => p.presentationId); + const presentations = await this.productsServices.findPresentations(ids); + + await this.productsServices.addPresentationsToProduct( + newProduct, + presentations, + createProductDto.presentations, + ); + } + return newProduct; } } diff --git a/src/products/products.module.ts b/src/products/products.module.ts index dc6404e..d01900a 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -8,12 +8,14 @@ import { Manufacturer } from './entities/manufacturer.entity'; import { AuthModule } from 'src/auth/auth.module'; import { Category } from './entities/category.entity'; import { ProductImage } from './entities/product-image.entity'; +import { Presentation } from './entities/presentation.entity'; @Module({ imports: [ TypeOrmModule.forFeature([ Product, ProductPresentation, + Presentation, Manufacturer, Category, ProductImage, diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 2a1b414..af78ee8 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -3,10 +3,14 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Product } from './entities/product.entity'; import { In, Repository } from 'typeorm'; import { ProductPresentation } from './entities/product-presentation.entity'; -import { CreateProductDTO } from './dto/create-product.dto'; +import { + CreateProductDTO, + CreateProductPresentationDTO, +} from './dto/create-product.dto'; import { Manufacturer } from './entities/manufacturer.entity'; import { Category } from './entities/category.entity'; import { ProductImage } from './entities/product-image.entity'; +import { Presentation } from './entities/presentation.entity'; @Injectable() export class ProductsService { @@ -14,6 +18,9 @@ export class ProductsService { @InjectRepository(ProductPresentation) private productPresentationRepository: Repository, + @InjectRepository(Presentation) + private PresentationRepository: Repository, + @InjectRepository(Product) private productRepository: Repository, @@ -121,4 +128,50 @@ export class ProductsService { await this.productRepository.save(product); } + + async findPresentations(ids: string[]): Promise { + if (!ids || ids.length === 0) { + return []; + } + + const presentations = await this.PresentationRepository.findBy({ + id: In(ids), + }); + + if (presentations.length !== ids.length) { + throw new NotFoundException('One or more presentations not found'); + } + + return presentations; + } + + async addPresentationsToProduct( + product: Product, + presentations: Presentation[], + productPresentationDTOs: CreateProductPresentationDTO[], + ): Promise { + if (productPresentationDTOs.length === 0) { + return; + } + + const productPresentations = productPresentationDTOs.map((dto) => { + const presentation = presentations.find( + (p) => p.id === dto.presentationId, + ); + + if (!presentation) { + throw new NotFoundException( + `Presentation with ID ${dto.presentationId} not found`, + ); + } + + return this.productPresentationRepository.create({ + product, + presentation, + price: dto.price, + }); + }); + + await this.productPresentationRepository.save(productPresentations); + } }