using Distribution.Features.DayEndReport; using Distribution.Models; using Moq; using Xunit; namespace Distribution.Tests.Services; public class DayEndReportServiceTests { private static readonly DateTime Day = new(2026, 10, 20); private readonly Mock _repo = new(); private readonly DayEndReportService _sut; public DayEndReportServiceTests() { _sut = new DayEndReportService(_repo.Object); } private void Returns(DayEndData data) => _repo.Setup(r => r.GetDataAsync(It.IsAny())).ReturnsAsync(data); private static DayEndData Data( List? invoices = null, List? payments = null, List? returns = null, List? expenses = null) => new(invoices ?? [], payments ?? [], returns ?? [], expenses ?? []); private static DayEndExpenseRow Expense(DateTime billDate, decimal amount, PaymentMethod? method = null, DateTime? paidAt = null) => new(Guid.NewGuid(), billDate, paidAt, method, amount, "Utilities", "Payee", null); // Validate / BuildFilter [Fact] public void Validate_ReturnsError_WhenToBeforeFrom() { var f = new DayEndFilter(Day, Day.AddDays(-1)); Assert.NotNull(DayEndReportService.Validate(f)); } [Fact] public void Validate_ReturnsError_WhenRangeTooLong() { var f = new DayEndFilter(Day, Day.AddDays(DayEndReportService.MaxRangeDays)); Assert.NotNull(DayEndReportService.Validate(f)); } [Fact] public void Validate_ReturnsNull_ForMaxAllowedRange() { var f = new DayEndFilter(Day, Day.AddDays(DayEndReportService.MaxRangeDays - 1)); Assert.Null(DayEndReportService.Validate(f)); } // GetDayAsync [Fact] public async Task GetDayAsync_ComputesSalesCollectionsAndIncome() { var inv1 = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(10), 1000m); var inv2 = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(15), 500m); Returns(Data( invoices: [inv1, inv2], payments: [ new(inv1.Id, PaymentMethod.Cash, 600m, Day.AddHours(11)), new(inv1.Id, PaymentMethod.Cheque, 400m, Day.AddHours(12)), new(inv2.Id, PaymentMethod.Credit, 500m, Day.AddHours(16)), ], returns: [new(Day.AddHours(9), 100m)], expenses: [Expense(Day, 250m)])); var result = await _sut.GetDayAsync(Day); var s = result.Summary; Assert.Equal(1500m, s.TotalSales); Assert.Equal(1000m, s.Collected); Assert.Equal(500m, s.Due); Assert.Equal(600m, s.Cash); Assert.Equal(400m, s.Cheque); Assert.Equal(500m, s.Credit); Assert.Equal(100m, s.Returns); Assert.Equal(250m, s.Expenses); Assert.Equal(1150m, s.Income); Assert.Equal(2, s.InvoiceCount); Assert.Equal(3, s.PaymentCount); } [Fact] public async Task GetDayAsync_CreditPayment_DoesNotReduceDue() { var inv = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(10), 800m); Returns(Data(invoices: [inv], payments: [new(inv.Id, PaymentMethod.Credit, 800m, Day.AddHours(10))])); var result = await _sut.GetDayAsync(Day); Assert.Equal(800m, result.Summary.Due); Assert.Equal(0m, result.Summary.Collected); } [Fact] public async Task GetDayAsync_PaymentAfterDayEnd_DoesNotReduceThatDaysDue() { var inv = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(10), 800m); Returns(Data(invoices: [inv], payments: [new(inv.Id, PaymentMethod.Cash, 800m, Day.AddDays(1).AddHours(1))])); var result = await _sut.GetDayAsync(Day); Assert.Equal(800m, result.Summary.Due); Assert.Equal(0m, result.Summary.Collected); } [Fact] public async Task GetDayAsync_InstalmentOnOlderInvoice_CountsAsCollectedNotSales() { var older = Guid.NewGuid(); Returns(Data(payments: [new(older, PaymentMethod.Cash, 300m, Day.AddHours(9))])); var result = await _sut.GetDayAsync(Day); Assert.Equal(0m, result.Summary.TotalSales); Assert.Equal(300m, result.Summary.Collected); } [Fact] public async Task GetDayAsync_Overpayment_DueIsNeverNegative() { var inv = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(10), 100m); Returns(Data(invoices: [inv], payments: [new(inv.Id, PaymentMethod.Cash, 150m, Day.AddHours(11))])); var result = await _sut.GetDayAsync(Day); Assert.Equal(0m, result.Summary.Due); } [Fact] public async Task GetDayAsync_OnlyCountsExpensesBilledThatDay() { Returns(Data(expenses: [ Expense(Day, 100m), Expense(Day.AddDays(-1), 999m, PaymentMethod.Cash, paidAt: Day.AddHours(8)), ])); var result = await _sut.GetDayAsync(Day); Assert.Equal(100m, result.Summary.Expenses); Assert.Single(result.Expenses); } [Fact] public async Task GetDayAsync_NetCash_IsCashCollectedMinusCashExpensesPaid() { var inv = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(10), 1000m); Returns(Data( invoices: [inv], payments: [ new(inv.Id, PaymentMethod.Cash, 700m, Day.AddHours(11)), new(inv.Id, PaymentMethod.Cheque, 300m, Day.AddHours(11)), ], expenses: [ Expense(Day, 200m, PaymentMethod.Cash, paidAt: Day.AddHours(14)), Expense(Day, 50m, PaymentMethod.Cheque, paidAt: Day.AddHours(14)), ])); var result = await _sut.GetDayAsync(Day); Assert.Equal(700m, result.Summary.CashCollected); Assert.Equal(200m, result.Summary.CashExpensesPaid); Assert.Equal(500m, result.Summary.NetCash); } [Fact] public async Task GetDayAsync_NoActivity_ReturnsZeros() { Returns(Data()); var result = await _sut.GetDayAsync(Day); Assert.Equal(0m, result.Summary.Income); Assert.Equal(3, result.ByMethod.Count); Assert.Empty(result.Expenses); } // GetDailyAsync [Fact] public async Task GetDailyAsync_FillsEmptyDays_AndTotalsMatchSumOfDays() { var inv = new DayEndInvoiceRow(Guid.NewGuid(), Day.AddHours(10), 1000m); Returns(Data( invoices: [inv], payments: [new(inv.Id, PaymentMethod.Cash, 1000m, Day.AddHours(11))], expenses: [Expense(Day.AddDays(2), 300m)])); var result = await _sut.GetDailyAsync(new DayEndFilter(Day, Day.AddDays(2))); Assert.Equal(3, result.Days.Count); Assert.Equal(0m, result.Days[1].TotalSales); Assert.Equal(1000m, result.Totals.TotalSales); Assert.Equal(300m, result.Totals.Expenses); Assert.Equal(700m, result.Totals.Income); Assert.Equal(result.Days.Sum(d => d.Income), result.Totals.Income); } }