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:
@@ -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 -> ToListAsync -> 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; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user