diff --git a/src/product/product.module.ts b/src/product/product.module.ts index e5ffd25..aa13d20 100644 --- a/src/product/product.module.ts +++ b/src/product/product.module.ts @@ -1,10 +1,12 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { ProductService } from './services'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { ProductController } from './controllers'; +@Global() @Module({ controllers: [ProductController], providers: [ProductService, ProductRepository], + exports: [ProductService], }) export class ProductModule {} diff --git a/src/product/services/product.service.ts b/src/product/services/product.service.ts index cebf9ba..4b9377c 100644 --- a/src/product/services/product.service.ts +++ b/src/product/services/product.service.ts @@ -21,4 +21,23 @@ export class ProductService { message: 'List of products retrieved successfully', }); } + + async findOne(productUuid: string): Promise { + const product = await this.productRepository.findOne({ + where: { + uuid: productUuid, + }, + }); + if (!product) { + throw new HttpException( + `No product with ${productUuid} found in the system`, + HttpStatus.NOT_FOUND, + ); + } + + return new SuccessResponseDto({ + data: product, + message: 'Succefully retrieved product', + }); + } } diff --git a/src/space-model/common/index.ts b/src/space-model/common/index.ts new file mode 100644 index 0000000..e371345 --- /dev/null +++ b/src/space-model/common/index.ts @@ -0,0 +1 @@ +export * from './services'; diff --git a/src/space-model/common/services/base-product-item-model.service.ts b/src/space-model/common/services/base-product-item-model.service.ts new file mode 100644 index 0000000..80c8119 --- /dev/null +++ b/src/space-model/common/services/base-product-item-model.service.ts @@ -0,0 +1,60 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; +import { SpaceModelEntity } from '@app/common/modules/space-model'; +import { CreateProductItemModelDto } from 'src/space-model/dtos'; + +export abstract class BaseProductItemService { + async validateTags( + itemModelDtos: CreateProductItemModelDto[], + queryRunner: QueryRunner, + spaceModel: SpaceModelEntity, + ): Promise { + const incomingTags = new Set( + itemModelDtos.map((item) => item.tag).filter(Boolean), + ); + + const duplicateTags = itemModelDtos + .map((item) => item.tag) + .filter((tag, index, array) => array.indexOf(tag) !== index); + + if (duplicateTags.length > 0) { + throw new HttpException( + `Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + const existingTagsQuery = ` + SELECT DISTINCT tag + FROM ( + SELECT spi.tag + FROM "subspace-product-item-model" spi + INNER JOIN "subspace-product-model" spm ON spi.subspace_product_model_uuid = spm.uuid + INNER JOIN "subspace-model" sm ON spm.subspace_model_uuid = sm.uuid + WHERE sm.space_model_uuid = $1 + UNION + SELECT spi.tag + FROM "space-product-item-model" spi + INNER JOIN "space-product-model" spm ON spi.space_product_model_uuid = spm.uuid + WHERE spm.space_model_uuid = $1 + ) AS combined_tags; + `; + + const existingTags = await queryRunner.manager.query(existingTagsQuery, [ + spaceModel.uuid, + ]); + const existingTagsSet = new Set( + existingTags.map((row: { tag: string }) => row.tag), + ); + + const conflictingTags = [...incomingTags].filter((tag) => + existingTagsSet.has(tag), + ); + if (conflictingTags.length > 0) { + throw new HttpException( + `Tags already exist in the model: ${conflictingTags.join(', ')}`, + HttpStatus.CONFLICT, + ); + } + } +} diff --git a/src/space-model/common/services/base-product-model.service.ts b/src/space-model/common/services/base-product-model.service.ts new file mode 100644 index 0000000..b83511a --- /dev/null +++ b/src/space-model/common/services/base-product-model.service.ts @@ -0,0 +1,24 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CreateSpaceProductModelDto } from 'src/space-model/dtos'; +import { ProductService } from '../../../product/services'; + +export abstract class BaseProductModelService { + constructor(private readonly productService: ProductService) {} + + protected async validateProductCount( + dto: CreateSpaceProductModelDto, + ): Promise { + const productItemCount = dto.items.length; + if (dto.productCount !== productItemCount) { + throw new HttpException( + `Product count (${dto.productCount}) does not match the number of items (${productItemCount}) for product ID ${dto.productUuid}.`, + HttpStatus.BAD_REQUEST, + ); + } + } + + protected async getProduct(productId: string) { + const product = await this.productService.findOne(productId); + return product.data; + } +} diff --git a/src/space-model/common/services/index.ts b/src/space-model/common/services/index.ts new file mode 100644 index 0000000..d1cc61e --- /dev/null +++ b/src/space-model/common/services/index.ts @@ -0,0 +1,2 @@ +export * from './base-product-item-model.service'; +export * from './base-product-model.service'; diff --git a/src/space-model/dtos/create-subspace-model.dto.ts b/src/space-model/dtos/create-subspace-model.dto.ts index a27ad3b..f8dbcdd 100644 --- a/src/space-model/dtos/create-subspace-model.dto.ts +++ b/src/space-model/dtos/create-subspace-model.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { CreateSpaceProductModelDto } from './create-space-product-model.dto'; +import { Type } from 'class-transformer'; export class CreateSubspaceModelDto { @ApiProperty({ @@ -9,4 +17,14 @@ export class CreateSubspaceModelDto { @IsNotEmpty() @IsString() subspaceName: string; + + @ApiProperty({ + description: 'List of products included in the model', + type: [CreateSpaceProductModelDto], + }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CreateSpaceProductModelDto) + spaceProductModels?: CreateSpaceProductModelDto[]; } diff --git a/src/space-model/services/index.ts b/src/space-model/services/index.ts index 88e2d41..9972aab 100644 --- a/src/space-model/services/index.ts +++ b/src/space-model/services/index.ts @@ -2,3 +2,5 @@ export * from './space-model.service'; export * from './space-product-item-model.service'; export * from './space-product-model.service'; export * from './subspace-model.service'; +export * from './subspace-product-item-model.service'; +export * from './space-product-model.service'; diff --git a/src/space-model/services/space-product-item-model.service.ts b/src/space-model/services/space-product-item-model.service.ts index f7f8b04..c69cae8 100644 --- a/src/space-model/services/space-product-item-model.service.ts +++ b/src/space-model/services/space-product-item-model.service.ts @@ -6,12 +6,15 @@ import { import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateProductItemModelDto } from '../dtos'; import { QueryRunner } from 'typeorm'; +import { BaseProductItemService } from '../common'; @Injectable() -export class SpaceProductItemModelService { +export class SpaceProductItemModelService extends BaseProductItemService { constructor( private readonly spaceProductItemRepository: SpaceProductItemModelRepository, - ) {} + ) { + super(); + } async createProdutItemModel( itemModelDtos: CreateProductItemModelDto[], @@ -41,41 +44,4 @@ export class SpaceProductItemModelService { ); } } - - private async validateTags( - itemModelDtos: CreateProductItemModelDto[], - queryRunner: QueryRunner, - spaceModel: SpaceModelEntity, - ) { - const incomingTags = itemModelDtos.map((item) => item.tag); - - const duplicateTags = incomingTags.filter( - (tag, index) => incomingTags.indexOf(tag) !== index, - ); - if (duplicateTags.length > 0) { - throw new HttpException( - `Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`, - HttpStatus.BAD_REQUEST, - ); - } - - const existingTags = await queryRunner.manager.find( - this.spaceProductItemRepository.target, - { - where: { spaceProductModel: { spaceModel } }, - select: ['tag'], - }, - ); - const existingTagSet = new Set(existingTags.map((item) => item.tag)); - - const conflictingTags = incomingTags.filter((tag) => - existingTagSet.has(tag), - ); - if (conflictingTags.length > 0) { - throw new HttpException( - `Tags already exist in the model: ${conflictingTags.join(', ')}`, - HttpStatus.CONFLICT, - ); - } - } } diff --git a/src/space-model/services/space-product-model.service.ts b/src/space-model/services/space-product-model.service.ts index 27bad29..1e16c6f 100644 --- a/src/space-model/services/space-product-model.service.ts +++ b/src/space-model/services/space-product-model.service.ts @@ -4,17 +4,20 @@ import { } from '@app/common/modules/space-model'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateSpaceProductModelDto } from '../dtos'; -import { ProductRepository } from '@app/common/modules/product/repositories'; import { SpaceProductItemModelService } from './space-product-item-model.service'; import { QueryRunner } from 'typeorm'; +import { BaseProductModelService } from '../common'; +import { ProductService } from 'src/product/services'; @Injectable() -export class SpaceProductModelService { +export class SpaceProductModelService extends BaseProductModelService { constructor( private readonly spaceProductModelRepository: SpaceProductModelRepository, - private readonly productRepository: ProductRepository, private readonly spaceProductItemModelService: SpaceProductItemModelService, - ) {} + productService: ProductService, + ) { + super(productService); + } async createSpaceProductModels( spaceProductModelDtos: CreateSpaceProductModelDto[], @@ -61,25 +64,4 @@ export class SpaceProductModelService { ); } } - - private validateProductCount(dto: CreateSpaceProductModelDto) { - const productItemCount = dto.items.length; - if (dto.productCount !== productItemCount) { - throw new HttpException( - `Product count (${dto.productCount}) does not match the number of items (${productItemCount}) for product ID ${dto.productUuid}.`, - HttpStatus.BAD_REQUEST, - ); - } - } - - private async getProduct(productId: string) { - const product = await this.productRepository.findOneBy({ uuid: productId }); - if (!product) { - throw new HttpException( - `Product with ID ${productId} not found.`, - HttpStatus.NOT_FOUND, - ); - } - return product; - } } diff --git a/src/space-model/services/subspace-model.service.ts b/src/space-model/services/subspace-model.service.ts index a6a75aa..11a501c 100644 --- a/src/space-model/services/subspace-model.service.ts +++ b/src/space-model/services/subspace-model.service.ts @@ -5,11 +5,13 @@ import { import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateSubspaceModelDto } from '../dtos'; import { QueryRunner } from 'typeorm'; +import { SubspaceProductModelService } from './subspace-product-model.service'; @Injectable() export class SubSpaceModelService { constructor( private readonly subspaceModelRepository: SubspaceModelRepository, + private readonly subSpaceProducetModelService: SubspaceProductModelService, ) {} async createSubSpaceModels( @@ -28,6 +30,18 @@ export class SubSpaceModelService { ); await queryRunner.manager.save(subspaces); + + await Promise.all( + subSpaceModelDtos.map((dto, index) => { + const subspaceModel = subspaces[index]; + return this.subSpaceProducetModelService.createSubspaceProductModels( + dto.spaceProductModels, + spaceModel, + subspaceModel, + queryRunner, + ); + }), + ); } catch (error) { if (error instanceof HttpException) { throw error; diff --git a/src/space-model/services/subspace-product-item-model.service.ts b/src/space-model/services/subspace-product-item-model.service.ts new file mode 100644 index 0000000..54c76ff --- /dev/null +++ b/src/space-model/services/subspace-product-item-model.service.ts @@ -0,0 +1,53 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateProductItemModelDto } from '../dtos'; +import { QueryRunner } from 'typeorm'; +import { + SpaceModelEntity, + SubspaceProductItemModelRepository, + SubspaceProductModelEntity, +} from '@app/common/modules/space-model'; +import { BaseProductItemService } from '../common'; + +@Injectable() +export class SubspaceProductItemModelService extends BaseProductItemService { + constructor( + private readonly subspaceProductItemRepository: SubspaceProductItemModelRepository, + ) { + super(); + } + + async createProdutItemModel( + itemModelDtos: CreateProductItemModelDto[], + subspaceProductModel: SubspaceProductModelEntity, + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ) { + if (!subspaceProductModel) { + throw new HttpException( + 'The spaceProductModel parameter is required but was not provided.', + HttpStatus.BAD_REQUEST, + ); + } + await this.validateTags(itemModelDtos, queryRunner, spaceModel); + try { + const productItems = itemModelDtos.map((dto) => + queryRunner.manager.create(this.subspaceProductItemRepository.target, { + tag: dto.tag, + subspaceProductModel, + }), + ); + + await queryRunner.manager.save(productItems); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || + 'An unexpected error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space-model/services/subspace-product-model.service.ts b/src/space-model/services/subspace-product-model.service.ts new file mode 100644 index 0000000..ccf3d11 --- /dev/null +++ b/src/space-model/services/subspace-product-model.service.ts @@ -0,0 +1,69 @@ +import { + SpaceModelEntity, + SubspaceModelEntity, + SubspaceProductModelRepository, +} from '@app/common/modules/space-model'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { SubspaceProductItemModelService } from './subspace-product-item-model.service'; +import { CreateSpaceProductModelDto } from '../dtos'; +import { QueryRunner } from 'typeorm'; +import { BaseProductModelService } from '../common'; +import { ProductService } from 'src/product/services'; + +@Injectable() +export class SubspaceProductModelService extends BaseProductModelService { + constructor( + private readonly subpaceProductModelRepository: SubspaceProductModelRepository, + productService: ProductService, + private readonly subspaceProductItemModelService: SubspaceProductItemModelService, + ) { + super(productService); + } + + async createSubspaceProductModels( + spaceProductModelDtos: CreateSpaceProductModelDto[], + spaceModel: SpaceModelEntity, + subspaceModel: SubspaceModelEntity, + queryRunner: QueryRunner, + ) { + try { + const productModels = await Promise.all( + spaceProductModelDtos.map(async (dto) => { + this.validateProductCount(dto); + const product = await this.getProduct(dto.productUuid); + return queryRunner.manager.create( + this.subpaceProductModelRepository.target, + { + product, + productCount: dto.productCount, + subspaceModel, + }, + ); + }), + ); + + const savedProductModels = await queryRunner.manager.save(productModels); + + await Promise.all( + spaceProductModelDtos.map((dto, index) => { + const savedModel = savedProductModels[index]; + return this.subspaceProductItemModelService.createProdutItemModel( + dto.items, + savedModel, // Pass the saved model + spaceModel, + queryRunner, + ); + }), + ); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || + 'An unexpected error occurred while creating product models.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index bbc6245..5c189a1 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -7,15 +7,19 @@ import { SpaceProductItemModelService, SpaceProductModelService, SubSpaceModelService, + SubspaceProductItemModelService, } from './services'; import { SpaceModelRepository, SpaceProductItemModelRepository, SpaceProductModelRepository, SubspaceModelRepository, + SubspaceProductItemModelRepository, + SubspaceProductModelRepository, } from '@app/common/modules/space-model'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SubspaceProductModelService } from './services/subspace-product-model.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -31,6 +35,10 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; ProductRepository, SpaceProductItemModelService, SpaceProductItemModelRepository, + SubspaceProductItemModelService, + SubspaceProductItemModelRepository, + SubspaceProductModelService, + SubspaceProductModelRepository, ], exports: [], })