45dab8b253
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 1m32s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
205 lines
7.7 KiB
C#
205 lines
7.7 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);
|
|
const int freeMaxDays = 8; // Free plan's report-history window
|
|
Assert.True(ReportPlanGate.IsDateInRange(freeMaxDays, today, today));
|
|
Assert.True(ReportPlanGate.IsDateInRange(freeMaxDays, today.AddDays(-7), today));
|
|
Assert.False(ReportPlanGate.IsDateInRange(freeMaxDays, today.AddDays(-8), today));
|
|
}
|
|
}
|