using Distribution.Models; namespace Distribution.Features.DayEndReport; public class DayEndReportService(IDayEndReportRepository repo) { public const int MaxRangeDays = 92; public static DayEndFilter BuildFilter(DateTime? from, DateTime? to) { var end = (to ?? DateTime.Now).Date; var start = (from ?? end.AddDays(-29)).Date; return new DayEndFilter(start, end); } public static string? Validate(DayEndFilter f) { if (f.To < f.From) return "'to' must not be before 'from'."; if ((f.To - f.From).TotalDays >= MaxRangeDays) return $"Date range cannot exceed {MaxRangeDays} days."; return null; } public async Task GetDayAsync(DateTime date) { var day = date.Date; var data = await repo.GetDataAsync(new DayEndFilter(day, day)); var row = BuildRow(day, data); var payments = data.Payments.Where(p => p.CollectedAt.Date == day).ToList(); var byMethod = Enum.GetValues().Select(m => { var rows = payments.Where(p => p.Method == m).ToList(); return new DayEndMethod(m.ToString(), rows.Count, rows.Sum(p => p.Amount)); }).ToList(); var expenses = data.Expenses .Where(e => e.BillDate.Date == day) .Select(e => new DayEndExpense(e.Id, e.Category, e.Payee, e.Description, e.Method?.ToString(), e.Amount)) .ToList(); return new DayEndResponse(row.Date, row, byMethod, expenses); } public async Task GetDailyAsync(DayEndFilter f) { var data = await repo.GetDataAsync(f); var days = new List(); for (var d = f.From.Date; d <= f.To.Date; d = d.AddDays(1)) days.Add(BuildRow(d, data)); var totals = new DayEndRow( "Total", days.Sum(r => r.TotalSales), days.Sum(r => r.Collected), days.Sum(r => r.Due), days.Sum(r => r.Cash), days.Sum(r => r.Cheque), days.Sum(r => r.Credit), days.Sum(r => r.Returns), days.Sum(r => r.Expenses), days.Sum(r => r.Income), days.Sum(r => r.InvoiceCount), days.Sum(r => r.PaymentCount), days.Sum(r => r.CashCollected), days.Sum(r => r.CashExpensesPaid), days.Sum(r => r.NetCash)); return new DayEndDailyResponse(f.From, f.To, days, totals); } // Accrual basis: Income = sales (finalized invoices) - returns - expenses billed that day. // Due is the unpaid part of that day's invoices as at the end of the day; Credit payments never count as paid. private static DayEndRow BuildRow(DateTime day, DayEndData data) { var next = day.AddDays(1); var invoices = data.Invoices.Where(i => i.FinalizedAt.Date == day).ToList(); var invoiceIds = invoices.Select(i => i.Id).ToHashSet(); var paidByInvoice = data.Payments .Where(p => invoiceIds.Contains(p.InvoiceId) && p.Method != PaymentMethod.Credit && p.CollectedAt < next) .GroupBy(p => p.InvoiceId) .ToDictionary(g => g.Key, g => g.Sum(p => p.Amount)); var totalSales = invoices.Sum(i => i.Total); var due = invoices.Sum(i => Math.Max(0m, i.Total - paidByInvoice.GetValueOrDefault(i.Id))); var payments = data.Payments.Where(p => p.CollectedAt.Date == day).ToList(); var cash = payments.Where(p => p.Method == PaymentMethod.Cash).Sum(p => p.Amount); var cheque = payments.Where(p => p.Method == PaymentMethod.Cheque).Sum(p => p.Amount); var credit = payments.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount); var returns = data.Returns.Where(r => r.At.Date == day).Sum(r => r.Amount); var expenses = data.Expenses.Where(e => e.BillDate.Date == day).Sum(e => e.Amount); var cashExpensesPaid = data.Expenses .Where(e => e.Method == PaymentMethod.Cash && e.PaidAt.HasValue && e.PaidAt.Value.Date == day) .Sum(e => e.Amount); return new DayEndRow( day.ToString("yyyy-MM-dd"), totalSales, cash + cheque, due, cash, cheque, credit, returns, expenses, totalSales - returns - expenses, invoices.Count, payments.Count, cash, cashExpensesPaid, cash - cashExpensesPaid); } }