From 47010e5f55e6498cb132f78b84c572c57d7f6ea7 Mon Sep 17 00:00:00 2001 From: Catherine Renelle <32781029+catrenelle@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:10:18 -0500 Subject: [PATCH] Use standard monthly compounding for all loan types Daily accrual made HELOC estimates worse due to front-loaded 31-day months. Revert to rate/12 for all types - it's the standard amortization method and works well for mortgages and personal loans. For HELOCs and auto loans, differences from bank statements are typically due to variable rates, extra payments, or daily billing conventions that a fixed amortization schedule cannot capture. Co-Authored-By: Claude Opus 4.6 --- frontend/src/views/loans/LoansView.vue | 9 +-------- src/Purrse.Api/Services/LoanService.cs | 25 +++++-------------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/frontend/src/views/loans/LoansView.vue b/frontend/src/views/loans/LoansView.vue index 8310a3b..eac7673 100644 --- a/frontend/src/views/loans/LoansView.vue +++ b/frontend/src/views/loans/LoansView.vue @@ -104,7 +104,7 @@ - Amounts shown are estimates using {{ isDailyAccrualAccount ? 'daily interest accrual' : 'standard monthly compounding' }}. Actual values may differ slightly due to your lender's rounding practices. + 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. @@ -269,17 +269,10 @@ const snackbarText = ref('') const snackbarColor = ref('success') const loanAccountTypes = ['AutoLoan', 'PersonalLoan', 'Mortgage', 'LineOfCredit'] -const dailyAccrualTypes = ['AutoLoan', 'LineOfCredit'] - const loanAccounts = computed(() => accountsStore.accounts.filter(a => a.hasLoanDetail || loanAccountTypes.includes(a.type)) ) -const isDailyAccrualAccount = computed(() => { - const account = accountsStore.accounts.find(a => a.id === props.id) - return account ? dailyAccrualTypes.includes(account.type) : false -}) - const amortHeaders = [ { title: '#', key: 'paymentNumber', width: '60px' }, { title: 'Date', key: 'paymentDate', width: '110px' }, diff --git a/src/Purrse.Api/Services/LoanService.cs b/src/Purrse.Api/Services/LoanService.cs index d1aca58..071326e 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, account.Type); + var entries = GenerateAmortizationSchedule(loanDetail); 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, account.Type); + var entries = GenerateAmortizationSchedule(l); foreach (var entry in entries) { entry.LoanDetailId = l.Id; @@ -118,14 +118,11 @@ public class LoanService : ILoanService // Build new schedule with extra payments var balance = Math.Abs(account.Balance); - var useDailyAccrual = UseDailyAccrual(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; var startDate = new DateTime(now.Year, now.Month, l.OriginationDate.Day, 0, 0, 0, DateTimeKind.Utc); - var previousDate = startDate; var newEntries = new List(); int paymentNum = 0; @@ -142,15 +139,12 @@ public class LoanService : ILoanService paymentNum++; var paymentDate = startDate.AddMonths(paymentNum); - var interest = useDailyAccrual - ? Math.Round(balance * dailyRate * (paymentDate - previousDate).Days, 2) - : Math.Round(balance * monthlyRate, 2); + var interest = Math.Round(balance * monthlyRate, 2); var principal = Math.Min(basePayment + extraMonthly - interest, balance); if (principal < 0) principal = 0; balance -= principal; if (balance < 0.01m) balance = 0; - previousDate = paymentDate; newEntries.Add(new AmortizationEntryResponse( paymentNum, paymentDate, principal + interest + (l.EscrowAmount ?? 0), @@ -187,30 +181,21 @@ public class LoanService : ILoanService return new LoanDefaultsResponse(0, interestRate, termMonths, hasSyncData); } - private static bool UseDailyAccrual(AccountType type) => - type is AccountType.AutoLoan or AccountType.LineOfCredit; - - private static List GenerateAmortizationSchedule(LoanDetail loan, AccountType accountType) + private static List GenerateAmortizationSchedule(LoanDetail loan) { var entries = new List(); var balance = loan.OriginalBalance; - var useDailyAccrual = UseDailyAccrual(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; - var previousDate = loan.OriginationDate.AddMonths(-1); for (int i = 1; i <= loan.TermMonths && balance > 0; i++) { var paymentDate = loan.OriginationDate.AddMonths(i - 1); - var interest = useDailyAccrual - ? Math.Round(balance * dailyRate * (paymentDate - previousDate).Days, 2) - : Math.Round(balance * monthlyRate, 2); + var interest = Math.Round(balance * monthlyRate, 2); var principal = Math.Min(payment + extra - interest, balance); balance -= principal; if (balance < 0.01m) balance = 0; - previousDate = paymentDate; entries.Add(new AmortizationEntry {