Private
Public Access
1
0

Normalize all DateTime values to UTC across the codebase

Npgsql requires DateTimeKind.Utc for timestamp with time zone
columns. This fixes errors in transaction create/edit, budgets,
reports, loans, scheduled transactions, and file imports.

Changes:
- TransactionService: SpecifyKind on request.Date for create,
  update, and transfer
- BudgetService: UTC kind on all new DateTime(year, month, 1)
- ReportService: UTC kind on endOfMonth; rewrite spending-by-
  category GroupBy to avoid untranslatable navigation properties
- LoanService: UTC kind on payoff scenario date construction
- ScheduledTransactionService: UTC kind on SemiMonthly date
- ImportService: SpecifyKind on parsed.Date when persisting
- OFX/CSV/QIF parsers: AssumeUniversal + AdjustToUniversal on
  all date parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-09 01:07:26 -05:00
parent bcb4a74158
commit 8b2f5aad56
9 changed files with 31 additions and 22 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ public class BudgetService : IBudgetService
if (budget == null) return null;
var startDate = new DateTime(year, month, 1);
var startDate = new DateTime(year, month, 1, 0, 0, 0, DateTimeKind.Utc);
var endDate = startDate.AddMonths(1).AddDays(-1);
var actuals = await GetActualSpending(userId, startDate, endDate);
@@ -77,7 +77,7 @@ public class BudgetService : IBudgetService
await _db.SaveChangesAsync();
var startDate = new DateTime(budget.Year, budget.Month, 1);
var startDate = new DateTime(budget.Year, budget.Month, 1, 0, 0, 0, DateTimeKind.Utc);
var endDate = startDate.AddMonths(1).AddDays(-1);
var actuals = await GetActualSpending(userId, startDate, endDate);
@@ -98,7 +98,7 @@ public class BudgetService : IBudgetService
.FirstOrDefaultAsync(b => b.Id == budgetId && b.UserId == userId)
?? throw new KeyNotFoundException("Budget not found");
var startDate = new DateTime(budget.Year, budget.Month, 1);
var startDate = new DateTime(budget.Year, budget.Month, 1, 0, 0, 0, DateTimeKind.Utc);
var endDate = startDate.AddMonths(1).AddDays(-1);
var actuals = await GetActualSpending(userId, startDate, endDate);
+1 -1
View File
@@ -215,7 +215,7 @@ public class ImportService : IImportService
{
Id = Guid.NewGuid(),
AccountId = batch.AccountId,
Date = parsed.Date,
Date = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc),
Amount = parsed.Amount,
PayeeId = matchedPayee?.Id,
PayeeName = parsed.PayeeName,
+2 -2
View File
@@ -115,8 +115,8 @@ public class LoanService : ILoanService
var monthlyRate = l.InterestRate / 100 / 12;
var extraMonthly = request.ExtraMonthlyPayment ?? 0;
var basePayment = l.MonthlyPayment - (l.EscrowAmount ?? 0);
var paymentDate = DateTime.UtcNow.Date;
paymentDate = new DateTime(paymentDate.Year, paymentDate.Month, l.OriginationDate.Day);
var now = DateTime.UtcNow;
var paymentDate = new DateTime(now.Year, now.Month, l.OriginationDate.Day, 0, 0, 0, DateTimeKind.Utc);
var newEntries = new List<AmortizationEntryResponse>();
int paymentNum = 0;
+15 -6
View File
@@ -23,15 +23,24 @@ public class ReportService : IReportService
query = query.Where(t => t.AccountId == accountId.Value);
var groups = await query
.GroupBy(t => new { t.CategoryId, CategoryName = t.Category!.Name, ParentName = t.Category.Parent != null ? t.Category.Parent.Name : (string?)null })
.Select(g => new { g.Key.CategoryName, g.Key.ParentName, Amount = Math.Abs(g.Sum(t => t.Amount)) })
.GroupBy(t => t.CategoryId)
.Select(g => new { CategoryId = g.Key, Amount = -g.Sum(t => t.Amount) })
.OrderByDescending(g => g.Amount)
.ToListAsync();
var categoryIds = groups.Select(g => g.CategoryId).ToList();
var categoryLookup = await _db.Categories
.Include(c => c.Parent)
.Where(c => categoryIds.Contains(c.Id))
.ToDictionaryAsync(c => c.Id, c => new { c.Name, ParentName = c.Parent != null ? c.Parent.Name : (string?)null });
var total = groups.Sum(g => g.Amount);
var categories = groups.Select(g => new CategorySpending(
g.CategoryName, g.Amount, total > 0 ? (g.Amount / total) * 100 : 0, g.ParentName
)).ToList();
var categories = groups.Select(g =>
{
var cat = categoryLookup.GetValueOrDefault(g.CategoryId!.Value);
return new CategorySpending(
cat?.Name ?? "Unknown", g.Amount, total > 0 ? (g.Amount / total) * 100 : 0, cat?.ParentName);
}).ToList();
return new SpendingByCategoryReport(startDate, endDate, total, categories);
}
@@ -71,7 +80,7 @@ public class ReportService : IReportService
while (currentDate <= endDate)
{
var endOfMonth = new DateTime(currentDate.Year, currentDate.Month, DateTime.DaysInMonth(currentDate.Year, currentDate.Month));
var endOfMonth = new DateTime(currentDate.Year, currentDate.Month, DateTime.DaysInMonth(currentDate.Year, currentDate.Month), 0, 0, 0, DateTimeKind.Utc);
var txnsToDate = transactions.Where(t => t.Date <= endOfMonth).Sum(t => t.Amount);
// Simplified: using current balances adjusted by future transactions
@@ -141,7 +141,7 @@ public class ScheduledTransactionService : IScheduledTransactionService
Frequency.Daily => current.AddDays(1),
Frequency.Weekly => current.AddDays(7),
Frequency.BiWeekly => current.AddDays(14),
Frequency.SemiMonthly => current.Day <= 15 ? new DateTime(current.Year, current.Month, Math.Min(28, DateTime.DaysInMonth(current.Year, current.Month))) : current.AddMonths(1).AddDays(-(current.Day - 1)),
Frequency.SemiMonthly => current.Day <= 15 ? new DateTime(current.Year, current.Month, Math.Min(28, DateTime.DaysInMonth(current.Year, current.Month)), 0, 0, 0, DateTimeKind.Utc) : current.AddMonths(1).AddDays(-(current.Day - 1)),
Frequency.Monthly => current.AddMonths(1),
Frequency.Quarterly => current.AddMonths(3),
Frequency.SemiAnnually => current.AddMonths(6),
@@ -47,7 +47,7 @@ public class TransactionService : ITransactionService
{
Id = Guid.NewGuid(),
AccountId = request.AccountId,
Date = request.Date,
Date = DateTime.SpecifyKind(request.Date, DateTimeKind.Utc),
Amount = request.Amount,
PayeeName = request.PayeeName,
PayeeId = request.PayeeId,
@@ -89,7 +89,7 @@ public class TransactionService : ITransactionService
?? throw new KeyNotFoundException("Transaction not found");
var oldAmount = transaction.Amount;
transaction.Date = request.Date;
transaction.Date = DateTime.SpecifyKind(request.Date, DateTimeKind.Utc);
transaction.Amount = request.Amount;
transaction.PayeeName = request.PayeeName;
transaction.PayeeId = request.PayeeId;
@@ -181,7 +181,7 @@ public class TransactionService : ITransactionService
{
Id = Guid.NewGuid(),
AccountId = request.FromAccountId,
Date = request.Date,
Date = DateTime.SpecifyKind(request.Date, DateTimeKind.Utc),
Amount = -Math.Abs(request.Amount),
PayeeName = $"Transfer to {toAccount.Name}",
Memo = request.Memo,
@@ -193,7 +193,7 @@ public class TransactionService : ITransactionService
{
Id = Guid.NewGuid(),
AccountId = request.ToAccountId,
Date = request.Date,
Date = DateTime.SpecifyKind(request.Date, DateTimeKind.Utc),
Amount = Math.Abs(request.Amount),
PayeeName = $"Transfer from {fromAccount.Name}",
Memo = request.Memo,