Files
meezi/tests/Meezi.API.Tests/DailyReportServiceTests.cs
T
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

204 lines
7.6 KiB
C#

using Meezi.API.Services;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Meezi.API.Tests;
public class DailyReportServiceTests
{
private static (AppDbContext Db, DailyReportService Service, string CafeId, string BranchId, string ItemId)
CreateFixture()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var db = new AppDbContext(options);
var cafeId = "cafe-report";
var branchId = "branch-1";
var branch2 = "branch-2";
var itemId = "item-espresso";
var catId = "cat-1";
var userId = "emp-1";
db.Cafes.Add(new Cafe { Id = cafeId, Name = "R", Slug = "r", PlanTier = PlanTier.Business });
db.Branches.Add(new Branch { Id = branchId, CafeId = cafeId, Name = "A", IsActive = true, UpdatedAt = DateTime.UtcNow });
db.Branches.Add(new Branch { Id = branch2, CafeId = cafeId, Name = "B", IsActive = true, UpdatedAt = DateTime.UtcNow });
db.Employees.Add(new Employee { Id = userId, CafeId = cafeId, BranchId = branchId, Name = "E", Phone = "09120000001", Role = EmployeeRole.Owner });
db.MenuCategories.Add(new MenuCategory { Id = catId, CafeId = cafeId, Name = "C", NameEn = "C", SortOrder = 0 });
db.MenuItems.Add(new MenuItem { Id = itemId, CafeId = cafeId, CategoryId = catId, Name = "Espresso", NameEn = "Espresso", Price = 50_000m, IsAvailable = true });
db.SaveChanges();
var service = new DailyReportService(db, NullLogger<DailyReportService>.Instance);
return (db, service, cafeId, branchId, itemId);
}
private static async Task SeedClosedOrderAsync(
AppDbContext db,
string cafeId,
string branchId,
string itemId,
DateTime createdAt,
bool voidFirstLine,
IReadOnlyList<(PaymentMethod Method, decimal Amount)>? payments = null,
int activeQty = 1)
{
var orderId = $"ord_{Guid.NewGuid():N}"[..16];
var activeRevenue = 50_000m * activeQty;
var order = new Order
{
Id = orderId,
CafeId = cafeId,
BranchId = branchId,
Status = OrderStatus.Delivered,
OrderType = OrderType.DineIn,
Subtotal = activeRevenue,
TaxTotal = 0,
Total = activeRevenue,
CreatedAt = createdAt
};
db.Orders.Add(order);
if (voidFirstLine)
{
db.OrderItems.Add(new OrderItem
{
OrderId = orderId,
MenuItemId = itemId,
Quantity = 2,
UnitPrice = 50_000m,
IsVoided = true,
VoidedAt = createdAt
});
}
db.OrderItems.Add(new OrderItem
{
OrderId = orderId,
MenuItemId = itemId,
Quantity = activeQty,
UnitPrice = 50_000m
});
if (payments is not null)
{
var shiftId = $"shift_{Guid.NewGuid():N}"[..16];
db.RegisterShifts.Add(new Shift
{
Id = shiftId,
CafeId = cafeId,
BranchId = branchId,
OpenedByUserId = "emp-1",
OpenedAt = createdAt.AddHours(-1),
OpeningCash = 0,
Status = ShiftStatus.Open,
CreatedAt = createdAt.AddHours(-1)
});
foreach (var p in payments)
{
db.CashTransactions.Add(new CashTransaction
{
CafeId = cafeId,
BranchId = branchId,
ShiftId = shiftId,
Type = CashTransactionType.OrderPayment,
Method = p.Method,
Amount = p.Amount,
ReferenceId = orderId,
CreatedByUserId = "emp-1",
CreatedAt = createdAt.AddMinutes(5)
});
}
}
await db.SaveChangesAsync();
}
[Fact]
public async Task GenerateReport_IsIdempotent()
{
var (db, service, cafeId, branchId, itemId) = CreateFixture();
var date = IranCalendar.TodayInIran.AddDays(-1);
var (start, _) = IranCalendar.GetUtcRangeForIranDay(date);
await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(12), voidFirstLine: false);
var first = await service.GenerateReportAsync(cafeId, branchId, date);
var second = await service.GenerateReportAsync(cafeId, branchId, date);
Assert.Equal(first.Id, second.Id);
Assert.Equal(first.TotalRevenue, second.TotalRevenue);
Assert.Equal(1, await db.DailyReports.CountAsync(r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date));
}
[Fact]
public async Task GenerateReport_ExcludesVoidedItemsFromRevenue()
{
var (db, service, cafeId, branchId, itemId) = CreateFixture();
var date = IranCalendar.TodayInIran;
var (start, _) = IranCalendar.GetUtcRangeForIranDay(date);
await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(10), voidFirstLine: true);
var report = await service.GenerateReportAsync(cafeId, branchId, date);
Assert.Equal(50_000m, report.TotalRevenue);
Assert.Equal(1, report.TotalVoids);
Assert.Equal(100_000m, report.VoidAmount);
}
[Fact]
public async Task GenerateReport_SumsPaymentMethods_FromCashTransactions()
{
var (db, service, cafeId, branchId, itemId) = CreateFixture();
var date = IranCalendar.TodayInIran;
var (start, _) = IranCalendar.GetUtcRangeForIranDay(date);
await SeedClosedOrderAsync(
db, cafeId, branchId, itemId, start.AddHours(11), false,
payments:
[
(PaymentMethod.Cash, 60_000m),
(PaymentMethod.Card, 40_000m),
(PaymentMethod.Credit, 50_000m)
]);
var report = await service.GenerateReportAsync(cafeId, branchId, date);
Assert.Equal(60_000m, report.CashRevenue);
Assert.Equal(40_000m, report.CardRevenue);
Assert.Equal(50_000m, report.CreditRevenue);
}
[Fact]
public async Task GetSummary_AggregatesAcrossBranches()
{
var (db, service, cafeId, branchId, itemId) = CreateFixture();
var branch2 = "branch-2";
var date = IranCalendar.TodayInIran;
var (start, _) = IranCalendar.GetUtcRangeForIranDay(date);
await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(9), false);
await SeedClosedOrderAsync(db, cafeId, branch2, itemId, start.AddHours(10), false);
await service.GenerateReportAsync(cafeId, branchId, date);
await service.GenerateReportAsync(cafeId, branch2, date);
var summary = await service.GetSummaryAsync(cafeId, 30);
Assert.Equal(2, summary.ByBranch.Count);
Assert.Equal(100_000m, summary.TotalRevenue);
Assert.Equal(100_000m, summary.ByBranch.Sum(b => b.TotalRevenue));
Assert.Equal(2, summary.TotalOrders);
}
[Fact]
public void ReportPlanGate_Free_AllowsEightDayWindow()
{
var today = new DateOnly(2026, 5, 21);
Assert.True(ReportPlanGate.IsDateInRange(PlanTier.Free, today, today));
Assert.True(ReportPlanGate.IsDateInRange(PlanTier.Free, today.AddDays(-7), today));
Assert.False(ReportPlanGate.IsDateInRange(PlanTier.Free, today.AddDays(-8), today));
}
}