ef15fd6247
Full backend implementation: - Multi-tenant cafe/restaurant management (menus, orders, tables, staff) - POS order flow with ZarinPal and Snappfood payment integration - OTP authentication via Kavenegar SMS - QR digital menu with public discover/finder endpoints - Customer loyalty, coupons, CRM - PostgreSQL via EF Core, Redis for caching/sessions - Background jobs, webhook handlers - Full migration history Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
190 lines
6.3 KiB
C#
190 lines
6.3 KiB
C#
using Meezi.API.Models.Menu;
|
|
using Meezi.API.Services;
|
|
using Meezi.Core.Entities;
|
|
using Meezi.Core.Enums;
|
|
using Meezi.Infrastructure.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
|
|
namespace Meezi.API.Tests;
|
|
|
|
public class BranchMenuTests
|
|
{
|
|
private static (AppDbContext Db, BranchMenuService Menu, string CafeId, string BranchId, string ItemA, string ItemB)
|
|
CreateFixture(PlanTier tier = PlanTier.Pro)
|
|
{
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var db = new AppDbContext(options);
|
|
|
|
var cafeId = "cafe-1";
|
|
var branchId = "branch-1";
|
|
var catId = "cat-1";
|
|
var itemA = "item-a";
|
|
var itemB = "item-b";
|
|
|
|
db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test", PlanTier = tier });
|
|
db.Branches.Add(new Branch
|
|
{
|
|
Id = branchId,
|
|
CafeId = cafeId,
|
|
Name = "Main",
|
|
IsActive = true,
|
|
UpdatedAt = DateTime.UtcNow
|
|
});
|
|
db.MenuCategories.Add(new MenuCategory
|
|
{
|
|
Id = catId,
|
|
CafeId = cafeId,
|
|
Name = "Drinks",
|
|
NameEn = "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.SaveChanges();
|
|
|
|
return (db, new BranchMenuService(db), cafeId, branchId, itemA, itemB);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBranchMenu_ExcludesUnavailableItems()
|
|
{
|
|
var (_, menu, cafeId, branchId, itemA, itemB) = CreateFixture();
|
|
await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemB,
|
|
new UpsertBranchMenuOverrideRequest(false, null),
|
|
PlanTier.Pro, EmployeeRole.Manager, "user-1");
|
|
|
|
var result = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Single(result!);
|
|
Assert.Equal(itemA, result![0].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBranchMenu_AppliesPriceOverride_WhenSet()
|
|
{
|
|
var (_, menu, cafeId, branchId, itemA, _) = CreateFixture();
|
|
await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemA,
|
|
new UpsertBranchMenuOverrideRequest(true, 88_000m),
|
|
PlanTier.Pro, EmployeeRole.Manager, "user-1");
|
|
|
|
var result = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false);
|
|
|
|
Assert.NotNull(result);
|
|
var row = Assert.Single(result!, r => r.Id == itemA);
|
|
Assert.Equal(100_000m, row.MasterPrice);
|
|
Assert.Equal(88_000m, row.EffectivePrice);
|
|
Assert.True(row.HasPriceOverride);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBranchMenu_UsesMasterPrice_WhenNoOverride()
|
|
{
|
|
var (_, menu, cafeId, branchId, itemA, _) = CreateFixture();
|
|
|
|
var result = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false);
|
|
|
|
var row = Assert.Single(result!, r => r.Id == itemA);
|
|
Assert.Equal(100_000m, row.MasterPrice);
|
|
Assert.Equal(100_000m, row.EffectivePrice);
|
|
Assert.False(row.IsOverridden);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpsertOverride_FreePlan_PriceOverride_ReturnsPlanLimitReached()
|
|
{
|
|
var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(PlanTier.Free);
|
|
|
|
var result = await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemA,
|
|
new UpsertBranchMenuOverrideRequest(true, 90_000m),
|
|
PlanTier.Free,
|
|
EmployeeRole.Manager,
|
|
"user-1");
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Equal("PLAN_LIMIT_REACHED", result.ErrorCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpsertOverride_ProPlan_PriceOverride_Succeeds()
|
|
{
|
|
var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(PlanTier.Pro);
|
|
|
|
var result = await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemA,
|
|
new UpsertBranchMenuOverrideRequest(true, 90_000m),
|
|
PlanTier.Pro,
|
|
EmployeeRole.Manager,
|
|
"user-1");
|
|
|
|
Assert.True(result.Success);
|
|
Assert.Equal(90_000m, result.Data!.PriceOverride);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteOverride_ResetsToMasterPrice()
|
|
{
|
|
var (_, menu, cafeId, branchId, itemA, _) = CreateFixture();
|
|
await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemA,
|
|
new UpsertBranchMenuOverrideRequest(true, 90_000m),
|
|
PlanTier.Pro, EmployeeRole.Owner, "owner-1");
|
|
|
|
var deleted = await menu.DeleteOverrideAsync(cafeId, branchId, itemA);
|
|
Assert.True(deleted);
|
|
|
|
var menuRows = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false);
|
|
var row = Assert.Single(menuRows!, r => r.Id == itemA);
|
|
Assert.Equal(100_000m, row.EffectivePrice);
|
|
Assert.False(row.IsOverridden);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Override_UniqueConstraint_UpsertNotDuplicate()
|
|
{
|
|
var (db, menu, cafeId, branchId, itemA, _) = CreateFixture();
|
|
await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemA,
|
|
new UpsertBranchMenuOverrideRequest(true, 80_000m),
|
|
PlanTier.Pro, EmployeeRole.Manager, "user-1");
|
|
await menu.UpsertOverrideAsync(
|
|
cafeId, branchId, itemA,
|
|
new UpsertBranchMenuOverrideRequest(false, 85_000m),
|
|
PlanTier.Pro, EmployeeRole.Manager, "user-1");
|
|
|
|
var count = await db.BranchMenuItemOverrides
|
|
.CountAsync(o => o.BranchId == branchId && o.MenuItemId == itemA);
|
|
Assert.Equal(1, count);
|
|
|
|
var rows = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: true);
|
|
Assert.Equal(2, rows!.Count);
|
|
var hidden = Assert.Single(rows, r => r.Id == itemA);
|
|
Assert.False(hidden.IsAvailable);
|
|
Assert.Equal(85_000m, hidden.EffectivePrice);
|
|
}
|
|
}
|