using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.Finance; using ROLAC.API.Entities; namespace ROLAC.API.Services; /// /// Read-only aggregation that produces the IRS Form 990 Part IX Statement of Functional /// Expenses. Expense scope matches FinanceDashboardService: Paid + Approved only. /// Single function per expense (direct-charge); no cost splitting. /// public class Form990ReportService : IForm990ReportService { private readonly AppDbContext _db; public Form990ReportService(AppDbContext db) => _db = db; public async Task> GetLinesAsync() => await _db.Form990ExpenseLines.AsNoTracking().Where(l => l.IsActive) .OrderBy(l => l.SortOrder) .Select(l => new Form990ExpenseLineDto { Id = l.Id, LineCode = l.LineCode, Name_en = l.Name_en, Name_zh = l.Name_zh, SortOrder = l.SortOrder, }) .ToListAsync(); public async Task GetFunctionalExpenseStatementAsync(DateOnly? from, DateOnly? to) { var lines = await _db.Form990ExpenseLines.AsNoTracking() .Where(l => l.IsActive).OrderBy(l => l.SortOrder).ToListAsync(); var fallbackId = lines.FirstOrDefault(l => l.LineCode == "24")?.Id; var expenses = _db.Expenses.Where(e => e.Status == "Paid" || e.Status == "Approved"); if (from.HasValue) expenses = expenses.Where(e => e.ExpenseDate >= from.Value); if (to.HasValue) expenses = expenses.Where(e => e.ExpenseDate <= to.Value); var rows = await ( from e in expenses join m in _db.Ministries on e.MinistryId equals m.Id join sub in _db.ExpenseSubCategories on e.SubCategoryId equals sub.Id join grp in _db.ExpenseCategoryGroups on e.CategoryGroupId equals grp.Id select new { e.Amount, e.FunctionalClass, MinistryDefault = m.DefaultFunctionalClass, SubLineId = sub.Form990LineId, GroupLineId = grp.Form990LineId, }).ToListAsync(); var acc = new Dictionary(); var unmapped = 0; foreach (var r in rows) { var function = FunctionalClasses.Normalize(r.FunctionalClass ?? r.MinistryDefault); var lineId = r.SubLineId ?? r.GroupLineId ?? fallbackId; if (lineId is null) continue; if (r.SubLineId is null) unmapped++; var cur = acc.GetValueOrDefault(lineId.Value); acc[lineId.Value] = function switch { FunctionalClasses.ManagementGeneral => (cur.P, cur.M + r.Amount, cur.F), FunctionalClasses.Fundraising => (cur.P, cur.M, cur.F + r.Amount), _ => (cur.P + r.Amount, cur.M, cur.F), }; } var dto = new FunctionalExpenseStatementDto { UnmappedExpenseCount = unmapped }; foreach (var line in lines) { var v = acc.GetValueOrDefault(line.Id); dto.Rows.Add(new FunctionalExpenseRowDto { LineCode = line.LineCode, Name_en = line.Name_en, Name_zh = line.Name_zh, Program = v.P, ManagementGeneral = v.M, Fundraising = v.F, Total = v.P + v.M + v.F, }); dto.ProgramTotal += v.P; dto.ManagementGeneralTotal += v.M; dto.FundraisingTotal += v.F; } dto.GrandTotal = dto.ProgramTotal + dto.ManagementGeneralTotal + dto.FundraisingTotal; return dto; } }