Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/products/dto/create-product.dto.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator';
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()
name: string;

@ApiProperty()
@IsString()
genericName: string;

@ApiProperty()
@IsOptional()
@IsString()
description?: string;

@ApiProperty()
@IsInt()
priority: number;

@ApiProperty()
@IsUUID()
manufacturer: string;

@ApiProperty({
required: false,
})
@IsArray()
@IsOptional()
@IsString({ each: true })
categoryIds: string[];

@ApiProperty({
required: false,
})
@IsArray()
@IsOptional()
@IsString({ each: true })
imageUrls: string[];

@ApiProperty({
type: [CreateProductPresentationDTO],
required: false,
})
@IsArray()
@IsOptional()
presentations: CreateProductPresentationDTO[];
}
69 changes: 59 additions & 10 deletions src/products/products.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import {
Post,
Query,
Req,
UnauthorizedException,
UseGuards,
} 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';
Expand All @@ -24,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)
Expand Down Expand Up @@ -70,19 +75,63 @@ export class ProductsController {
}

@Post()
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.BRANCH_ADMIN)
@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(
Comment thread
iabrahaamxs marked this conversation as resolved.
@Body() createProductDto: CreateProductDTO,
@Req() request: CustomRequest,
): Promise<Product> {
if (![UserRole.ADMIN, UserRole.BRANCH_ADMIN].includes(request.user.role)) {
throw new UnauthorizedException();
}

const manufacturer = await this.productsServices.findManufacturer(
createProductDto.manufacturer,
);

return this.productsServices.createProduct(createProductDto, manufacturer);
const newProduct = await this.productsServices.createProduct(
createProductDto,
manufacturer,
);

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

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;
}
}
12 changes: 11 additions & 1 deletion src/products/products.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@ 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';
import { Presentation } from './entities/presentation.entity';

@Module({
imports: [
TypeOrmModule.forFeature([Product, ProductPresentation, Manufacturer]),
TypeOrmModule.forFeature([
Product,
ProductPresentation,
Presentation,
Manufacturer,
Category,
ProductImage,
]),
AuthModule,
],
controllers: [ProductsController],
Expand Down
105 changes: 103 additions & 2 deletions src/products/products.service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
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 {
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 {
constructor(
@InjectRepository(ProductPresentation)
private productPresentationRepository: Repository<ProductPresentation>,

@InjectRepository(Presentation)
private PresentationRepository: Repository<Presentation>,

@InjectRepository(Product)
private productRepository: Repository<Product>,

@InjectRepository(Manufacturer)
private manufacturerRepository: Repository<Manufacturer>,

@InjectRepository(Category)
private categoryRepository: Repository<Category>,

@InjectRepository(ProductImage)
private productImageRepository: Repository<ProductImage>,
) {}

async countProducts(): Promise<number> {
Expand Down Expand Up @@ -61,6 +76,22 @@ export class ProductsService {
return manufacturer;
}

async findCategories(ids: string[]): Promise<Category[]> {
if (!ids || ids.length === 0) {
return [];
}

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,
Expand All @@ -73,4 +104,74 @@ export class ProductsService {
const savedProduct = await this.productRepository.save(newProduct);
return savedProduct;
}

async createProductImage(product: Product, images: string[]): Promise<void> {
const productImages = images.map((url) =>
this.productImageRepository.create({ url, product }),
);
await this.productImageRepository.save(productImages);
}

async addCategoriesToProduct(
product: Product,
categoriesToAdd: Category[],
): Promise<void> {
if (categoriesToAdd.length === 0) {
return;
}

if (!product.categories) {
product.categories = [];
}

product.categories = [...product.categories, ...categoriesToAdd];

await this.productRepository.save(product);
}

async findPresentations(ids: string[]): Promise<Presentation[]> {
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<void> {
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);
}
}