Files
dkala-api/src/modules/products/providers/product.service.ts
T
2026-02-09 12:34:47 +03:30

222 lines
6.6 KiB
TypeScript

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { CreateProductDto } from '../dto/create-product.dto';
import { FindProductsDto } from '../dto/find-products.dto';
import { ProductRepository } from '../repositories/product.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
import { Product } from '../entities/product.entity';
import { CategoryMessage, ProductMessage } from 'src/common/enums/message.enum';
import { Favorite } from '../entities/favorite.entity';
import { Variant } from '../entities/variant.entity';
import { ShopService } from 'src/modules/shops/providers/shops.service';
import { CategoryService } from './category.service';
@Injectable()
export class ProductService {
constructor(
private readonly productRepository: ProductRepository,
private readonly categoryRepository: CategoryRepository,
private readonly categoryService: CategoryService,
private readonly shopService: ShopService,
private readonly em: EntityManager,
) { }
async create(shopId: string, dto: CreateProductDto) {
const { categoryId, variants, ...rest } = dto;
const shop = await this.shopService.findOrFail(shopId);
const category = await this.categoryService.findOrFail(categoryId)
const product = await this.em.transactional(async em => {
// prepare data with defaults for required fields so repository.create typing is satisfied
const data: RequiredEntityData<Product> = {
desc: rest.desc,
isActive: rest.isActive ?? true,
discount: rest.discount ?? 0,
isSpecialOffer: rest.isSpecialOffer ?? false,
order: rest.order ?? null,
// map single-title/content DTO to localized fields
title: rest.title,
attribute: rest.attribute,
images: rest.images ?? undefined,
shop: shop,
category: category,
};
const product = em.create(Product, data);
for (const variant of variants) {
const variantRecord = em.create(Variant, {
product,
value: variant.value,
price: variant.price,
})
em.persist(variantRecord)
}
await em.flush();
return product;
});
return product;
}
findAll(shopId: string, dto: FindProductsDto) {
return this.productRepository.findAllPaginated(shopId, dto);
}
async findPublicById(productId: string, userId?: string): Promise<any> {
const product = await this.findOrFail(productId)
let isFavorite = false;
if (userId) {
isFavorite = (await this.em.count(Favorite, { user: { id: userId }, product: { id: productId } })) > 0;
}
return {
...product,
isFavorite,
};
}
async findAdminById(shopId: string, productId: string): Promise<Product> {
const product = await this.findOrFail(productId)
if (product.shop.id !== shopId) throw new NotFoundException(ProductMessage.NOT_FOUND);
return product;
}
async findByShop(slug: string): Promise<Product[]> {
const shop = await this.shopService.findOrFailBySlug(slug);
const queryFilter: FilterQuery<Product> = {
shop: { slug },
isActive: true,
} as unknown as FilterQuery<Product>;
return this.productRepository.find(queryFilter, {
populate: ['category'],
orderBy: { order: 'asc' }
});
}
async update(shopId: string, productId: string, dto: Partial<CreateProductDto>): Promise<Product> {
const { categoryId, variants, ...rest } = dto;
const product = await this.findOrFail(productId)
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
if (categoryId) {
const category = await this.categoryRepository.findOne({ id: categoryId, shop: { id: shopId } });
if (!category) {
throw new NotFoundException(CategoryMessage.NOT_FOUND);
}
this.em.assign(product, { category: category });
}
if (variants) {
// Get existing variants for this product
const existingVariants = product.variants
// Create a map of existing variants by id for easy lookup
const existingVariantsMap = new Map(existingVariants.map((v: any) => [v.id, v]));
// Track which existing variants are being updated (to avoid deletion)
const updatedVariantIds = new Set<string>();
for (const variant of variants) {
const { id, value, price } = variant;
if (id) {
// Update existing variant
const existingVariant = existingVariantsMap.get(id);
if (!existingVariant) {
throw new NotFoundException('Variant not found');
}
// Update the variant fields
this.em.assign(existingVariant, { value, price });
updatedVariantIds.add(id);
} else {
// Create new variant
const variantRecord = this.em.create(Variant, {
product,
value,
price,
});
this.em.persist(variantRecord);
}
}
// Delete variants that exist but were not included in the update
for (const existingVariant of existingVariants) {
if (!updatedVariantIds.has((existingVariant as any).id)) {
this.em.remove(existingVariant);
}
}
}
// assign other fields from DTO (excluding dailyStock handled below)
this.em.assign(product, rest);
await this.em.persistAndFlush(product);
return product;
}
async remove(shopId: string, id: string): Promise<void> {
const product = await this.findOrFail(id)
if (product.shop.id !== shopId) {
throw new BadRequestException("Product doesnt belongs to you")
}
product.deletedAt = new Date();
await this.em.flush();
}
async toggleFavorite(userId: string, productId: string) {
const favorite = await this.em.findOne(Favorite, { user: userId, product: productId });
if (favorite) {
return this.em.removeAndFlush(favorite);
}
const newFavorite = this.em.create(Favorite, {
user: userId,
product: productId,
});
return this.em.persistAndFlush(newFavorite);
}
async getMyFavorites(userId: string, shopId: string) {
return this.em.find(Favorite, { user: userId, product: { shop: { id: shopId } } }, { populate: ['product'] });
}
/**
* Helper
*/
async findOrFail(productId: string) {
const product = await this.productRepository.findOne({ id: productId },
{ populate: ['category', 'variants', 'shop'] });
if (!product) throw new NotFoundException(ProductMessage.NOT_FOUND);
return product
}
}