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..9b2c565b3 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; //category linked to the product } 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..3821705e9 100644 --- a/src/modules/products/products.module.ts +++ b/src/modules/products/products.module.ts @@ -14,6 +14,7 @@ import { Product } from './entities/product.entity'; import { ProductsController } from './products.controller'; import { ProductsService } from './products.service'; + @Module({ imports: [ TypeOrmModule.forFeature([ diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index 380791dc0..ba2169d05 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, ForbiddenException, HttpStatus, Injectable, @@ -17,7 +18,6 @@ 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'; interface SearchCriteria { @@ -34,7 +34,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,18 +56,33 @@ 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', @@ -198,6 +214,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,