From 437932900191674e47077367d09000d28c8c2cc9 Mon Sep 17 00:00:00 2001 From: Biglens Date: Sat, 1 Mar 2025 12:28:16 +0100 Subject: [PATCH 1/5] fix: @skipAuth() decorator was removed. it was initially added by me just for endpoint test --- src/database/seeding/seeding.service.ts | 59 +++++++++++++++---- src/entities/base.entity.ts | 2 +- .../entities/product-category.entity.ts | 3 + .../entities/product-variant.entity.ts | 3 + .../products/entities/product.entity.ts | 13 ++-- src/modules/products/products.controller.ts | 1 + src/modules/products/products.module.ts | 4 +- src/modules/products/products.service.ts | 54 +++++++++++------ .../tests/mocks/deleted-product.mock.ts | 3 +- .../tests/mocks/product-category.mock.ts | 11 ++++ .../tests/mocks/product-request-dto.mock.ts | 1 + .../products/tests/mocks/product.mock.ts | 4 +- 12 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 src/modules/products/tests/mocks/product-category.mock.ts diff --git a/src/database/seeding/seeding.service.ts b/src/database/seeding/seeding.service.ts index ef06e7cef..2d7f6eef4 100644 --- a/src/database/seeding/seeding.service.ts +++ b/src/database/seeding/seeding.service.ts @@ -226,38 +226,71 @@ export class SeedingService { await categoryRepository.save([c1, c2, c3]); // Create products with associated categories + const category1 = await categoryRepository.findOne({ + where: { name: 'electricity' }, + }); + + if (!category1) { + throw new BadRequestException(`Invalid category: electricity`); + } + const p1 = productRepository.create({ name: 'Product 1', description: 'Description for Product 1', size: ProductSizeType.STANDARD, - category: 'electricity', - quantity: 1, - price: 500, + category: category1, + quantity: 2, + price: 50, org: or1, }); + const category2 = await categoryRepository.findOne({ + where: { name: 'electronics' }, + }); + + if (!category2) { + throw new BadRequestException(`Invalid category: electronics`); + } + const p2 = productRepository.create({ name: 'Product 2', description: 'Description for Product 2', - size: ProductSizeType.LARGE, - category: 'electricity', - quantity: 2, - price: 50, + size: ProductSizeType.STANDARD, + category: category2, + quantity: 5, + price: 100, org: or2, }); + + const category3 = await categoryRepository.findOne({ + where: { name: 'electricity' }, + }); + + if (!category3) { + throw new BadRequestException(`Invalid category: electricity`); + } + const p3 = productRepository.create({ - name: 'Product 2', - description: 'Description for Product 2', + name: 'Product 3', + description: 'Description for Product 3', size: ProductSizeType.STANDARD, - category: 'electricity', + category: category3, quantity: 2, price: 50, org: or1, }); + const category4 = await categoryRepository.findOne({ + where: { name: 'electricity' }, + }); + + if (!category4) { + throw new BadRequestException(`Invalid category: electricity`); + } + const p4 = productRepository.create({ - name: 'Product 2', - description: 'Description for Product 2', + name: 'Product 4', + description: 'Description for Product 4', size: ProductSizeType.SMALL, - category: 'clothing', + category: category4, quantity: 2, price: 50, org: or2, diff --git a/src/entities/base.entity.ts b/src/entities/base.entity.ts index 051a5389c..bfda6e314 100644 --- a/src/entities/base.entity.ts +++ b/src/entities/base.entity.ts @@ -14,4 +14,4 @@ export class AbstractBaseEntity { @ApiProperty() @UpdateDateColumn({ name: 'updated_at' }) updated_at: Date; -} +} \ No newline at end of file diff --git a/src/modules/product-category/entities/product-category.entity.ts b/src/modules/product-category/entities/product-category.entity.ts index 033975221..4ad33abfe 100644 --- a/src/modules/product-category/entities/product-category.entity.ts +++ b/src/modules/product-category/entities/product-category.entity.ts @@ -12,4 +12,7 @@ export class ProductCategory extends AbstractBaseEntity { @ApiProperty() @Column({ type: 'text', nullable: true }) description: string; + + @OneToMany(() => Product, product => product.category) + products: Product[]; } diff --git a/src/modules/products/entities/product-variant.entity.ts b/src/modules/products/entities/product-variant.entity.ts index f3613af23..5b5fbf184 100644 --- a/src/modules/products/entities/product-variant.entity.ts +++ b/src/modules/products/entities/product-variant.entity.ts @@ -5,4 +5,7 @@ import { Product } from './product.entity'; export class ProductVariant { @PrimaryGeneratedColumn('uuid') id: string; + + @ManyToOne(() => Product, product => product.variants, { onDelete: 'CASCADE' }) + product: Product; } diff --git a/src/modules/products/entities/product.entity.ts b/src/modules/products/entities/product.entity.ts index 945aca9a6..a29223fc2 100644 --- a/src/modules/products/entities/product.entity.ts +++ b/src/modules/products/entities/product.entity.ts @@ -4,6 +4,8 @@ import { Comment } from '../../../modules/comments/entities/comments.entity'; import { Organisation } from '../../../modules/organisations/entities/organisations.entity'; import { Cart } from '../../dashboard/entities/cart.entity'; import { OrderItem } from '../../dashboard/entities/order-items.entity'; +import { ProductVariant } from '../../products/entities/product-variant.entity' +import { ProductCategory } from '../../../modules/product-category/entities/product-category.entity'; export enum StockStatusType { IN_STOCK = 'in stock', @@ -25,9 +27,6 @@ export class Product extends AbstractBaseEntity { @Column({ type: 'text', nullable: true }) description: string; - @Column({ type: 'text', nullable: true }) - category: string; - @Column({ type: 'text', nullable: true }) image: string; @@ -68,4 +67,10 @@ export class Product extends AbstractBaseEntity { @OneToMany(() => Cart, cart => cart.product) cart: Cart[]; -} + + @OneToMany(() => ProductVariant, variant => variant.product, { cascade: true }) + variants?: ProductVariant[]; + + @ManyToOne(() => ProductCategory, (category) => category.products, { nullable: false }) + category: ProductCategory; +} \ No newline at end of file diff --git a/src/modules/products/products.controller.ts b/src/modules/products/products.controller.ts index 7bbcbe895..4f0a4846b 100644 --- a/src/modules/products/products.controller.ts +++ b/src/modules/products/products.controller.ts @@ -70,6 +70,7 @@ export class ProductsController { return await this.productsService.getTotalProducts(); } + @ApiBearerAuth() @UseGuards(OwnershipGuard) @Post('organisations/:orgId/products') diff --git a/src/modules/products/products.module.ts b/src/modules/products/products.module.ts index 441eca43c..6996489a4 100644 --- a/src/modules/products/products.module.ts +++ b/src/modules/products/products.module.ts @@ -13,11 +13,12 @@ import { Role } from '../role/entities/role.entity'; import { Product } from './entities/product.entity'; import { ProductsController } from './products.controller'; import { ProductsService } from './products.service'; +import { ProductCategory } from '../product-category/entities/product-category.entity'; @Module({ imports: [ TypeOrmModule.forFeature([ - Product, + Product, Organisation, Organisation, ProductVariant, User, @@ -27,6 +28,7 @@ import { ProductsService } from './products.service'; Order, OrderItem, Cart, + ProductCategory ]), UserModule, ], diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index 380791dc0..68f94b271 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, ForbiddenException, HttpStatus, Injectable, @@ -19,6 +20,7 @@ import { CreateProductRequestDto } from './dto/create-product.dto'; import { UpdateProductDTO } from './dto/update-product.dto'; import { ProductVariant } from './entities/product-variant.entity'; import { Product, ProductSizeType, StockStatusType } from './entities/product.entity'; +import { ProductCategory } from '../product-category/entities/product-category.entity'; interface SearchCriteria { name?: string; @@ -34,7 +36,8 @@ export class ProductsService { @InjectRepository(Product) private productRepository: Repository, @InjectRepository(Organisation) private organisationRepository: Repository, @InjectRepository(Comment) private commentRepository: Repository, - @InjectRepository(User) private userRepository: Repository + @InjectRepository(User) private userRepository: Repository, + @InjectRepository(ProductCategory) private categoryRepository: Repository ) {} async createProduct(id: string, dto: CreateProductRequestDto) { @@ -55,33 +58,49 @@ export class ProductsService { size: dto.size as ProductSizeType, }; - const newProduct: Product = this.productRepository.create(payload); + const category = await this.categoryRepository.findOne({ + where: { id: payload.category }, + }); + + if (!category) { + throw new BadRequestException(`Invalid category ID: ${payload.category}`); + } + + + const newProduct: Product = this.productRepository.create({ + ...payload, + category, + }); + newProduct.org = org; const statusCal = await this.calculateProductStatus(dto.quantity); newProduct.stock_status = statusCal; newProduct.cost_price = 0.2 * dto.price - dto.price; + const product = await this.productRepository.save(newProduct); - if (!product || !newProduct) + + if (!product || !newProduct) { throw new InternalServerErrorException({ status_code: 500, status: 'Internal server error', message: 'An unexpected error occurred. Please try again later.', }); + } - return { - status: 'success', - message: 'Product created successfully', - data: { - id: product.id, - name: product.name, - description: product.description, - price: product.price, - status: product.stock_status, - quantity: product.quantity, - created_at: product.created_at, - updated_at: product.updated_at, - }, - }; + return { + status: 'success', + message: 'Product created successfully', + data: { + id: product.id, + name: product.name, + description: product.description, + price: product.price, + status: product.stock_status, + quantity: product.quantity, + created_at: product.created_at, + updated_at: product.updated_at, + }, + }; } async getAllProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { @@ -198,6 +217,7 @@ export class ProductsService { try { await this.productRepository.update(productId, { ...updateProductDto, + category: updateProductDto.category ? {id: updateProductDto.category} : undefined, cost_price: 0.2 * updateProductDto.price - updateProductDto.price, stock_status: await this.calculateProductStatus(updateProductDto.quantity), }); diff --git a/src/modules/products/tests/mocks/deleted-product.mock.ts b/src/modules/products/tests/mocks/deleted-product.mock.ts index 5781f036f..d4841c931 100644 --- a/src/modules/products/tests/mocks/deleted-product.mock.ts +++ b/src/modules/products/tests/mocks/deleted-product.mock.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'crypto'; import { orgMock } from '../../../organisations/tests/mocks/organisation.mock'; import { Product, StockStatusType } from '../../entities/product.entity'; +import { productCategoryMock } from './product-category.mock'; enum ProductSizeType { SMALL = 'Small', @@ -15,7 +16,7 @@ export const deletedProductMock: Product = { stock_status: StockStatusType.LOW_STOCK, image: '', price: 12, - category: 'Fashion', + category: productCategoryMock, quantity: 7, cost_price: 10, orderItems: [], diff --git a/src/modules/products/tests/mocks/product-category.mock.ts b/src/modules/products/tests/mocks/product-category.mock.ts new file mode 100644 index 000000000..955ffe33b --- /dev/null +++ b/src/modules/products/tests/mocks/product-category.mock.ts @@ -0,0 +1,11 @@ +import { ProductCategory } from "src/modules/product-category/entities/product-category.entity"; + + +export const productCategoryMock: ProductCategory = { + id: 'category-id-123', + name: 'Fashion', + description: '', + products: [], + created_at: new Date(), + updated_at: new Date(), +}; diff --git a/src/modules/products/tests/mocks/product-request-dto.mock.ts b/src/modules/products/tests/mocks/product-request-dto.mock.ts index a4b1af6f8..cf9a9eb83 100644 --- a/src/modules/products/tests/mocks/product-request-dto.mock.ts +++ b/src/modules/products/tests/mocks/product-request-dto.mock.ts @@ -1,6 +1,7 @@ import { CreateProductRequestDto } from '../../dto/create-product.dto'; import { productMock } from './product.mock'; + export const createProductRequestDtoMock: CreateProductRequestDto = { name: productMock.name, price: productMock.price, diff --git a/src/modules/products/tests/mocks/product.mock.ts b/src/modules/products/tests/mocks/product.mock.ts index c6b26ac6c..aab940d36 100644 --- a/src/modules/products/tests/mocks/product.mock.ts +++ b/src/modules/products/tests/mocks/product.mock.ts @@ -1,6 +1,8 @@ import { randomUUID } from 'crypto'; import { orgMock } from '../../../../modules/organisations/tests/mocks/organisation.mock'; import { Product, StockStatusType } from '../../entities/product.entity'; +import { productCategoryMock } from './product-category.mock'; + enum ProductSizeType { SMALL = 'Small', @@ -15,7 +17,7 @@ export const productMock: Product = { stock_status: StockStatusType.LOW_STOCK, image: '', price: 12, - category: 'Fashion', + category: productCategoryMock, quantity: 7, size: ProductSizeType.SMALL, org: orgMock, From 058517c009f4805587fd0b3907c32e70c212f051 Mon Sep 17 00:00:00 2001 From: Biglens Date: Sat, 1 Mar 2025 12:33:22 +0100 Subject: [PATCH 2/5] fix: product category linked to the product --- src/modules/products/entities/product.entity.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/products/entities/product.entity.ts b/src/modules/products/entities/product.entity.ts index a29223fc2..9b2c565b3 100644 --- a/src/modules/products/entities/product.entity.ts +++ b/src/modules/products/entities/product.entity.ts @@ -4,7 +4,7 @@ import { Comment } from '../../../modules/comments/entities/comments.entity'; import { Organisation } from '../../../modules/organisations/entities/organisations.entity'; import { Cart } from '../../dashboard/entities/cart.entity'; import { OrderItem } from '../../dashboard/entities/order-items.entity'; -import { ProductVariant } from '../../products/entities/product-variant.entity' +import { ProductVariant } from '../../products/entities/product-variant.entity'; import { ProductCategory } from '../../../modules/product-category/entities/product-category.entity'; export enum StockStatusType { @@ -71,6 +71,6 @@ export class Product extends AbstractBaseEntity { @OneToMany(() => ProductVariant, variant => variant.product, { cascade: true }) variants?: ProductVariant[]; - @ManyToOne(() => ProductCategory, (category) => category.products, { nullable: false }) - category: ProductCategory; -} \ No newline at end of file + @ManyToOne(() => ProductCategory, category => category.products, { nullable: false }) + category: ProductCategory; //category linked to the product +} From 0ccda213a1c560ac6e955e3845ed4963f2007e16 Mon Sep 17 00:00:00 2001 From: Biglens Date: Sat, 1 Mar 2025 13:35:05 +0100 Subject: [PATCH 3/5] fix: two conflicting files resolved --- src/modules/products/products.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/products/products.module.ts b/src/modules/products/products.module.ts index 6996489a4..c8f0aaeb5 100644 --- a/src/modules/products/products.module.ts +++ b/src/modules/products/products.module.ts @@ -18,7 +18,7 @@ import { ProductCategory } from '../product-category/entities/product-category.e @Module({ imports: [ TypeOrmModule.forFeature([ - Product, Organisation, + Product, Organisation, ProductVariant, User, @@ -28,7 +28,7 @@ import { ProductCategory } from '../product-category/entities/product-category.e Order, OrderItem, Cart, - ProductCategory + ProductCategory, ]), UserModule, ], From 4eff116f1580d46cdbb7433ecd5ce46eb74c8c61 Mon Sep 17 00:00:00 2001 From: Biglens Date: Sat, 1 Mar 2025 13:38:34 +0100 Subject: [PATCH 4/5] fix: two conflicting files resolved --- src/modules/products/products.module.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/products/products.module.ts b/src/modules/products/products.module.ts index c8f0aaeb5..3821705e9 100644 --- a/src/modules/products/products.module.ts +++ b/src/modules/products/products.module.ts @@ -13,7 +13,7 @@ import { Role } from '../role/entities/role.entity'; import { Product } from './entities/product.entity'; import { ProductsController } from './products.controller'; import { ProductsService } from './products.service'; -import { ProductCategory } from '../product-category/entities/product-category.entity'; + @Module({ imports: [ @@ -28,7 +28,6 @@ import { ProductCategory } from '../product-category/entities/product-category.e Order, OrderItem, Cart, - ProductCategory, ]), UserModule, ], From c673542f3800e2a8f26259984ebaffe58327ed5f Mon Sep 17 00:00:00 2001 From: Biglens Date: Sat, 1 Mar 2025 13:41:29 +0100 Subject: [PATCH 5/5] fix: two conflicting files resolved --- src/modules/products/products.service.ts | 37 +++++++++++------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index 68f94b271..ba2169d05 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -18,9 +18,7 @@ import { Organisation } from '../organisations/entities/organisations.entity'; import { User } from '../user/entities/user.entity'; import { CreateProductRequestDto } from './dto/create-product.dto'; import { UpdateProductDTO } from './dto/update-product.dto'; -import { ProductVariant } from './entities/product-variant.entity'; import { Product, ProductSizeType, StockStatusType } from './entities/product.entity'; -import { ProductCategory } from '../product-category/entities/product-category.entity'; interface SearchCriteria { name?: string; @@ -61,15 +59,14 @@ export class ProductsService { const category = await this.categoryRepository.findOne({ where: { id: payload.category }, }); - + if (!category) { throw new BadRequestException(`Invalid category ID: ${payload.category}`); } - const newProduct: Product = this.productRepository.create({ ...payload, - category, + category, }); newProduct.org = org; @@ -87,20 +84,20 @@ export class ProductsService { }); } - return { - status: 'success', - message: 'Product created successfully', - data: { - id: product.id, - name: product.name, - description: product.description, - price: product.price, - status: product.stock_status, - quantity: product.quantity, - created_at: product.created_at, - updated_at: product.updated_at, - }, - }; + return { + status: 'success', + message: 'Product created successfully', + data: { + id: product.id, + name: product.name, + description: product.description, + price: product.price, + status: product.stock_status, + quantity: product.quantity, + created_at: product.created_at, + updated_at: product.updated_at, + }, + }; } async getAllProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { @@ -217,7 +214,7 @@ export class ProductsService { try { await this.productRepository.update(productId, { ...updateProductDto, - category: updateProductDto.category ? {id: updateProductDto.category} : undefined, + category: updateProductDto.category ? { id: updateProductDto.category } : undefined, cost_price: 0.2 * updateProductDto.price - updateProductDto.price, stock_status: await this.calculateProductStatus(updateProductDto.quantity), });