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 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-alert v-if="loanDetail" type="info" variant="tonal" density="compact" class="mb-4">
|
<v-alert v-if="loanDetail" type="info" variant="tonal" density="compact" class="mb-4">
|
||||||
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.
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<!-- Amortization Schedule -->
|
<!-- Amortization Schedule -->
|
||||||
@@ -269,17 +269,10 @@ const snackbarText = ref('')
|
|||||||
const snackbarColor = ref('success')
|
const snackbarColor = ref('success')
|
||||||
|
|
||||||
const loanAccountTypes = ['AutoLoan', 'PersonalLoan', 'Mortgage', 'LineOfCredit']
|
const loanAccountTypes = ['AutoLoan', 'PersonalLoan', 'Mortgage', 'LineOfCredit']
|
||||||
const dailyAccrualTypes = ['AutoLoan', 'LineOfCredit']
|
|
||||||
|
|
||||||
const loanAccounts = computed(() =>
|
const loanAccounts = computed(() =>
|
||||||
accountsStore.accounts.filter(a => a.hasLoanDetail || loanAccountTypes.includes(a.type))
|
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 = [
|
const amortHeaders = [
|
||||||
{ title: '#', key: 'paymentNumber', width: '60px' },
|
{ title: '#', key: 'paymentNumber', width: '60px' },
|
||||||
{ title: 'Date', key: 'paymentDate', width: '110px' },
|
{ title: 'Date', key: 'paymentDate', width: '110px' },
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class LoanService : ILoanService
|
|||||||
ExtraPayment = request.ExtraPayment
|
ExtraPayment = request.ExtraPayment
|
||||||
};
|
};
|
||||||
|
|
||||||
var entries = GenerateAmortizationSchedule(loanDetail, account.Type);
|
var entries = GenerateAmortizationSchedule(loanDetail);
|
||||||
loanDetail.AmortizationEntries = entries;
|
loanDetail.AmortizationEntries = entries;
|
||||||
|
|
||||||
_db.LoanDetails.Add(loanDetail);
|
_db.LoanDetails.Add(loanDetail);
|
||||||
@@ -82,7 +82,7 @@ public class LoanService : ILoanService
|
|||||||
l.EscrowAmount = request.EscrowAmount;
|
l.EscrowAmount = request.EscrowAmount;
|
||||||
l.ExtraPayment = request.ExtraPayment;
|
l.ExtraPayment = request.ExtraPayment;
|
||||||
|
|
||||||
var entries = GenerateAmortizationSchedule(l, account.Type);
|
var entries = GenerateAmortizationSchedule(l);
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
{
|
{
|
||||||
entry.LoanDetailId = l.Id;
|
entry.LoanDetailId = l.Id;
|
||||||
@@ -118,14 +118,11 @@ public class LoanService : ILoanService
|
|||||||
|
|
||||||
// Build new schedule with extra payments
|
// Build new schedule with extra payments
|
||||||
var balance = Math.Abs(account.Balance);
|
var balance = Math.Abs(account.Balance);
|
||||||
var useDailyAccrual = UseDailyAccrual(account.Type);
|
|
||||||
var monthlyRate = l.InterestRate / 100m / 12m;
|
var monthlyRate = l.InterestRate / 100m / 12m;
|
||||||
var dailyRate = l.InterestRate / 100m / 365m;
|
|
||||||
var extraMonthly = request.ExtraMonthlyPayment ?? 0;
|
var extraMonthly = request.ExtraMonthlyPayment ?? 0;
|
||||||
var basePayment = l.MonthlyPayment - (l.EscrowAmount ?? 0);
|
var basePayment = l.MonthlyPayment - (l.EscrowAmount ?? 0);
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var startDate = new DateTime(now.Year, now.Month, l.OriginationDate.Day, 0, 0, 0, DateTimeKind.Utc);
|
var startDate = new DateTime(now.Year, now.Month, l.OriginationDate.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
var previousDate = startDate;
|
|
||||||
|
|
||||||
var newEntries = new List<AmortizationEntryResponse>();
|
var newEntries = new List<AmortizationEntryResponse>();
|
||||||
int paymentNum = 0;
|
int paymentNum = 0;
|
||||||
@@ -142,15 +139,12 @@ public class LoanService : ILoanService
|
|||||||
paymentNum++;
|
paymentNum++;
|
||||||
var paymentDate = startDate.AddMonths(paymentNum);
|
var paymentDate = startDate.AddMonths(paymentNum);
|
||||||
|
|
||||||
var interest = useDailyAccrual
|
var interest = Math.Round(balance * monthlyRate, 2);
|
||||||
? Math.Round(balance * dailyRate * (paymentDate - previousDate).Days, 2)
|
|
||||||
: Math.Round(balance * monthlyRate, 2);
|
|
||||||
var principal = Math.Min(basePayment + extraMonthly - interest, balance);
|
var principal = Math.Min(basePayment + extraMonthly - interest, balance);
|
||||||
if (principal < 0) principal = 0;
|
if (principal < 0) principal = 0;
|
||||||
|
|
||||||
balance -= principal;
|
balance -= principal;
|
||||||
if (balance < 0.01m) balance = 0;
|
if (balance < 0.01m) balance = 0;
|
||||||
previousDate = paymentDate;
|
|
||||||
|
|
||||||
newEntries.Add(new AmortizationEntryResponse(
|
newEntries.Add(new AmortizationEntryResponse(
|
||||||
paymentNum, paymentDate, principal + interest + (l.EscrowAmount ?? 0),
|
paymentNum, paymentDate, principal + interest + (l.EscrowAmount ?? 0),
|
||||||
@@ -187,30 +181,21 @@ public class LoanService : ILoanService
|
|||||||
return new LoanDefaultsResponse(0, interestRate, termMonths, hasSyncData);
|
return new LoanDefaultsResponse(0, interestRate, termMonths, hasSyncData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UseDailyAccrual(AccountType type) =>
|
private static List<AmortizationEntry> GenerateAmortizationSchedule(LoanDetail loan)
|
||||||
type is AccountType.AutoLoan or AccountType.LineOfCredit;
|
|
||||||
|
|
||||||
private static List<AmortizationEntry> GenerateAmortizationSchedule(LoanDetail loan, AccountType accountType)
|
|
||||||
{
|
{
|
||||||
var entries = new List<AmortizationEntry>();
|
var entries = new List<AmortizationEntry>();
|
||||||
var balance = loan.OriginalBalance;
|
var balance = loan.OriginalBalance;
|
||||||
var useDailyAccrual = UseDailyAccrual(accountType);
|
|
||||||
var monthlyRate = loan.InterestRate / 100m / 12m;
|
var monthlyRate = loan.InterestRate / 100m / 12m;
|
||||||
var dailyRate = loan.InterestRate / 100m / 365m;
|
|
||||||
var payment = loan.MonthlyPayment - (loan.EscrowAmount ?? 0);
|
var payment = loan.MonthlyPayment - (loan.EscrowAmount ?? 0);
|
||||||
var extra = loan.ExtraPayment ?? 0;
|
var extra = loan.ExtraPayment ?? 0;
|
||||||
var previousDate = loan.OriginationDate.AddMonths(-1);
|
|
||||||
|
|
||||||
for (int i = 1; i <= loan.TermMonths && balance > 0; i++)
|
for (int i = 1; i <= loan.TermMonths && balance > 0; i++)
|
||||||
{
|
{
|
||||||
var paymentDate = loan.OriginationDate.AddMonths(i - 1);
|
var paymentDate = loan.OriginationDate.AddMonths(i - 1);
|
||||||
var interest = useDailyAccrual
|
var interest = Math.Round(balance * monthlyRate, 2);
|
||||||
? Math.Round(balance * dailyRate * (paymentDate - previousDate).Days, 2)
|
|
||||||
: Math.Round(balance * monthlyRate, 2);
|
|
||||||
var principal = Math.Min(payment + extra - interest, balance);
|
var principal = Math.Min(payment + extra - interest, balance);
|
||||||
balance -= principal;
|
balance -= principal;
|
||||||
if (balance < 0.01m) balance = 0;
|
if (balance < 0.01m) balance = 0;
|
||||||
previousDate = paymentDate;
|
|
||||||
|
|
||||||
entries.Add(new AmortizationEntry
|
entries.Add(new AmortizationEntry
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user