Files
meezi/tests/Meezi.API.Tests/OrderVoidTransferTests.cs
soroush.asadi ef15fd6247 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>
2026-05-27 21:33:48 +03:30

213 lines
8.5 KiB
C#

using Meezi.API.Models.Orders;
using Meezi.API.Models.Shifts;
using Meezi.API.Services;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using TenantContext = Meezi.Infrastructure.Data.TenantContext;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace Meezi.API.Tests;
public class OrderVoidTransferTests
{
private static (
AppDbContext Db,
OrderService Orders,
string CafeId,
string Table1Id,
string Table2Id,
string MenuItemId,
TenantContext Tenant)
CreateFixture()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var db = new AppDbContext(options);
var cafeId = "cafe-void";
var table1 = "table-1";
var table2 = "table-2";
var menuItemId = "item-1";
var categoryId = "cat-1";
var managerId = "emp-manager";
db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test-void", PlanTier = PlanTier.Pro });
db.Tables.Add(new Table { Id = table1, CafeId = cafeId, Number = "1", QrCode = "qr1", Capacity = 4 });
db.Tables.Add(new Table { Id = table2, CafeId = cafeId, Number = "2", QrCode = "qr2", Capacity = 4 });
db.MenuCategories.Add(new MenuCategory
{
Id = categoryId,
CafeId = cafeId,
Name = "Drinks",
NameEn = "Drinks",
SortOrder = 0
});
db.MenuItems.Add(new MenuItem
{
Id = menuItemId,
CafeId = cafeId,
CategoryId = categoryId,
Name = "Tea",
NameEn = "Tea",
Price = 50_000m,
IsAvailable = true
});
db.Employees.Add(new Employee
{
Id = managerId,
CafeId = cafeId,
Name = "Manager",
Phone = "09120000000",
Role = EmployeeRole.Manager
});
db.SaveChanges();
var tenant = new TenantContext
{
CafeId = cafeId,
UserId = managerId,
Role = EmployeeRole.Manager,
PlanTier = PlanTier.Pro,
Language = "fa"
};
var orders = new OrderService(db, new NoOpKdsNotifier(), new NoOpSnappfood(), new NoOpDeliverySync(), new NoOpShiftService(), TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService());
return (db, orders, cafeId, table1, table2, menuItemId, tenant);
}
private static async Task<OrderDto> CreateOpenOrderAsync(
OrderService orders,
string cafeId,
TenantContext tenant,
string tableId,
string menuItemId,
int qty = 1)
{
var result = await orders.CreateOrderAsync(
cafeId,
tenant,
new CreateOrderRequest(
OrderType.DineIn,
null,
tableId,
null,
"Guest",
null,
null,
null,
[new CreateOrderItemRequest(menuItemId, qty, null)]));
Assert.True(result.Success);
return result.Data!;
}
[Fact]
public async Task VoidItem_ReducesOrderTotal()
{
var (_, orders, cafeId, table1, _, menuItemId, tenant) = CreateFixture();
var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId, 2);
var itemId = order.Items[0].Id;
var before = order.Total;
var result = await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!);
Assert.True(result.Success);
Assert.True(result.Data!.Items.First(i => i.Id == itemId).IsVoided);
Assert.True(result.Data.Total < before);
}
[Fact]
public async Task VoidItem_AlreadyVoided_ReturnsError()
{
var (_, orders, cafeId, table1, _, menuItemId, tenant) = CreateFixture();
var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId);
var itemId = order.Items[0].Id;
await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!);
var second = await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!);
Assert.False(second.Success);
Assert.Equal("ITEM_ALREADY_VOIDED", second.ErrorCode);
}
[Fact]
public async Task TransferTable_MovesOrderToFreeTable()
{
var (_, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture();
var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId);
var result = await orders.TransferTableAsync(cafeId, order.Id, table2);
Assert.True(result.Success);
Assert.Equal(table2, result.Data!.TableId);
}
[Fact]
public async Task TransferTable_ToOccupiedTable_ReturnsTableOccupied()
{
var (_, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture();
await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId);
var order2 = await CreateOpenOrderAsync(orders, cafeId, tenant, table2, menuItemId);
var result = await orders.TransferTableAsync(cafeId, order2.Id, table1);
Assert.False(result.Success);
Assert.Equal("TABLE_OCCUPIED", result.ErrorCode);
}
[Fact]
public async Task TransferTable_ToCleaningTable_ReturnsTableCleaning()
{
var (db, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture();
var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId);
var table = await db.Tables.FirstAsync(t => t.Id == table2);
table.IsCleaning = true;
await db.SaveChangesAsync();
var result = await orders.TransferTableAsync(cafeId, order.Id, table2);
Assert.False(result.Success);
Assert.Equal("TABLE_CLEANING", result.ErrorCode);
}
private sealed class NoOpKdsNotifier : 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 sealed class NoOpSnappfood : ISnappfoodClient
{
public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
}
private sealed class NoOpShiftService : IShiftService
{
public Task<ShiftServiceResult<ShiftDto>> OpenShiftAsync(string cafeId, string branchId, decimal openingCash, string userId, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<ShiftServiceResult<ShiftDto>> CloseShiftAsync(string cafeId, string shiftId, decimal closingCash, string userId, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<ShiftDto?> GetCurrentShiftAsync(string cafeId, string branchId, CancellationToken cancellationToken = default) =>
Task.FromResult<ShiftDto?>(null);
public Task<IReadOnlyList<CashTransactionDto>?> GetTransactionsAsync(string cafeId, string shiftId, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<ShiftServiceResult<CashTransactionDto>> RecordTransactionAsync(string cafeId, string shiftId, CashTransactionType type, PaymentMethod method, decimal amount, string createdByUserId, string? referenceId = null, string? note = null, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task<ShiftServiceResult<Shift>> RequireOpenShiftForBranchAsync(string cafeId, string branchId, CancellationToken cancellationToken = default) =>
Task.FromResult(new ShiftServiceResult<Shift>(true, new Shift { Id = "shift-test", CafeId = cafeId, BranchId = branchId }));
}
}