product module
This commit is contained in:
@@ -1,32 +1,32 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateFoodDto } from '../dto/create-product.dto';
|
||||
import { FindFoodsDto } from ../../..products.dto';
|
||||
import { FoodRepository } from '../repositories/product.repository';
|
||||
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, FoodMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { RestRepository } from ../../..shops/repositories/rest.repository';
|
||||
import { CacheService } from '../../../utils/cache.service';
|
||||
import { CategoryMessage, ProductMessage, RestMessage } 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';
|
||||
|
||||
@Injectable()
|
||||
export class FoodService {
|
||||
export class ProductService {
|
||||
constructor(
|
||||
private readonly foodRepository: FoodRepository,
|
||||
private readonly productRepository: ProductRepository,
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly shopService: ShopService,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async create(restId: string, createFoodDto: CreateFoodDto) {
|
||||
const { categoryId, ...rest } = createFoodDto;
|
||||
const shop = await this.restRepository.findOne({ id: restId });
|
||||
async create(shopId: string, dto: CreateProductDto) {
|
||||
const { categoryId, variants, ...rest } = dto;
|
||||
const shop = await this.shopService.findOrFail(shopId);
|
||||
if (!shop) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const category = await this.categoryRepository.findOne({ id: categoryId, shop: { id: restId } });
|
||||
const category = await this.categoryRepository.findOne({ id: categoryId, shop: { id: shopId } });
|
||||
if (!category) {
|
||||
throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class FoodService {
|
||||
order: rest.order ?? null,
|
||||
// map single-title/content DTO to localized fields
|
||||
title: rest.title,
|
||||
content: rest.content,
|
||||
attribute: rest.attribute,
|
||||
// numeric/array fields
|
||||
price: rest.price ?? 0,
|
||||
images: rest.images ?? undefined,
|
||||
@@ -50,31 +50,41 @@ export class FoodService {
|
||||
};
|
||||
|
||||
const product = em.create(Product, data);
|
||||
|
||||
em.persist(product)
|
||||
|
||||
for (const variant of variants) {
|
||||
const variantRecord = em.create(Variant, {
|
||||
product,
|
||||
value: variant.value,
|
||||
stock: variant.stock,
|
||||
price: variant.price,
|
||||
})
|
||||
|
||||
em.persist(variantRecord)
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
return product;
|
||||
});
|
||||
|
||||
// Re-load created entities with the primary EM to ensure they're attached
|
||||
const savedFood = product?.id
|
||||
? await this.foodRepository.findOne({ id: product.id }, { populate: ['category', 'shop'] })
|
||||
: null;
|
||||
|
||||
return savedFood ?? product;
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
findAll(restId: string, dto: FindFoodsDto) {
|
||||
return this.foodRepository.findAllPaginated(restId, dto);
|
||||
findAll(restId: string, dto: FindProductsDto) {
|
||||
return this.productRepository.findAllPaginated(restId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public product detail (only active products are visible).
|
||||
*/
|
||||
async findPublicById(foodId: string, userId?: string): Promise<any> {
|
||||
const product = await this.foodRepository.findOne({ id: foodId, isActive: true }, { populate: ['category'] });
|
||||
if (!product) throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
|
||||
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: foodId } })) > 0;
|
||||
isFavorite = (await this.em.count(Favorite, { user: { id: userId }, product: { id: productId } })) > 0;
|
||||
}
|
||||
return {
|
||||
...product,
|
||||
@@ -82,44 +92,33 @@ export class FoodService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin product detail (scoped to the authenticated shop).
|
||||
*/
|
||||
async findAdminById(restId: string, id: string): Promise<Product> {
|
||||
const product = await this.foodRepository.findOne({ id, shop: { id: restId } }, { populate: ['category'] });
|
||||
|
||||
if (!product) throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active products for a shop.
|
||||
* @param slug - Shop slug identifier
|
||||
* @returns Array of active products
|
||||
*/
|
||||
async findByWeekDateAndMealType(slug: string): Promise<Product[]> {
|
||||
const shop = await this.restRepository.findOne({ slug });
|
||||
if (!shop) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
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.foodRepository.find(queryFilter, {
|
||||
return this.productRepository.find(queryFilter, {
|
||||
populate: ['category'],
|
||||
orderBy: { order: 'asc' }
|
||||
});
|
||||
}
|
||||
|
||||
async update(restId: string, id: string, dto: Partial<CreateFoodDto>): Promise<Product> {
|
||||
const { categoryId, ...rest } = dto;
|
||||
const product = await this.foodRepository.findOne({ id, shop: { id: restId } }, { populate: ['shop'] });
|
||||
if (!product) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
async update(restId: 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) {
|
||||
@@ -132,44 +131,80 @@ export class FoodService {
|
||||
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, stock, 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, stock, price });
|
||||
updatedVariantIds.add(id);
|
||||
} else {
|
||||
// Create new variant
|
||||
const variantRecord = this.em.create(Variant, {
|
||||
product,
|
||||
value,
|
||||
stock,
|
||||
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);
|
||||
|
||||
// Re-load the product to ensure populated relations and stable instance
|
||||
const savedFood = await this.foodRepository.findOne({ id: product.id }, { populate: ['category', 'shop'] });
|
||||
|
||||
// Invalidate cache for the shop if present (optional)
|
||||
// await this.invalidateRestaurantFoodsCache(savedFood?.shop?.slug);
|
||||
|
||||
return savedFood ?? product;
|
||||
return product;
|
||||
}
|
||||
|
||||
async remove(restId: string, id: string): Promise<void> {
|
||||
const product = await this.foodRepository.findOne({ id, shop: { id: restId } }, { populate: ['shop'] });
|
||||
if (!product) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
const product = await this.findOrFail(id)
|
||||
|
||||
if (product.shop.id !== restId) {
|
||||
throw new BadRequestException("Product doesnt belongs to you")
|
||||
}
|
||||
|
||||
// const restaurantSlug = product.shop.slug;
|
||||
product.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(product);
|
||||
|
||||
// Invalidate cache for the shop
|
||||
// await this.invalidateRestaurantFoodsCache(restaurantSlug);
|
||||
await this.em.flush();
|
||||
|
||||
}
|
||||
|
||||
|
||||
async toggleFavorite(userId: string, foodId: string) {
|
||||
async toggleFavorite(userId: string, productId: string) {
|
||||
|
||||
const favorite = await this.em.findOne(Favorite, { user: userId, product: foodId });
|
||||
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: foodId,
|
||||
product: productId,
|
||||
});
|
||||
return this.em.persistAndFlush(newFavorite);
|
||||
}
|
||||
@@ -179,10 +214,14 @@ export class FoodService {
|
||||
return this.em.find(Favorite, { user: userId, product: { shop: { id: restId } } }, { populate: ['product'] });
|
||||
}
|
||||
/**
|
||||
* Invalidate cache for shop products
|
||||
* Helper
|
||||
*/
|
||||
// private async invalidateRestaurantFoodsCache(slug: string): Promise<void> {
|
||||
// const cacheKey = `${this.FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX}${slug}`;
|
||||
// await this.cacheService.del(cacheKey);
|
||||
// }
|
||||
async findOrFail(productId: string) {
|
||||
const product = await this.productRepository.findOne({ id: productId, isActive: true },
|
||||
{ populate: ['category', 'variants', 'shop'] });
|
||||
|
||||
if (!product) throw new NotFoundException(ProductMessage.NOT_FOUND);
|
||||
|
||||
return product
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user