Files
dmenu-api/src/modules/foods/providers/food.service.ts
T
2025-12-16 14:47:15 +03:30

165 lines
5.6 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateFoodDto } from '../dto/create-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import { FoodRepository } from '../repositories/food.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData } from '@mikro-orm/core';
import { Food } from '../entities/food.entity';
import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { CacheService } from '../../utils/cache.service';
@Injectable()
export class FoodService {
private readonly FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX = 'foods:restaurant:';
private readonly FOODS_CACHE_TTL = 600; // 10 minutes in seconds
constructor(
private readonly foodRepository: FoodRepository,
private readonly categoryRepository: CategoryRepository,
private readonly restRepository: RestRepository,
private readonly em: EntityManager,
private readonly cacheService: CacheService,
) { }
async create(restId: string, createFoodDto: CreateFoodDto): Promise<Food> {
const { categoryId, ...rest } = createFoodDto;
const restaurant = await this.restRepository.findOne({ id: restId });
if (!restaurant) {
throw new NotFoundException(RestMessage.NOT_FOUND);
}
const category = await this.categoryRepository.findOne({ id: categoryId });
if (!category) {
throw new NotFoundException(CategoryMessage.NOT_FOUND);
}
// prepare data with defaults for required fields so repository.create typing is satisfied
const data: RequiredEntityData<Food> = {
desc: rest.desc,
isActive: rest.isActive ?? true,
inPlaceServe: rest.inPlaceServe ?? false,
pickupServe: rest.pickupServe ?? false,
discount: rest.discount ?? 0,
isSpecialOffer: rest.isSpecialOffer ?? false,
// map single-title/content DTO to localized fields
title: rest.title,
content: rest.content,
// numeric/array fields
price: rest.price ?? 0,
prepareTime: rest.prepareTime ?? 0,
images: rest.images ?? undefined,
// new fields
weekDays: rest.weekDays ?? [0, 1, 2, 3, 4, 5, 6],
mealTypes: rest.mealTypes ?? [],
score: rest.score ?? null,
restaurant: restaurant,
category: category,
};
const food = this.foodRepository.create(data);
if (!food) {
throw new Error('Failed to create food entity');
}
await this.em.persistAndFlush(food);
// Invalidate cache for the restaurant
await this.invalidateRestaurantFoodsCache(restaurant.slug);
return food;
}
findAll(restId: string, dto: FindFoodsDto) {
return this.foodRepository.findAllPaginated(restId, dto);
}
async findById(id: string): Promise<Food> {
const food = await this.foodRepository.findOne({ id }, { populate: ['category'] });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
return food;
}
async findAllByRestaurant(slug: string): Promise<Food[]> {
const cacheKey = `${this.FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX}${slug}`;
// Try to get from cache first
const cachedFoods = await this.cacheService.get<string>(cacheKey);
if (cachedFoods) {
try {
const parsed: unknown = JSON.parse(cachedFoods);
if (Array.isArray(parsed)) {
return parsed as Food[];
}
} catch {
// If parsing fails, continue to fetch from DB
}
}
// If not in cache, fetch from database
const restaurant = await this.restRepository.findOne({ slug });
if (!restaurant) {
throw new NotFoundException(RestMessage.NOT_FOUND);
}
const foods = await this.foodRepository.find({ restaurant: { slug }, isActive: true }, { populate: ['category'] });
// Cache the result
await this.cacheService.set(cacheKey, JSON.stringify(foods), this.FOODS_CACHE_TTL);
return foods;
}
async update(id: string, dto: Partial<CreateFoodDto>): Promise<Food> {
const food = await this.foodRepository.findOne({ id }, { populate: ['restaurant'] });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
if (dto.categoryId) {
const category = await this.categoryRepository.findOne({ id: dto.categoryId });
if (!category) {
throw new NotFoundException(CategoryMessage.NOT_FOUND);
}
this.em.assign(food, { category: category });
}
// assign other fields from DTO
const { categoryId, ...rest } = dto;
this.em.assign(food, rest);
await this.em.persistAndFlush(food);
// Invalidate cache for the restaurant
await this.invalidateRestaurantFoodsCache(food.restaurant.slug);
return food;
}
async remove(id: string) {
const food = await this.foodRepository.findOne({ id }, { populate: ['restaurant'] });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
const restaurantSlug = food.restaurant.slug;
food.deletedAt = new Date();
await this.em.persistAndFlush(food);
// Invalidate cache for the restaurant
await this.invalidateRestaurantFoodsCache(restaurantSlug);
}
/**
* Invalidate cache for restaurant foods
*/
private async invalidateRestaurantFoodsCache(slug: string): Promise<void> {
const cacheKey = `${this.FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX}${slug}`;
await this.cacheService.del(cacheKey);
}
}