Use first payment date and maturity date instead of origination date and term
Replace the term months field with a maturity date picker - users know when their loan ends, not the month count. Compute termMonths from the two dates. Rename "Origination Date" to "First Payment Date" and adjust the amortization schedule so payment #1 falls on the entered date. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,7 +84,7 @@
|
|||||||
<div>{{ loanDetail.termMonths }} months</div>
|
<div>{{ loanDetail.termMonths }} months</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="6" md="3">
|
<v-col cols="6" md="3">
|
||||||
<div class="text-medium-emphasis text-caption">Origination Date</div>
|
<div class="text-medium-emphasis text-caption">First Payment Date</div>
|
||||||
<div>{{ formatDate(loanDetail.originationDate) }}</div>
|
<div>{{ formatDate(loanDetail.originationDate) }}</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="6" md="3">
|
<v-col cols="6" md="3">
|
||||||
@@ -220,9 +220,9 @@
|
|||||||
</v-alert>
|
</v-alert>
|
||||||
<v-text-field v-model.number="loanForm.originalBalance" label="Original Balance" type="number" prefix="$" />
|
<v-text-field v-model.number="loanForm.originalBalance" label="Original Balance" type="number" prefix="$" />
|
||||||
<v-text-field v-model.number="loanForm.interestRate" label="Interest Rate (%)" type="number" suffix="%" step="0.125" />
|
<v-text-field v-model.number="loanForm.interestRate" label="Interest Rate (%)" type="number" suffix="%" step="0.125" />
|
||||||
<v-text-field v-model.number="loanForm.termMonths" label="Term (months)" type="number" />
|
|
||||||
<v-text-field v-model.number="loanForm.monthlyPayment" label="Monthly Payment" type="number" prefix="$" />
|
<v-text-field v-model.number="loanForm.monthlyPayment" label="Monthly Payment" type="number" prefix="$" />
|
||||||
<v-text-field v-model="loanForm.originationDate" label="Origination Date" type="date" />
|
<v-text-field v-model="loanForm.originationDate" label="First Payment Date" type="date" />
|
||||||
|
<v-text-field v-model="loanForm.maturityDate" label="Maturity Date" type="date" />
|
||||||
<v-text-field v-model.number="loanForm.escrowAmount" label="Escrow Amount (optional)" type="number" prefix="$" />
|
<v-text-field v-model.number="loanForm.escrowAmount" label="Escrow Amount (optional)" type="number" prefix="$" />
|
||||||
<v-text-field v-model.number="loanForm.extraPayment" label="Extra Payment (optional)" type="number" prefix="$" />
|
<v-text-field v-model.number="loanForm.extraPayment" label="Extra Payment (optional)" type="number" prefix="$" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -289,9 +289,9 @@ const payoffForm = ref({
|
|||||||
const loanForm = ref({
|
const loanForm = ref({
|
||||||
originalBalance: 0,
|
originalBalance: 0,
|
||||||
interestRate: 0,
|
interestRate: 0,
|
||||||
termMonths: 360,
|
|
||||||
monthlyPayment: 0,
|
monthlyPayment: 0,
|
||||||
originationDate: '',
|
originationDate: '',
|
||||||
|
maturityDate: '',
|
||||||
escrowAmount: null as number | null,
|
escrowAmount: null as number | null,
|
||||||
extraPayment: null as number | null,
|
extraPayment: null as number | null,
|
||||||
})
|
})
|
||||||
@@ -326,9 +326,9 @@ async function openSetupDialog() {
|
|||||||
loanForm.value = {
|
loanForm.value = {
|
||||||
originalBalance: 0,
|
originalBalance: 0,
|
||||||
interestRate: 0,
|
interestRate: 0,
|
||||||
termMonths: 360,
|
|
||||||
monthlyPayment: 0,
|
monthlyPayment: 0,
|
||||||
originationDate: new Date().toISOString().split('T')[0],
|
originationDate: new Date().toISOString().split('T')[0],
|
||||||
|
maturityDate: '',
|
||||||
escrowAmount: null,
|
escrowAmount: null,
|
||||||
extraPayment: null,
|
extraPayment: null,
|
||||||
}
|
}
|
||||||
@@ -338,7 +338,6 @@ async function openSetupDialog() {
|
|||||||
const { data } = await loansApi.getLoanDefaults(props.id)
|
const { data } = await loansApi.getLoanDefaults(props.id)
|
||||||
if (data.originalBalance > 0) loanForm.value.originalBalance = data.originalBalance
|
if (data.originalBalance > 0) loanForm.value.originalBalance = data.originalBalance
|
||||||
if (data.interestRate > 0) loanForm.value.interestRate = data.interestRate
|
if (data.interestRate > 0) loanForm.value.interestRate = data.interestRate
|
||||||
if (data.termMonths > 0) loanForm.value.termMonths = data.termMonths
|
|
||||||
prePopulated.value = data.hasSyncData
|
prePopulated.value = data.hasSyncData
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to zero defaults
|
// Fall back to zero defaults
|
||||||
@@ -354,9 +353,9 @@ function openEditDialog() {
|
|||||||
loanForm.value = {
|
loanForm.value = {
|
||||||
originalBalance: loanDetail.value.originalBalance,
|
originalBalance: loanDetail.value.originalBalance,
|
||||||
interestRate: loanDetail.value.interestRate,
|
interestRate: loanDetail.value.interestRate,
|
||||||
termMonths: loanDetail.value.termMonths,
|
|
||||||
monthlyPayment: loanDetail.value.monthlyPayment,
|
monthlyPayment: loanDetail.value.monthlyPayment,
|
||||||
originationDate: loanDetail.value.originationDate.split('T')[0],
|
originationDate: loanDetail.value.originationDate.split('T')[0],
|
||||||
|
maturityDate: loanDetail.value.maturityDate.split('T')[0],
|
||||||
escrowAmount: loanDetail.value.escrowAmount,
|
escrowAmount: loanDetail.value.escrowAmount,
|
||||||
extraPayment: loanDetail.value.extraPayment,
|
extraPayment: loanDetail.value.extraPayment,
|
||||||
}
|
}
|
||||||
@@ -367,13 +366,17 @@ async function saveLoanDetail() {
|
|||||||
if (!props.id) return
|
if (!props.id) return
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const [year, month, day] = loanForm.value.originationDate.split('-').map(Number)
|
const [sY, sM, sD] = loanForm.value.originationDate.split('-').map(Number)
|
||||||
const maturityDate = new Date(Date.UTC(year, month - 1 + loanForm.value.termMonths, day))
|
const [eY, eM, eD] = loanForm.value.maturityDate.split('-').map(Number)
|
||||||
|
const termMonths = (eY - sY) * 12 + (eM - sM) + 1
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...loanForm.value,
|
originalBalance: loanForm.value.originalBalance,
|
||||||
originationDate: new Date(Date.UTC(year, month - 1, day)).toISOString(),
|
interestRate: loanForm.value.interestRate,
|
||||||
maturityDate: maturityDate.toISOString(),
|
termMonths,
|
||||||
|
monthlyPayment: loanForm.value.monthlyPayment,
|
||||||
|
originationDate: new Date(Date.UTC(sY, sM - 1, sD)).toISOString(),
|
||||||
|
maturityDate: new Date(Date.UTC(eY, eM - 1, eD)).toISOString(),
|
||||||
escrowAmount: loanForm.value.escrowAmount || undefined,
|
escrowAmount: loanForm.value.escrowAmount || undefined,
|
||||||
extraPayment: loanForm.value.extraPayment || undefined,
|
extraPayment: loanForm.value.extraPayment || undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -665,20 +665,21 @@ public class BankSyncService : IBankSyncService
|
|||||||
var originalBalance = liability.OriginalBalance ?? Math.Abs(linked.LastKnownBalance ?? 0);
|
var originalBalance = liability.OriginalBalance ?? Math.Abs(linked.LastKnownBalance ?? 0);
|
||||||
var interestRate = liability.InterestRate ?? 0;
|
var interestRate = liability.InterestRate ?? 0;
|
||||||
var termMonths = liability.TermMonths ?? (liability.LoanType == "Mortgage" ? 360 : 60);
|
var termMonths = liability.TermMonths ?? (liability.LoanType == "Mortgage" ? 360 : 60);
|
||||||
var originationDate = liability.OriginationDate ?? DateTime.UtcNow;
|
// Plaid gives loan origination date; first payment is typically one month later
|
||||||
|
var firstPaymentDate = liability.OriginationDate?.AddMonths(1) ?? DateTime.UtcNow;
|
||||||
var escrowBalance = liability.EscrowBalance;
|
var escrowBalance = liability.EscrowBalance;
|
||||||
|
|
||||||
// Compute monthly payment via amortization formula if not provided
|
// Compute monthly payment via amortization formula if not provided
|
||||||
var monthlyPayment = liability.MonthlyPayment ?? ComputeMonthlyPayment(originalBalance, interestRate, termMonths);
|
var monthlyPayment = liability.MonthlyPayment ?? ComputeMonthlyPayment(originalBalance, interestRate, termMonths);
|
||||||
|
|
||||||
var maturityDate = liability.MaturityDate ?? originationDate.AddMonths(termMonths);
|
var maturityDate = liability.MaturityDate ?? firstPaymentDate.AddMonths(termMonths - 1);
|
||||||
|
|
||||||
var request = new CreateLoanDetailRequest(
|
var request = new CreateLoanDetailRequest(
|
||||||
originalBalance,
|
originalBalance,
|
||||||
interestRate,
|
interestRate,
|
||||||
termMonths,
|
termMonths,
|
||||||
monthlyPayment,
|
monthlyPayment,
|
||||||
DateTime.SpecifyKind(originationDate, DateTimeKind.Utc),
|
DateTime.SpecifyKind(firstPaymentDate, DateTimeKind.Utc),
|
||||||
DateTime.SpecifyKind(maturityDate, DateTimeKind.Utc),
|
DateTime.SpecifyKind(maturityDate, DateTimeKind.Utc),
|
||||||
escrowBalance,
|
escrowBalance,
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -187,11 +187,11 @@ public class LoanService : ILoanService
|
|||||||
var dailyRate = loan.InterestRate / 100m / 365m;
|
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;
|
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);
|
var paymentDate = loan.OriginationDate.AddMonths(i - 1);
|
||||||
var days = (paymentDate - previousDate).Days;
|
var days = (paymentDate - previousDate).Days;
|
||||||
var interest = balance * dailyRate * days;
|
var interest = balance * dailyRate * days;
|
||||||
var principal = Math.Min(payment + extra - interest, balance);
|
var principal = Math.Min(payment + extra - interest, balance);
|
||||||
|
|||||||
Reference in New Issue
Block a user