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