all food endponit
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-06-21 16:46:41 +03:30
parent 449cd89287
commit 19c2d5bf56
7 changed files with 157 additions and 5 deletions
@@ -11,7 +11,10 @@ import {
ApiParam,
ApiBearerAuth,
ApiHeader,
ApiOkResponse,
} from '@nestjs/swagger';
import { SearchFoodsDto } from '../dto/search-foods.dto';
import { PublicFoodSearchItemDto } from '../dto/public-food-search-item.dto';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { RestId, UserId } from 'src/common/decorators';
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
@@ -37,6 +40,16 @@ export class FoodController {
return this.foodsService.findByRestuarantSlug(slug);
}
@Get('public/foods/search')
@ApiOperation({ summary: 'Search foods across active restaurants' })
@ApiQuery({ name: 'search', required: true, type: String, description: 'Search term (min 2 characters)' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiOkResponse({ description: 'Paginated food search results', type: [PublicFoodSearchItemDto] })
searchFoodsAcrossRestaurants(@Query() dto: SearchFoodsDto) {
return this.foodsService.searchAcrossRestaurants(dto);
}
@Get('public/foods/:foodId')
@UseGuards(OptionalAuthGuard)
@ApiHeader(API_HEADER_SLUG)
@@ -0,0 +1,42 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
class FoodSearchRestaurantDto {
@ApiProperty({ example: 'zhivan', description: 'Restaurant slug' })
slug!: string;
@ApiProperty({ example: 'کافه ژیوان', description: 'Restaurant name' })
name!: string;
@ApiPropertyOptional({ example: 'https://example.com/logo.png', description: 'Restaurant logo URL' })
logo?: string;
}
export class PublicFoodSearchItemDto {
@ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000', description: 'Food ID' })
id!: string;
@ApiPropertyOptional({ example: 'پیتزا مخصوص', description: 'Food title' })
title?: string;
@ApiPropertyOptional({ example: 'پیتزا با پنیر و قارچ', description: 'Food description' })
desc?: string;
@ApiPropertyOptional({ example: 250000, description: 'Food price' })
price?: number;
@ApiPropertyOptional({
example: ['https://example.com/food.jpg'],
description: 'Food image URLs',
type: [String],
})
images?: string[];
@ApiProperty({ example: 0, description: 'Discount percentage' })
discount!: number;
@ApiProperty({ example: false, description: 'Whether the food is a special offer' })
isSpecialOffer!: boolean;
@ApiProperty({ type: FoodSearchRestaurantDto, description: 'Restaurant that serves this food' })
restaurant!: FoodSearchRestaurantDto;
}
+25
View File
@@ -0,0 +1,25 @@
import { IsNotEmpty, IsNumber, IsOptional, IsString, Min, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class SearchFoodsDto {
@ApiProperty({ example: 'پیتزا', description: 'Search term matched against food title and description' })
@IsString()
@IsNotEmpty()
@MinLength(2)
search!: string;
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page: number = 1;
@ApiPropertyOptional({ default: 10, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit: number = 10;
}
@@ -27,3 +27,18 @@ export interface PublicFoodListItem {
isAvailable: boolean;
unavailabilityMessage: string | null;
}
export interface PublicFoodSearchItem {
id: string;
title?: string;
desc?: string;
price?: number;
images?: string[];
discount: number;
isSpecialOffer: boolean;
restaurant: {
slug: string;
name: string;
logo?: string;
};
}
+31 -4
View File
@@ -8,7 +8,9 @@ import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
import { Food } from '../entities/food.entity';
import { CategoryMessage, FoodMessage, RestMessage, CartMessage } from 'src/common/enums/message.enum';
import { Favorite } from '../entities/favorite.entity';
import { MealType, PublicFoodListItem, FoodAvailabilityInfo } from '../interface/food.interface';
import { MealType, PublicFoodListItem, PublicFoodSearchItem, FoodAvailabilityInfo } from '../interface/food.interface';
import { SearchFoodsDto } from '../dto/search-foods.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { Inventory } from 'src/modules/inventory/entities/inventory.entity';
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service';
import { CacheService } from 'src/modules/utils/cache.service';
@@ -86,6 +88,31 @@ export class FoodService {
return this.foodRepository.findAllPaginated(restId, dto);
}
async searchAcrossRestaurants(dto: SearchFoodsDto): Promise<PaginatedResult<PublicFoodSearchItem>> {
const result = await this.foodRepository.searchActiveAcrossRestaurants(dto.search, {
page: dto.page,
limit: dto.limit,
});
return {
data: result.data.map((food): PublicFoodSearchItem => ({
id: food.id,
title: food.title,
desc: food.desc,
price: food.price,
images: food.images,
discount: food.discount,
isSpecialOffer: food.isSpecialOffer,
restaurant: {
slug: food.restaurant.slug,
name: food.restaurant.name,
logo: food.restaurant.logo,
},
})),
meta: result.meta,
};
}
/**
* Public food detail (only active foods are visible).
*/
@@ -198,7 +225,7 @@ export class FoodService {
return {
isAvailable: false,
unavailabilityMessage: `فقط در روزهای ${allowedDays} در دسترس است. امروز ${currentDayName} است.`,
unavailabilityMessage: `عدم تطابق روز`,
};
}
@@ -206,7 +233,7 @@ export class FoodService {
if (mealType === null) {
return {
isAvailable: false,
unavailabilityMessage: 'فقط در ساعات وعده‌های غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است.',
unavailabilityMessage: 'عدم تطابق ساعت',
};
}
@@ -216,7 +243,7 @@ export class FoodService {
return {
isAvailable: false,
unavailabilityMessage: `فقط برای ${allowedMealTypes} در دسترس است. زمان فعلی ${currentMealType} است.`,
unavailabilityMessage: `عدم تطابق ساعت`,
};
}
}
@@ -62,7 +62,37 @@ export class FoodRepository extends EntityRepository<Food> {
totalPages,
},
};
}
async searchActiveAcrossRestaurants(
search: string,
opts: Pick<FindFoodsOpts, 'page' | 'limit'> = {},
): Promise<PaginatedResult<Food>> {
const { page = 1, limit = 10 } = opts;
const offset = (page - 1) * limit;
const pattern = `%${search}%`;
const where: FilterQuery<Food> = {
isActive: true,
restaurant: { isActive: true },
$or: [{ title: { $ilike: pattern } }, { desc: { $ilike: pattern } }],
};
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { title: 'asc' },
populate: ['restaurant'],
});
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
}
@@ -163,7 +163,7 @@ export class RestaurantsService {
async findAllActiveRestaurants(): Promise<ActiveRestaurantListItemDto[]> {
const restaurants = await this.restRepository.findAllActive();
return restaurants.map(({ slug, name, seoTitle, address, score, logo, establishedYear }) => ({
return restaurants.map(({ slug, name, seoTitle, address, logo, establishedYear }) => ({
slug,
name,
title: seoTitle,