From 1b06894383861d47236d1e558cfd0b1fdb158208 Mon Sep 17 00:00:00 2001 From: Catherine Renelle <32781029+catrenelle@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:47:14 -0500 Subject: [PATCH] Use daily interest accrual for lines of credit and auto loans, fix hasLoanDetail on account list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lines of credit and auto loans use daily accrual (rate/365 × days) matching actual bank calculations. Mortgages and personal loans keep monthly compounding. Include LoanDetail in GetAllAsync so the loan cards correctly show status. Co-Authored-By: Claude Opus 4.6 --- frontend/src/views/loans/LoansView.vue | 2 +- src/Purrse.Api/Services/AccountService.cs | 1 + src/Purrse.Api/Services/LoanService.cs | 40 ++++++++++++++++++++--- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/frontend/src/views/loans/LoansView.vue b/frontend/src/views/loans/LoansView.vue index eac7673..418b3c9 100644 --- a/frontend/src/views/loans/LoansView.vue +++ b/frontend/src/views/loans/LoansView.vue @@ -104,7 +104,7 @@ - Amounts shown are estimates based on standard monthly compounding. Actual values may differ due to your lender's rounding practices, extra payments, or rate changes. + Amounts shown are estimates. Lines of credit and auto loans use daily interest accrual; mortgages and personal loans use standard monthly compounding. Actual values may differ due to payment timing, your lender's rounding practices, extra payments, or rate changes. diff --git a/src/Purrse.Api/Services/AccountService.cs b/src/Purrse.Api/Services/AccountService.cs index c2c1831..60816c3 100644 --- a/src/Purrse.Api/Services/AccountService.cs +++ b/src/Purrse.Api/Services/AccountService.cs @@ -18,6 +18,7 @@ public class AccountService : IAccountService public async Task> GetAllAsync(Guid userId) { return await _db.Accounts + .Include(a => a.LoanDetail) .Where(a => a.UserId == userId) .OrderBy(a => a.SortOrder).ThenBy(a => a.Name) .Select(a => MapToResponse(a)) diff --git a/src/Purrse.Api/Services/LoanService.cs b/src/Purrse.Api/Services/LoanService.cs index 071326e..78bd8ee 100644 --- a/src/Purrse.Api/Services/LoanService.cs +++ b/src/Purrse.Api/Services/LoanService.cs @@ -49,7 +49,7 @@ public class LoanService : ILoanService ExtraPayment = request.ExtraPayment }; - var entries = GenerateAmortizationSchedule(loanDetail); + var entries = GenerateAmortizationSchedule(loanDetail, account.Type); loanDetail.AmortizationEntries = entries; _db.LoanDetails.Add(loanDetail); @@ -82,7 +82,7 @@ public class LoanService : ILoanService l.EscrowAmount = request.EscrowAmount; l.ExtraPayment = request.ExtraPayment; - var entries = GenerateAmortizationSchedule(l); + var entries = GenerateAmortizationSchedule(l, account.Type); foreach (var entry in entries) { entry.LoanDetailId = l.Id; @@ -118,7 +118,9 @@ public class LoanService : ILoanService // Build new schedule with extra payments var balance = Math.Abs(account.Balance); + var useDailyAccrual = UsesDailyAccrual(account.Type); var monthlyRate = l.InterestRate / 100m / 12m; + var dailyRate = l.InterestRate / 100m / 365m; var extraMonthly = request.ExtraMonthlyPayment ?? 0; var basePayment = l.MonthlyPayment - (l.EscrowAmount ?? 0); var now = DateTime.UtcNow; @@ -139,7 +141,18 @@ public class LoanService : ILoanService paymentNum++; var paymentDate = startDate.AddMonths(paymentNum); - var interest = Math.Round(balance * monthlyRate, 2); + decimal interest; + if (useDailyAccrual) + { + var prevDate = startDate.AddMonths(paymentNum - 1); + var days = (paymentDate - prevDate).Days; + interest = Math.Round(balance * dailyRate * days, 2); + } + else + { + interest = Math.Round(balance * monthlyRate, 2); + } + var principal = Math.Min(basePayment + extraMonthly - interest, balance); if (principal < 0) principal = 0; @@ -181,18 +194,35 @@ public class LoanService : ILoanService return new LoanDefaultsResponse(0, interestRate, termMonths, hasSyncData); } - private static List GenerateAmortizationSchedule(LoanDetail loan) + private static bool UsesDailyAccrual(AccountType type) => + type is AccountType.LineOfCredit or AccountType.AutoLoan; + + private static List GenerateAmortizationSchedule(LoanDetail loan, AccountType accountType) { var entries = new List(); var balance = loan.OriginalBalance; + var useDailyAccrual = UsesDailyAccrual(accountType); var monthlyRate = loan.InterestRate / 100m / 12m; + var dailyRate = loan.InterestRate / 100m / 365m; var payment = loan.MonthlyPayment - (loan.EscrowAmount ?? 0); var extra = loan.ExtraPayment ?? 0; for (int i = 1; i <= loan.TermMonths && balance > 0; i++) { var paymentDate = loan.OriginationDate.AddMonths(i - 1); - var interest = Math.Round(balance * monthlyRate, 2); + + decimal interest; + if (useDailyAccrual) + { + var prevDate = loan.OriginationDate.AddMonths(i - 2); + var days = (paymentDate - prevDate).Days; + interest = Math.Round(balance * dailyRate * days, 2); + } + else + { + interest = Math.Round(balance * monthlyRate, 2); + } + var principal = Math.Min(payment + extra - interest, balance); balance -= principal; if (balance < 0.01m) balance = 0;