Files
meezi/tests/Meezi.API.Tests/QrMenuTests.cs
T
soroush.asadi 7d06f149d3 feat(plans): menu watermark on Free (removed by paid feature)
Guest QR menu shows a "ساخته‌شده با میزی" watermark under the menu unless the café's
plan has the `watermark_removed` feature (Starter+).

- PublicMenuDto gains ShowWatermark; PublicService computes it from
  IsFeatureEnabledForCafeAsync("watermark_removed") for both slug and branch menus.
- Guest menu renders the watermark footer when showWatermark.
- NoOpPlatformCatalogService test double (all features on) for the PublicService
  ctor; QrMenuTests updated.

86 tests pass; dashboard tsc clean.
2026-06-03 02:10:24 +03:30

237 lines
8.5 KiB
C#

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<AppDbContext>()
.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<string, string?> { ["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, new NoOpPlatformCatalogService());
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);
}
}