refactor(1099): materialize report query for Npgsql safety; deterministic year + ordering

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-06-25 16:58:28 -07:00
parent 0754ed8d69
commit 0767a3fe94
+59 -25
View File
@@ -24,30 +24,58 @@ public class Form1099ReportService : IForm1099ReportService
Name_zh = b.Name_zh, FormType = b.FormType, SortOrder = b.SortOrder,
}).ToListAsync();
private IQueryable<PaidLine> ReportableLines(int taxYear) =>
from e in _db.Expenses.Where(e => e.Status == "Paid" && e.PaidAt != null
&& e.PaidAt.Value.Year == taxYear && e.PayeeId != null)
join p in _db.Payee1099s.Where(p => p.Is1099Tracked) on e.PayeeId equals p.Id
join l in _db.ExpenseLines on e.Id equals l.ExpenseId
join sub in _db.ExpenseSubCategories on l.SubCategoryId equals sub.Id
join grp in _db.ExpenseCategoryGroups on l.CategoryGroupId equals grp.Id
select new PaidLine
{
PayeeId = p.Id,
LegalName = p.LegalName,
TinLast4 = p.TinLast4,
W9Status = p.W9Status,
PaidAt = e.PaidAt!.Value,
Description = e.Description,
CategoryName = grp.Name_en + " / " + sub.Name_en,
Amount = l.Amount,
BoxId = sub.Form1099BoxId ?? grp.Form1099BoxId,
};
/// <summary>
/// Pulls the reportable expense lines for the tax year and materializes them (anonymous
/// projection -&gt; ToListAsync -&gt; in-memory map), mirroring Form990ReportService so the SQL
/// translation stays simple on Npgsql. The tax year is a half-open UTC range
/// [Jan 1 taxYear, Jan 1 taxYear+1), deterministic regardless of server timezone and matching
/// how Expense.PaidAt is written (midnight UTC). Unmapped lines (no 1099 box) are dropped here
/// so callers always receive reportable lines.
/// </summary>
private async Task<List<PaidLine>> LoadReportableLinesAsync(int taxYear)
{
var start = new DateTimeOffset(new DateTime(taxYear, 1, 1), TimeSpan.Zero);
var end = start.AddYears(1);
var raw = await (
from e in _db.Expenses.Where(e => e.Status == "Paid" && e.PaidAt != null
&& e.PaidAt >= start && e.PaidAt < end && e.PayeeId != null)
join p in _db.Payee1099s.Where(p => p.Is1099Tracked) on e.PayeeId equals p.Id
join l in _db.ExpenseLines on e.Id equals l.ExpenseId
join sub in _db.ExpenseSubCategories on l.SubCategoryId equals sub.Id
join grp in _db.ExpenseCategoryGroups on l.CategoryGroupId equals grp.Id
select new
{
PayeeId = p.Id,
p.LegalName,
p.TinLast4,
p.W9Status,
PaidAt = e.PaidAt!.Value,
e.Description,
GroupName = grp.Name_en,
SubName = sub.Name_en,
l.Amount,
BoxId = sub.Form1099BoxId ?? grp.Form1099BoxId,
}).ToListAsync();
return raw.Where(x => x.BoxId != null)
.Select(x => new PaidLine
{
PayeeId = x.PayeeId,
LegalName = x.LegalName,
TinLast4 = x.TinLast4,
W9Status = x.W9Status,
PaidAt = x.PaidAt,
Description = x.Description,
CategoryName = x.GroupName + " / " + x.SubName,
Amount = x.Amount,
BoxId = x.BoxId,
}).ToList();
}
public async Task<Form1099SummaryDto> GetAnnualSummaryAsync(int taxYear)
{
var boxes = await _db.Form1099Boxes.AsNoTracking().ToDictionaryAsync(b => b.Id, b => b.BoxCode);
var lines = await ReportableLines(taxYear).Where(x => x.BoxId != null).ToListAsync();
var lines = await LoadReportableLinesAsync(taxYear);
var dto = new Form1099SummaryDto { TaxYear = taxYear };
foreach (var g in lines.GroupBy(x => x.PayeeId))
@@ -64,7 +92,7 @@ public class Form1099ReportService : IForm1099ReportService
GrandTotal = nec + rents, MeetsThreshold = meets, W9Missing = w9Missing,
});
}
dto.Rows = dto.Rows.OrderByDescending(r => r.GrandTotal).ToList();
dto.Rows = dto.Rows.OrderByDescending(r => r.GrandTotal).ThenBy(r => r.LegalName).ToList();
dto.TotalReportable = dto.Rows.Sum(r => r.GrandTotal);
dto.RecipientsAtThreshold = dto.Rows.Count(r => r.MeetsThreshold);
dto.RecipientsMissingW9 = dto.Rows.Count(r => r.W9Missing);
@@ -76,7 +104,7 @@ public class Form1099ReportService : IForm1099ReportService
var payee = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(p => p.Id == payeeId);
if (payee is null) return null;
var boxes = await _db.Form1099Boxes.AsNoTracking().ToDictionaryAsync(b => b.Id, b => b.BoxCode);
var lines = await ReportableLines(taxYear).Where(x => x.PayeeId == payeeId && x.BoxId != null).ToListAsync();
var lines = (await LoadReportableLinesAsync(taxYear)).Where(x => x.PayeeId == payeeId).ToList();
return new Form1099RecipientDetailDto
{
@@ -93,8 +121,14 @@ public class Form1099ReportService : IForm1099ReportService
private sealed class PaidLine
{
public int PayeeId; public string LegalName = ""; public string? TinLast4; public string W9Status = "";
public DateTimeOffset PaidAt; public string Description = ""; public string CategoryName = "";
public decimal Amount; public int? BoxId;
public int PayeeId { get; set; }
public string LegalName { get; set; } = "";
public string? TinLast4 { get; set; }
public string W9Status { get; set; } = "";
public DateTimeOffset PaidAt { get; set; }
public string Description { get; set; } = "";
public string CategoryName { get; set; } = "";
public decimal Amount { get; set; }
public int? BoxId { get; set; }
}
}