using Meezi.API.Models.Menu; using Meezi.API.Models.Orders; using Meezi.API.Models.Public; using Meezi.API.Security; using Meezi.API.Services; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Xunit; using LiveOrderDto = Meezi.API.Models.Orders.LiveOrderDto; namespace Meezi.API.Tests; public class QrMenuTests { private sealed class NoOpKds : IKdsNotifier { public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken ct = default) => Task.CompletedTask; public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken ct = default) => Task.CompletedTask; public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken ct = default) => Task.CompletedTask; } private static ( AppDbContext Db, TableService Tables, PublicService Public, string CafeId, string BranchId, string TableId, string ItemA, string ItemB, string QrCode ) CreateFixture() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new AppDbContext(options); var cafeId = "cafe-qr"; var branchId = "branch-qr"; var catId = "cat-qr"; var itemA = "item-qr-a"; var itemB = "item-qr-b"; var tableId = "table-qr-1"; const string qrCode = "demo_table_01"; db.Cafes.Add(new Cafe { Id = cafeId, Name = "Demo", Slug = "demo", PlanTier = PlanTier.Pro, DefaultTaxRate = 9m }); db.Branches.Add(new Branch { Id = branchId, CafeId = cafeId, Name = "Main", IsActive = true, WelcomeText = "خوش آمدید", UpdatedAt = DateTime.UtcNow }); db.MenuCategories.Add(new MenuCategory { Id = catId, CafeId = cafeId, Name = "Drinks", SortOrder = 0, IsActive = true }); db.MenuItems.AddRange( new MenuItem { Id = itemA, CafeId = cafeId, CategoryId = catId, Name = "Espresso", Price = 100_000m, IsAvailable = true }, new MenuItem { Id = itemB, CafeId = cafeId, CategoryId = catId, Name = "Latte", Price = 150_000m, IsAvailable = true }); db.Tables.Add(new Table { Id = tableId, CafeId = cafeId, BranchId = branchId, Number = "1", QrCode = qrCode, IsActive = true }); db.SaveChanges(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["App:QrPublicBaseUrl"] = "https://meezi.test" }) .Build(); var kds = new NoOpKds(); var branchMenu = new BranchMenuService(db); var identity = new BranchIdentityService(db); var tables = new TableService(db, config, kds, identity); var shifts = new ShiftService(db); var orders = new OrderService(db, kds, new NoOpSnappfood(), new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); var abuse = new NoOpAbuseProtectionService(); var http = new HttpContextAccessor(); var media = new NoOpMediaStorageService(); var reviews = new ReviewService(db, abuse, http, media); var publicSvc = new PublicService(db, orders, reviews, kds, branchMenu, identity, abuse, http); return (db, tables, publicSvc, cafeId, branchId, tableId, itemA, itemB, qrCode); } [Fact] public async Task ResolveQr_ValidCode_ReturnsBranchInfo() { var (_, tables, _, _, branchId, tableId, _, _, qrCode) = CreateFixture(); var result = await tables.ResolveQrAsync(qrCode); Assert.NotNull(result); Assert.Equal(branchId, result!.BranchId); Assert.Equal(tableId, result.TableId); Assert.False(result.IsCleaning); } [Fact] public async Task ResolveQr_InvalidCode_ReturnsNull() { var (_, tables, _, _, _, _, _, _, _) = CreateFixture(); Assert.Null(await tables.ResolveQrAsync("missing_code")); } [Fact] public async Task GetBranchMenu_ExcludesUnavailableItems() { var (db, _, pub, cafeId, branchId, _, itemA, itemB, _) = CreateFixture(); var menuSvc = new BranchMenuService(db); await menuSvc.UpsertOverrideAsync( cafeId, branchId, itemB, new UpsertBranchMenuOverrideRequest(false, null), PlanTier.Pro, EmployeeRole.Manager, "u1"); var menu = await pub.GetBranchMenuAsync(cafeId, branchId); var ids = menu!.Categories.SelectMany(c => c.Items).Select(i => i.Id).ToList(); Assert.Contains(itemA, ids); Assert.DoesNotContain(itemB, ids); } [Fact] public async Task GetBranchMenu_AppliesBranchPriceOverride() { var (db, _, pub, cafeId, branchId, _, itemA, _, _) = CreateFixture(); await new BranchMenuService(db).UpsertOverrideAsync( cafeId, branchId, itemA, new UpsertBranchMenuOverrideRequest(true, 88_000m), PlanTier.Pro, EmployeeRole.Manager, "u1"); var item = (await pub.GetBranchMenuAsync(cafeId, branchId))! .Categories.SelectMany(c => c.Items) .First(i => i.Id == itemA); Assert.Equal(88_000m, item.Price); } [Fact] public async Task PlaceGuestOrder_ValidItems_CreatesOrderWithGuestQrSource() { var (db, _, pub, cafeId, branchId, tableId, itemA, _, _) = CreateFixture(); var (data, code, _) = await pub.PlaceBranchGuestOrderAsync( cafeId, branchId, new PlaceGuestOrderRequest(tableId, "Guest", null, [new CreateOrderItemRequest(itemA, 2, null)])); Assert.Null(code); Assert.NotNull(data); var order = await db.Orders.Include(o => o.Items).FirstAsync(o => o.Id == data!.OrderId); Assert.Equal(OrderSource.GuestQr, order.Source); Assert.Equal(2, order.Items.Sum(i => i.Quantity)); } [Fact] public async Task PlaceGuestOrder_MergesWithExistingOpenOrder() { var (db, _, pub, cafeId, branchId, tableId, itemA, _, _) = CreateFixture(); await pub.PlaceBranchGuestOrderAsync( cafeId, branchId, new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest(itemA, 1, null)])); var (second, code, _) = await pub.PlaceBranchGuestOrderAsync( cafeId, branchId, new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest(itemA, 1, null)])); Assert.Null(code); Assert.Equal(1, await db.Orders.CountAsync(o => o.TableId == tableId)); var order = await db.Orders.Include(o => o.Items).FirstAsync(o => o.Id == second!.OrderId); Assert.Equal(2, order.Items.Sum(i => i.Quantity)); } [Fact] public async Task PlaceGuestOrder_CleaningTable_ReturnsTableCleaning() { var (db, _, pub, cafeId, branchId, tableId, itemA, _, _) = CreateFixture(); var table = await db.Tables.FirstAsync(t => t.Id == tableId); table.IsCleaning = true; await db.SaveChangesAsync(); var (_, code, _) = await pub.PlaceBranchGuestOrderAsync( cafeId, branchId, new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest(itemA, 1, null)])); Assert.Equal("TABLE_CLEANING", code); } [Fact] public async Task PlaceGuestOrder_InvalidMenuItem_ReturnsInvalidMenuItems() { var (_, _, pub, cafeId, branchId, tableId, _, _, _) = CreateFixture(); var (_, code, _) = await pub.PlaceBranchGuestOrderAsync( cafeId, branchId, new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest("missing", 1, null)])); Assert.Equal("INVALID_MENU_ITEMS", code); } }