subspace model also has product model and item

This commit is contained in:
hannathkadher
2024-12-13 15:00:33 +04:00
parent 5faeba0869
commit 1373dfb0ce
14 changed files with 286 additions and 66 deletions

View File

@ -1,10 +1,12 @@
import { Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { ProductService } from './services'; import { ProductService } from './services';
import { ProductRepository } from '@app/common/modules/product/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories';
import { ProductController } from './controllers'; import { ProductController } from './controllers';
@Global()
@Module({ @Module({
controllers: [ProductController], controllers: [ProductController],
providers: [ProductService, ProductRepository], providers: [ProductService, ProductRepository],
exports: [ProductService],
}) })
export class ProductModule {} export class ProductModule {}

View File

@ -21,4 +21,23 @@ export class ProductService {
message: 'List of products retrieved successfully', message: 'List of products retrieved successfully',
}); });
} }
async findOne(productUuid: string): Promise<BaseResponseDto> {
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',
});
}
} }

View File

@ -0,0 +1 @@
export * from './services';

View File

@ -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<void> {
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,
);
}
}
}

View File

@ -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<void> {
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;
}
}

View File

@ -0,0 +1,2 @@
export * from './base-product-item-model.service';
export * from './base-product-model.service';

View File

@ -1,5 +1,13 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { export class CreateSubspaceModelDto {
@ApiProperty({ @ApiProperty({
@ -9,4 +17,14 @@ export class CreateSubspaceModelDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
subspaceName: string; subspaceName: string;
@ApiProperty({
description: 'List of products included in the model',
type: [CreateSpaceProductModelDto],
})
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => CreateSpaceProductModelDto)
spaceProductModels?: CreateSpaceProductModelDto[];
} }

View File

@ -2,3 +2,5 @@ export * from './space-model.service';
export * from './space-product-item-model.service'; export * from './space-product-item-model.service';
export * from './space-product-model.service'; export * from './space-product-model.service';
export * from './subspace-model.service'; export * from './subspace-model.service';
export * from './subspace-product-item-model.service';
export * from './space-product-model.service';

View File

@ -6,12 +6,15 @@ import {
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateProductItemModelDto } from '../dtos'; import { CreateProductItemModelDto } from '../dtos';
import { QueryRunner } from 'typeorm'; import { QueryRunner } from 'typeorm';
import { BaseProductItemService } from '../common';
@Injectable() @Injectable()
export class SpaceProductItemModelService { export class SpaceProductItemModelService extends BaseProductItemService {
constructor( constructor(
private readonly spaceProductItemRepository: SpaceProductItemModelRepository, private readonly spaceProductItemRepository: SpaceProductItemModelRepository,
) {} ) {
super();
}
async createProdutItemModel( async createProdutItemModel(
itemModelDtos: CreateProductItemModelDto[], 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,
);
}
}
} }

View File

@ -4,17 +4,20 @@ import {
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSpaceProductModelDto } from '../dtos'; import { CreateSpaceProductModelDto } from '../dtos';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { SpaceProductItemModelService } from './space-product-item-model.service'; import { SpaceProductItemModelService } from './space-product-item-model.service';
import { QueryRunner } from 'typeorm'; import { QueryRunner } from 'typeorm';
import { BaseProductModelService } from '../common';
import { ProductService } from 'src/product/services';
@Injectable() @Injectable()
export class SpaceProductModelService { export class SpaceProductModelService extends BaseProductModelService {
constructor( constructor(
private readonly spaceProductModelRepository: SpaceProductModelRepository, private readonly spaceProductModelRepository: SpaceProductModelRepository,
private readonly productRepository: ProductRepository,
private readonly spaceProductItemModelService: SpaceProductItemModelService, private readonly spaceProductItemModelService: SpaceProductItemModelService,
) {} productService: ProductService,
) {
super(productService);
}
async createSpaceProductModels( async createSpaceProductModels(
spaceProductModelDtos: CreateSpaceProductModelDto[], 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;
}
} }

View File

@ -5,11 +5,13 @@ import {
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSubspaceModelDto } from '../dtos'; import { CreateSubspaceModelDto } from '../dtos';
import { QueryRunner } from 'typeorm'; import { QueryRunner } from 'typeorm';
import { SubspaceProductModelService } from './subspace-product-model.service';
@Injectable() @Injectable()
export class SubSpaceModelService { export class SubSpaceModelService {
constructor( constructor(
private readonly subspaceModelRepository: SubspaceModelRepository, private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly subSpaceProducetModelService: SubspaceProductModelService,
) {} ) {}
async createSubSpaceModels( async createSubSpaceModels(
@ -28,6 +30,18 @@ export class SubSpaceModelService {
); );
await queryRunner.manager.save(subspaces); 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) { } catch (error) {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;

View File

@ -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,
);
}
}
}

View File

@ -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,
);
}
}
}

View File

@ -7,15 +7,19 @@ import {
SpaceProductItemModelService, SpaceProductItemModelService,
SpaceProductModelService, SpaceProductModelService,
SubSpaceModelService, SubSpaceModelService,
SubspaceProductItemModelService,
} from './services'; } from './services';
import { import {
SpaceModelRepository, SpaceModelRepository,
SpaceProductItemModelRepository, SpaceProductItemModelRepository,
SpaceProductModelRepository, SpaceProductModelRepository,
SubspaceModelRepository, SubspaceModelRepository,
SubspaceProductItemModelRepository,
SubspaceProductModelRepository,
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ProductRepository } from '@app/common/modules/product/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories';
import { SubspaceProductModelService } from './services/subspace-product-model.service';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule], imports: [ConfigModule, SpaceRepositoryModule],
@ -31,6 +35,10 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
ProductRepository, ProductRepository,
SpaceProductItemModelService, SpaceProductItemModelService,
SpaceProductItemModelRepository, SpaceProductItemModelRepository,
SubspaceProductItemModelService,
SubspaceProductItemModelRepository,
SubspaceProductModelService,
SubspaceProductModelRepository,
], ],
exports: [], exports: [],
}) })