using FluentValidation; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Models.Menu; using Meezi.API.Services; using Meezi.Core.Authorization; using Meezi.Core.Constants; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using Meezi.Infrastructure.Services.Platform; using Meezi.Shared; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/menu")] public class MenuController : CafeApiControllerBase { private readonly IMenuService _menuService; private readonly IMenuAi3dGenerationService _menuAi3d; private readonly IValidator _createCategoryValidator; private readonly IValidator _createItemValidator; private readonly AppDbContext _db; private readonly IPlatformCatalogService _catalog; private const string CategoryLimitMessage = "به سقف دسته‌بندی منوی پلن شما رسیدید. برای افزودن دسته‌بندی بیشتر، پلن خود را ارتقا دهید."; private const string ItemLimitMessage = "به سقف آیتم منوی پلن شما رسیدید. برای افزودن آیتم بیشتر، پلن خود را ارتقا دهید."; public MenuController( IMenuService menuService, IMenuAi3dGenerationService menuAi3d, IValidator createCategoryValidator, IValidator createItemValidator, AppDbContext db, IPlatformCatalogService catalog) { _menuService = menuService; _menuAi3d = menuAi3d; _createCategoryValidator = createCategoryValidator; _createItemValidator = createItemValidator; _db = db; _catalog = catalog; } [HttpGet("categories")] public async Task GetCategories(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; var data = await _menuService.GetCategoriesAsync(cafeId, cancellationToken); return Ok(new ApiResponse>(true, data)); } [HttpPost("categories")] public async Task CreateCategory( string cafeId, [FromBody] CreateMenuCategoryRequest request, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.CreateMenuItem) is { } permDenied) return permDenied; var validation = await _createCategoryValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var tier = tenant.PlanTier ?? PlanTier.Free; var max = (await _catalog.GetLimitsAsync(tier, cancellationToken)).MaxMenuCategories; if (max != int.MaxValue) { var count = await _db.MenuCategories.CountAsync( c => c.CafeId == cafeId && c.DeletedAt == null, cancellationToken); if (count >= max) return StatusCode(403, new ApiResponse(false, null, new ApiError("PLAN_LIMIT_REACHED", CategoryLimitMessage))); } var data = await _menuService.CreateCategoryAsync(cafeId, request, cancellationToken); return Ok(new ApiResponse(true, data)); } [HttpPatch("categories/{id}")] public async Task UpdateCategory( string cafeId, string id, [FromBody] UpdateMenuCategoryRequest request, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var data = await _menuService.UpdateCategoryAsync(cafeId, id, request, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); } [HttpDelete("categories/{id}")] public async Task DeleteCategory(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.DeleteMenuItem) is { } permDenied) return permDenied; var deleted = await _menuService.DeleteCategoryAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); } [HttpGet("items")] public async Task GetItems( string cafeId, [FromQuery] string? categoryId, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; var data = await _menuService.GetItemsAsync(cafeId, categoryId, cancellationToken); return Ok(new ApiResponse>(true, data)); } [HttpPost("items")] public async Task CreateItem( string cafeId, [FromBody] CreateMenuItemRequest request, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.CreateMenuItem) is { } permDenied) return permDenied; var validation = await _createItemValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var tier = tenant.PlanTier ?? PlanTier.Free; var max = (await _catalog.GetLimitsAsync(tier, cancellationToken)).MaxMenuItems; if (max != int.MaxValue) { var count = await _db.MenuItems.CountAsync( i => i.CafeId == cafeId && i.DeletedAt == null, cancellationToken); if (count >= max) return StatusCode(403, new ApiResponse(false, null, new ApiError("PLAN_LIMIT_REACHED", ItemLimitMessage))); } var data = await _menuService.CreateItemAsync(cafeId, request, cancellationToken); if (data is null) return NotFoundError("Category not found."); return Ok(new ApiResponse(true, data)); } [HttpPatch("items/{id}")] public async Task UpdateItem( string cafeId, string id, [FromBody] UpdateMenuItemRequest request, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var data = await _menuService.UpdateItemAsync(cafeId, id, request, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); } [HttpPatch("items/{id}/availability")] public async Task SetAvailability( string cafeId, string id, [FromBody] UpdateMenuItemAvailabilityRequest request, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var data = await _menuService.SetAvailabilityAsync(cafeId, id, request.IsAvailable, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); } [HttpDelete("items/{id}")] public async Task DeleteItem(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.DeleteMenuItem) is { } permDenied) return permDenied; var deleted = await _menuService.DeleteItemAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); } [HttpGet("ai-3d/usage")] public async Task GetAi3dUsage(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; var tier = tenant.PlanTier ?? PlanTier.Free; var data = await _menuAi3d.GetUsageAsync(cafeId, tier, cancellationToken); return Ok(new ApiResponse(true, data)); } [HttpPost("items/{id}/ai-3d")] public async Task GenerateAi3d( string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var tier = tenant.PlanTier ?? PlanTier.Free; var (data, code, message) = await _menuAi3d.GenerateFromItemImageAsync(cafeId, id, tier, cancellationToken); if (code is not null) { return code switch { "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message ?? ""))), "PLAN_FEATURE_DISABLED" => StatusCode( StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError(code, message ?? ""))), "PLAN_LIMIT_REACHED" => StatusCode( StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError(code, message ?? ""))), _ => BadRequest(new ApiResponse(false, null, new ApiError(code, message ?? ""))) }; } return Ok(new ApiResponse(true, data)); } }