2026-05-27 21:33:48 +03:30
|
|
|
using Meezi.API.Models.Menu;
|
|
|
|
|
using Meezi.Core.Constants;
|
|
|
|
|
using Meezi.Core.Entities;
|
|
|
|
|
using Meezi.Infrastructure.Data;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
|
|
namespace Meezi.API.Services;
|
|
|
|
|
|
|
|
|
|
public interface IMenuService
|
|
|
|
|
{
|
|
|
|
|
Task<IReadOnlyList<MenuCategoryDto>> GetCategoriesAsync(string cafeId, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<MenuCategoryDto?> CreateCategoryAsync(string cafeId, CreateMenuCategoryRequest request, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<MenuCategoryDto?> UpdateCategoryAsync(string cafeId, string id, UpdateMenuCategoryRequest request, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<bool> DeleteCategoryAsync(string cafeId, string id, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<IReadOnlyList<MenuItemDto>> GetItemsAsync(string cafeId, string? categoryId, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<MenuItemDto?> CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<MenuItemDto?> UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default);
|
|
|
|
|
Task<MenuItemDto?> SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default);
|
2026-06-02 12:24:09 +03:30
|
|
|
Task<bool> DeleteItemAsync(string cafeId, string id, CancellationToken cancellationToken = default);
|
2026-05-27 21:33:48 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class MenuService : IMenuService
|
|
|
|
|
{
|
|
|
|
|
private readonly AppDbContext _db;
|
|
|
|
|
|
|
|
|
|
public MenuService(AppDbContext db)
|
|
|
|
|
{
|
|
|
|
|
_db = db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<MenuCategoryDto>> GetCategoriesAsync(string cafeId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
return await _db.MenuCategories
|
|
|
|
|
.Where(c => c.CafeId == cafeId)
|
|
|
|
|
.OrderBy(c => c.SortOrder)
|
|
|
|
|
.Select(c => ToCategoryDto(c))
|
|
|
|
|
.ToListAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<MenuCategoryDto?> CreateCategoryAsync(string cafeId, CreateMenuCategoryRequest request, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var entity = new MenuCategory
|
|
|
|
|
{
|
|
|
|
|
CafeId = cafeId,
|
|
|
|
|
Name = request.Name,
|
|
|
|
|
NameAr = request.NameAr,
|
|
|
|
|
NameEn = request.NameEn,
|
|
|
|
|
SortOrder = request.SortOrder,
|
|
|
|
|
TaxId = request.TaxId,
|
|
|
|
|
DiscountPercent = request.DiscountPercent,
|
|
|
|
|
Icon = NormalizeOptionalText(request.Icon),
|
|
|
|
|
IconPresetId = NormalizeIconPreset(request.IconPresetId),
|
|
|
|
|
IconStyle = NormalizeIconStyle(request.IconStyle)
|
|
|
|
|
?? (NormalizeIconPreset(request.IconPresetId) is not null
|
|
|
|
|
? CategoryIconPresets.IconStyle.Flat
|
|
|
|
|
: null),
|
|
|
|
|
ImageUrl = NormalizeOptionalText(request.ImageUrl),
|
|
|
|
|
IsActive = request.IsActive,
|
|
|
|
|
KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId)
|
|
|
|
|
? null
|
|
|
|
|
: request.KitchenStationId
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_db.MenuCategories.Add(entity);
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return ToCategoryDto(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<MenuCategoryDto?> UpdateCategoryAsync(string cafeId, string id, UpdateMenuCategoryRequest request, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var entity = await _db.MenuCategories.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (entity is null) return null;
|
|
|
|
|
|
|
|
|
|
if (request.Name is not null) entity.Name = request.Name;
|
|
|
|
|
if (request.NameAr is not null) entity.NameAr = request.NameAr;
|
|
|
|
|
if (request.NameEn is not null) entity.NameEn = request.NameEn;
|
|
|
|
|
if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value;
|
|
|
|
|
if (request.TaxId is not null) entity.TaxId = request.TaxId;
|
|
|
|
|
if (request.DiscountPercent.HasValue) entity.DiscountPercent = request.DiscountPercent.Value;
|
|
|
|
|
if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;
|
|
|
|
|
if (request.Icon is not null)
|
|
|
|
|
entity.Icon = NormalizeOptionalText(request.Icon);
|
|
|
|
|
if (request.IconPresetId is not null)
|
|
|
|
|
entity.IconPresetId = NormalizeIconPreset(request.IconPresetId);
|
|
|
|
|
if (request.IconStyle is not null)
|
|
|
|
|
entity.IconStyle = NormalizeIconStyle(request.IconStyle);
|
|
|
|
|
if (request.ImageUrl is not null)
|
|
|
|
|
entity.ImageUrl = NormalizeOptionalText(request.ImageUrl);
|
|
|
|
|
if (request.KitchenStationId is not null)
|
|
|
|
|
entity.KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId)
|
|
|
|
|
? null
|
|
|
|
|
: request.KitchenStationId;
|
|
|
|
|
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return ToCategoryDto(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> DeleteCategoryAsync(string cafeId, string id, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var entity = await _db.MenuCategories.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (entity is null) return false;
|
|
|
|
|
|
|
|
|
|
entity.DeletedAt = DateTime.UtcNow;
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<MenuItemDto>> GetItemsAsync(string cafeId, string? categoryId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var query = _db.MenuItems.Where(i => i.CafeId == cafeId);
|
|
|
|
|
if (!string.IsNullOrEmpty(categoryId))
|
|
|
|
|
query = query.Where(i => i.CategoryId == categoryId);
|
|
|
|
|
|
|
|
|
|
var items = await query.Include(i => i.Category).OrderBy(i => i.Name).ToListAsync(cancellationToken);
|
|
|
|
|
return items.Select(ToItemDto).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<MenuItemDto?> CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var category = await _db.MenuCategories
|
|
|
|
|
.FirstOrDefaultAsync(c => c.Id == request.CategoryId && c.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (category is null) return null;
|
|
|
|
|
|
|
|
|
|
var imageUrl = string.IsNullOrWhiteSpace(request.ImageUrl)
|
|
|
|
|
? MenuItemImageDefaults.GetDefaultImageUrl(
|
|
|
|
|
MenuItemImageDefaults.InferKind(request.CategoryId, category.Name))
|
|
|
|
|
: request.ImageUrl;
|
|
|
|
|
|
|
|
|
|
var entity = new MenuItem
|
|
|
|
|
{
|
|
|
|
|
CafeId = cafeId,
|
|
|
|
|
CategoryId = request.CategoryId,
|
|
|
|
|
Name = request.Name,
|
|
|
|
|
NameAr = request.NameAr,
|
|
|
|
|
NameEn = request.NameEn,
|
|
|
|
|
Description = request.Description,
|
|
|
|
|
Price = request.Price,
|
|
|
|
|
DiscountPercent = request.DiscountPercent,
|
|
|
|
|
ImageUrl = imageUrl,
|
|
|
|
|
VideoUrl = request.VideoUrl,
|
|
|
|
|
Model3dUrl = NormalizeOptionalText(request.Model3dUrl),
|
2026-06-25 10:08:07 +03:30
|
|
|
IsAvailable = request.IsAvailable,
|
|
|
|
|
KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) ? null : request.KitchenStationId,
|
2026-05-27 21:33:48 +03:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_db.MenuItems.Add(entity);
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return ToItemDto(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<MenuItemDto?> UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var entity = await _db.MenuItems
|
|
|
|
|
.Include(i => i.Category)
|
|
|
|
|
.FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (entity is null) return null;
|
|
|
|
|
|
|
|
|
|
if (request.CategoryId is not null)
|
|
|
|
|
{
|
|
|
|
|
var categoryExists = await _db.MenuCategories.AnyAsync(c => c.Id == request.CategoryId && c.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (!categoryExists) return null;
|
|
|
|
|
entity.CategoryId = request.CategoryId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (request.Name is not null) entity.Name = request.Name;
|
|
|
|
|
if (request.NameAr is not null) entity.NameAr = request.NameAr;
|
|
|
|
|
if (request.NameEn is not null) entity.NameEn = request.NameEn;
|
|
|
|
|
if (request.Description is not null) entity.Description = request.Description;
|
|
|
|
|
if (request.Price.HasValue) entity.Price = request.Price.Value;
|
|
|
|
|
if (request.DiscountPercent.HasValue) entity.DiscountPercent = request.DiscountPercent.Value;
|
|
|
|
|
if (request.ImageUrl is not null)
|
|
|
|
|
{
|
|
|
|
|
entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl)
|
|
|
|
|
? MenuItemImageDefaults.ResolveImageUrl(entity.Id, entity.CategoryId, entity.Category?.Name)
|
|
|
|
|
: request.ImageUrl;
|
|
|
|
|
}
|
|
|
|
|
if (request.VideoUrl is not null)
|
|
|
|
|
entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl;
|
|
|
|
|
if (request.Model3dUrl is not null)
|
|
|
|
|
entity.Model3dUrl = string.IsNullOrWhiteSpace(request.Model3dUrl) ? null : request.Model3dUrl.Trim();
|
|
|
|
|
if (request.IsAvailable.HasValue) entity.IsAvailable = request.IsAvailable.Value;
|
2026-06-25 10:08:07 +03:30
|
|
|
if (request.KitchenStationId is not null)
|
|
|
|
|
entity.KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) ? null : request.KitchenStationId;
|
2026-05-27 21:33:48 +03:30
|
|
|
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return ToItemDto(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<MenuItemDto?> SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var entity = await _db.MenuItems.FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (entity is null) return null;
|
|
|
|
|
|
|
|
|
|
entity.IsAvailable = isAvailable;
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return ToItemDto(entity);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:24:09 +03:30
|
|
|
public async Task<bool> DeleteItemAsync(string cafeId, string id, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var entity = await _db.MenuItems.FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (entity is null) return false;
|
|
|
|
|
|
|
|
|
|
entity.DeletedAt = DateTime.UtcNow;
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 21:33:48 +03:30
|
|
|
private static string? NormalizeOptionalText(string? value) =>
|
|
|
|
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
|
|
|
|
|
|
|
|
private static string? NormalizeIconPreset(string? value)
|
|
|
|
|
{
|
|
|
|
|
var normalized = CategoryIconPresets.NormalizePreset(value);
|
|
|
|
|
if (normalized is null) return null;
|
|
|
|
|
return CategoryIconPresets.IsValidPreset(normalized) ? normalized : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string? NormalizeIconStyle(string? value)
|
|
|
|
|
{
|
|
|
|
|
var normalized = CategoryIconPresets.NormalizeStyle(value);
|
|
|
|
|
if (normalized is null) return null;
|
|
|
|
|
return CategoryIconPresets.IsValidStyle(normalized) ? normalized : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static MenuCategoryDto ToCategoryDto(MenuCategory c) => new(
|
|
|
|
|
c.Id, c.Name, c.NameAr, c.NameEn, c.SortOrder, c.TaxId, c.DiscountPercent,
|
|
|
|
|
c.Icon, c.IconPresetId, c.IconStyle, c.ImageUrl, c.IsActive, c.KitchenStationId);
|
|
|
|
|
|
|
|
|
|
private static MenuItemDto ToItemDto(MenuItem i) => new(
|
|
|
|
|
i.Id,
|
|
|
|
|
i.CategoryId,
|
|
|
|
|
i.Name,
|
|
|
|
|
i.NameAr,
|
|
|
|
|
i.NameEn,
|
|
|
|
|
i.Description,
|
|
|
|
|
i.Price,
|
|
|
|
|
i.DiscountPercent,
|
|
|
|
|
MenuItemImageDefaults.ResolveDisplayImageUrl(i),
|
|
|
|
|
i.VideoUrl,
|
|
|
|
|
i.Model3dUrl,
|
2026-06-25 10:08:07 +03:30
|
|
|
i.IsAvailable,
|
|
|
|
|
i.KitchenStationId);
|
2026-05-27 21:33:48 +03:30
|
|
|
}
|