feat(api): .NET 10 multi-tenant REST API
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>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using Meezi.API.Models.Orders;
|
||||
using Meezi.API.Models.Tables;
|
||||
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 BranchTableTests
|
||||
{
|
||||
private sealed class NullKdsNotifier : IKdsNotifier
|
||||
{
|
||||
public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) =>
|
||||
Task.CompletedTask;
|
||||
public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) =>
|
||||
Task.CompletedTask;
|
||||
public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static (AppDbContext Db, TableService Tables, string CafeId, string BranchA, string BranchB, string ManagerId)
|
||||
CreateFixture()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var db = new AppDbContext(options);
|
||||
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build();
|
||||
var tables = new TableService(db, config, new NullKdsNotifier(), new BranchIdentityService(db));
|
||||
|
||||
var cafeId = "cafe-1";
|
||||
var branchA = "branch-a";
|
||||
var branchB = "branch-b";
|
||||
var managerId = "mgr-1";
|
||||
|
||||
db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test", PlanTier = PlanTier.Pro });
|
||||
db.Branches.AddRange(
|
||||
new Branch { Id = branchA, CafeId = cafeId, Name = "A", IsActive = true, UpdatedAt = DateTime.UtcNow },
|
||||
new Branch { Id = branchB, CafeId = cafeId, Name = "B", IsActive = true, UpdatedAt = DateTime.UtcNow });
|
||||
db.Employees.Add(new Employee
|
||||
{
|
||||
Id = managerId,
|
||||
CafeId = cafeId,
|
||||
BranchId = branchA,
|
||||
Name = "Manager",
|
||||
Phone = "09121111111",
|
||||
Role = EmployeeRole.Manager
|
||||
});
|
||||
db.Tables.AddRange(
|
||||
new Table { Id = "t-a", CafeId = cafeId, BranchId = branchA, Number = "1", QrCode = "qr-a" },
|
||||
new Table { Id = "t-b", CafeId = cafeId, BranchId = branchB, Number = "2", QrCode = "qr-b" });
|
||||
db.SaveChanges();
|
||||
|
||||
return (db, tables, cafeId, branchA, branchB, managerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTables_ReturnsBranchTablesOnly()
|
||||
{
|
||||
var (_, tables, cafeId, branchA, _, _) = CreateFixture();
|
||||
var result = await tables.GetBranchTablesAsync(cafeId, branchA);
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.Equal("t-a", result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTables_DoesNotReturnOtherBranchTables()
|
||||
{
|
||||
var (_, tables, cafeId, branchA, _, _) = CreateFixture();
|
||||
var result = await tables.GetBranchTablesAsync(cafeId, branchA);
|
||||
Assert.NotNull(result);
|
||||
Assert.DoesNotContain(result, t => t.Id == "t-b");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTable_AssignsToBranch()
|
||||
{
|
||||
var (db, tables, cafeId, branchA, _, _) = CreateFixture();
|
||||
var created = await tables.CreateBranchTableAsync(
|
||||
cafeId,
|
||||
branchA,
|
||||
new CreateBranchTableRequest("9", 6));
|
||||
Assert.True(created.Success);
|
||||
Assert.Equal(branchA, created.Data!.BranchId);
|
||||
Assert.Equal(1, await db.Tables.CountAsync(t => t.BranchId == branchA && t.Number == "9"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTable_WithOpenOrder_ReturnsTableHasOpenOrder()
|
||||
{
|
||||
var (db, tables, cafeId, branchA, _, _) = CreateFixture();
|
||||
db.Orders.Add(new Order
|
||||
{
|
||||
Id = "ord-1",
|
||||
CafeId = cafeId,
|
||||
BranchId = branchA,
|
||||
TableId = "t-a",
|
||||
Status = OrderStatus.Preparing,
|
||||
OrderType = OrderType.DineIn,
|
||||
Subtotal = 100,
|
||||
Total = 100
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var result = await tables.DeleteBranchTableAsync(cafeId, branchA, "t-a");
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal("TABLE_HAS_OPEN_ORDER", result.ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSection_WithTables_ReturnsTableSectionHasTables()
|
||||
{
|
||||
var (db, tables, cafeId, branchA, _, _) = CreateFixture();
|
||||
var section = new TableSection
|
||||
{
|
||||
Id = "sec-1",
|
||||
CafeId = cafeId,
|
||||
BranchId = branchA,
|
||||
Name = "Hall"
|
||||
};
|
||||
db.TableSections.Add(section);
|
||||
var table = await db.Tables.FirstAsync(t => t.Id == "t-a");
|
||||
table.SectionId = section.Id;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var result = await tables.DeleteBranchSectionAsync(cafeId, branchA, section.Id);
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal("TABLE_SECTION_HAS_TABLES", result.ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ManagerCannotAccessOtherBranchTables()
|
||||
{
|
||||
var (_, tables, cafeId, _, branchB, managerId) = CreateFixture();
|
||||
var allowed = await tables.CanAccessBranchAsync(
|
||||
cafeId, branchB, managerId, EmployeeRole.Manager);
|
||||
Assert.False(allowed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user