Files
meezi/src/Meezi.API/Services/MenuService.cs
T
soroush.asadi 27b3ac60c7
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m28s
feat(menu): per-item print station (cold bar / kitchen / barista)
Each menu item can now pick its own print station, overriding the category's —
so a category can fan out to different printers (e.g. a drink → cold bar, a
food → kitchen). Adds MenuItem.KitchenStationId (+ migration, FK SetNull), wires
create/update/DTO, and updates kitchen-ticket routing to group by the item's
station ?? the category's station ?? the branch kitchen printer. Deleting a
station now also clears item assignments. Menu item editor gains a "Print
station" dropdown (default = "same as category"). fa/en/ar added.

Backend built clean via the Nexus mirror; migration applies on deploy (MigrateAsync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:08:07 +03:30

245 lines
11 KiB
C#

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);
Task<bool> DeleteItemAsync(string cafeId, string id, CancellationToken cancellationToken = default);
}
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),
IsAvailable = request.IsAvailable,
KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) ? null : request.KitchenStationId,
};
_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;
if (request.KitchenStationId is not null)
entity.KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) ? null : request.KitchenStationId;
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);
}
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;
}
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,
i.IsAvailable,
i.KitchenStationId);
}