diff --git a/Masuit.Tools.Abstractions/Models/LoanModel.cs b/Masuit.Tools.Abstractions/Models/LoanModel.cs index 123629c3..3b113780 100644 --- a/Masuit.Tools.Abstractions/Models/LoanModel.cs +++ b/Masuit.Tools.Abstractions/Models/LoanModel.cs @@ -15,185 +15,185 @@ namespace Masuit.Tools.Models; /// 开始还款日 public record LoanModel(decimal Loan, decimal Rate, int Period, DateTime Start, LoanType LoanType = LoanType.EquivalentInterest) { - public Dictionary RateAdjustments { get; set; } = new(); - - public List Prepayments { get; set; } = new(); - - private static decimal CumIPMT(decimal rate, decimal loan, int period) - { - double interest = 0; - for (int i = 1; i <= period; i++) - { - interest += Financial.IPmt((double)(rate / 12), i, period, (double)loan); - } - - return interest.ToDecimal(2); - } - - /// - /// 生成还款计划 - /// - /// - public LoanResult Payment() - { - var result = LoanType == LoanType.EquivalentPrincipal ? PrepaymentPrincipal() : PrepaymentInterest(); - result.Plans[0].OriginRemainInterest = result.Plans[0].RemainInterest; - for (var i = 1; i < result.Plans.Count; i++) - { - result.Plans[i].Period = i + 1; - result.Plans[i].OriginRemainInterest = result.Plans[i - 1].RemainInterest - result.Plans[i].Interest; - if (result.Plans[i].LoanType != result.Plans[i - 1].LoanType) - { - result.Plans[i].Repayment = result.Plans[i - 1].Balance - result.Plans[i].Balance - result.Plans[i].Amount; - } - } - - return result; - } - - private LoanResult PrepaymentInterest() - { - var list = new List() - { - new() - { - Date = Start, - LoanType = LoanType.EquivalentInterest - } - }; - var pmt = -Financial.Pmt((double)(Rate / 12), Period, (double)Loan); - list[0].Rate = Rate; - list[0].Period = 1; - list[0].RemainPeriod = Period; - list[0].Payment = pmt.ToDecimal(2); - list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero); - list[0].Amount = list[0].Payment - list[0].Interest; - list[0].Balance = Loan - list[0].Amount; - for (var i = 1; i < Period; i++) - { - var current = Start.AddMonths(i); - var adj = RateAdjustments.FirstOrDefault(x => x.Key <= current && x.Key > current.AddMonths(-1)); - var newRate = adj.Value ?? list[i - 1].Rate; - var prepayment = Prepayments.Find(x => x.Date <= current && x.Date > current.AddMonths(-1)); - if (prepayment?.ChangeType is LoanType.EquivalentPrincipal) - { - list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod - 1, current, LoanType.EquivalentPrincipal) - { - Prepayments = Prepayments, - RateAdjustments = RateAdjustments - }.PrepaymentPrincipal().Plans); - break; - } - - list.Add(new PaymentPlan() - { - Period = i, - Date = current, - LoanType = LoanType.EquivalentInterest - }); - list[i].Rate = newRate; - list[i].Repayment = prepayment?.Amount ?? 0; - if (Prepayments.FirstOrDefault(x => x.Date <= current.AddMonths(-1) && x.Date > current.AddMonths(-2))?.ReducePeriod == true) - { - var leftPeriod = (int)Math.Round(-Math.Log((double)(1 - (list[i - 1].Balance * list[i].Rate / 12) / list[i - 1].Payment)) / Math.Log((double)(1 + list[i].Rate / 12))); - list[i].PeriodReduce = Period - list.Count + 1 - leftPeriod; - list[i].RemainPeriod = leftPeriod; - } - else - { - list[i].RemainPeriod = list[i - 1].RemainPeriod - 1; - } - list[i].Payment = -Financial.Pmt((double)(list[i].Rate / 12), list[i].RemainPeriod, (double)list[i - 1].Balance).ToDecimal(2); - if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30) - { - var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays; - list[i].Payment = list[i - 1].Payment / days * (decimal)Math.Abs((adj.Key - list[i - 1].Date).TotalDays) + list[i].Payment / days * (decimal)Math.Abs((current - adj.Key).TotalDays); - } - list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2); - list[i].Amount = Math.Round(list[i].Payment - list[i].Interest, 2); - list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2); - if (list[i].Balance <= 0) - { - list[i].Payment += list[i].Balance; - break; - } - } - - var totalInterest = -CumIPMT(Rate, Loan, Period); - return new LoanResult(totalInterest, list); - } - - private LoanResult PrepaymentPrincipal() - { - var list = new List() - { - new() - { - Date = Start, - LoanType = LoanType.EquivalentPrincipal, - RemainPeriod = Period - } - }; - list[0].Rate = Rate; - list[0].Period = 1; - list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero); - list[0].Amount = Math.Round(Loan / Period, 2, MidpointRounding.AwayFromZero); - list[0].Payment = Math.Round(list[0].Amount + list[0].Interest, 2, MidpointRounding.AwayFromZero); - list[0].Balance = Math.Round(Loan - list[0].Amount, 2, MidpointRounding.AwayFromZero); - for (var i = 1; i < Period; i++) - { - var current = Start.AddMonths(i); - var adj = RateAdjustments.FirstOrDefault(x => x.Key <= current && x.Key > current.AddMonths(-1)); - var newRate = adj.Value ?? list[i - 1].Rate; - var prepayment = Prepayments.Find(x => x.Date <= current && x.Date > current.AddMonths(-1)); - if (prepayment?.ChangeType is LoanType.EquivalentInterest) - { - list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod, current) - { - Prepayments = Prepayments, - RateAdjustments = RateAdjustments - }.PrepaymentInterest().Plans); - break; - } - - list.Add(new PaymentPlan() - { - Period = i, - Date = current, - LoanType = LoanType.EquivalentPrincipal - }); - list[i].Rate = newRate; - list[i].Repayment = prepayment?.Amount ?? 0; - list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2, MidpointRounding.AwayFromZero); - if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30) - { - var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays; - list[i].Interest = list[i - 1].Interest / days * (decimal)Math.Abs((adj.Key - list[i - 1].Date).TotalDays) + list[i].Interest / days * (decimal)Math.Abs((current - adj.Key).TotalDays); - } - - if (prepayment?.ReducePeriod == true) - { - list[i].PeriodReduce = (int)Math.Round(list[i].Repayment / (Loan / Period)); - list[i].RemainPeriod = list[i - 1].RemainPeriod - list[i].PeriodReduce - 1; - } - else - { - list[i].RemainPeriod = list[i - 1].RemainPeriod - 1; - } - - list[i].Amount = Math.Round(list[i - 1].Balance / (Period - i - list.Sum(p => p.PeriodReduce)), 2, MidpointRounding.AwayFromZero); - list[i].Payment = Math.Round(list[i].Amount + list[i].Interest, 2, MidpointRounding.AwayFromZero); - list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2, MidpointRounding.AwayFromZero); - if (list[i].Balance <= 0) - { - list[i].Payment += list[i].Balance; - break; - } - } - - var totalInterest = Loan * Rate / 12 * (Period + 1) / 2; - return new LoanResult(totalInterest, list); - } + public Dictionary RateAdjustments { get; set; } = new(); + + public List Prepayments { get; set; } = new(); + + private static decimal CumIPMT(decimal rate, decimal loan, int period) + { + double interest = 0; + for (int i = 1; i <= period; i++) + { + interest += Financial.IPmt((double)(rate / 12), i, period, (double)loan); + } + + return interest.ToDecimal(2); + } + + /// + /// 生成还款计划 + /// + /// + public LoanResult Payment() + { + var result = LoanType == LoanType.EquivalentPrincipal ? PrepaymentPrincipal() : PrepaymentInterest(); + result.Plans[0].OriginRemainInterest = result.Plans[0].RemainInterest; + for (var i = 1; i < result.Plans.Count; i++) + { + result.Plans[i].Period = i + 1; + result.Plans[i].OriginRemainInterest = result.Plans[i - 1].RemainInterest - result.Plans[i].Interest; + if (result.Plans[i].LoanType != result.Plans[i - 1].LoanType) + { + result.Plans[i].Repayment = result.Plans[i - 1].Balance - result.Plans[i].Balance - result.Plans[i].Amount; + } + } + + return result; + } + + private LoanResult PrepaymentInterest() + { + var list = new List() + { + new() + { + Date = Start, + LoanType = LoanType.EquivalentInterest + } + }; + var pmt = -Financial.Pmt((double)(Rate / 12), Period, (double)Loan); + list[0].Rate = Rate; + list[0].Period = 1; + list[0].RemainPeriod = Period; + list[0].Payment = pmt.ToDecimal(2); + list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero); + list[0].Amount = list[0].Payment - list[0].Interest; + list[0].Balance = Loan - list[0].Amount; + for (var i = 1; i < Period; i++) + { + var current = Start.AddMonths(i); + var adj = RateAdjustments.FirstOrDefault(x => x.Key.AddMonths(1) <= current && x.Key.AddMonths(1) > current.AddMonths(-1)); + var newRate = adj.Value ?? list[i - 1].Rate; + var prepayment = Prepayments.Find(x => x.Date <= current && x.Date > current.AddMonths(-1)); + if (prepayment?.ChangeType is LoanType.EquivalentPrincipal) + { + list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod - 1, current, LoanType.EquivalentPrincipal) + { + Prepayments = Prepayments, + RateAdjustments = RateAdjustments + }.PrepaymentPrincipal().Plans); + break; + } + + list.Add(new PaymentPlan() + { + Period = i, + Date = current, + LoanType = LoanType.EquivalentInterest + }); + list[i].Rate = newRate; + list[i].Repayment = prepayment?.Amount ?? 0; + if (Prepayments.FirstOrDefault(x => x.Date <= current.AddMonths(-1) && x.Date > current.AddMonths(-2))?.ReducePeriod == true) + { + var leftPeriod = (int)Math.Round(-Math.Log((double)(1 - (list[i - 1].Balance * list[i].Rate / 12) / list[i - 1].Payment)) / Math.Log((double)(1 + list[i].Rate / 12))); + list[i].PeriodReduce = Period - list.Count + 1 - leftPeriod; + list[i].RemainPeriod = leftPeriod; + } + else + { + list[i].RemainPeriod = list[i - 1].RemainPeriod - 1; + } + list[i].Payment = -Financial.Pmt((double)(list[i].Rate / 12), list[i].RemainPeriod, (double)list[i - 1].Balance).ToDecimal(2); + if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30) + { + var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays; + list[i].Payment = list[i - 1].Payment / days * (decimal)Math.Abs((adj.Key - list[i - 1].Date).TotalDays) + list[i].Payment / days * (decimal)Math.Abs((current - adj.Key).TotalDays); + } + list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2); + list[i].Amount = Math.Round(list[i].Payment - list[i].Interest, 2); + list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2); + if (list[i].Balance <= 0) + { + list[i].Payment += list[i].Balance; + break; + } + } + + var totalInterest = -CumIPMT(Rate, Loan, Period); + return new LoanResult(totalInterest, list); + } + + private LoanResult PrepaymentPrincipal() + { + var list = new List() + { + new() + { + Date = Start, + LoanType = LoanType.EquivalentPrincipal, + RemainPeriod = Period + } + }; + list[0].Rate = Rate; + list[0].Period = 1; + list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero); + list[0].Amount = Math.Round(Loan / Period, 2, MidpointRounding.AwayFromZero); + list[0].Payment = Math.Round(list[0].Amount + list[0].Interest, 2, MidpointRounding.AwayFromZero); + list[0].Balance = Math.Round(Loan - list[0].Amount, 2, MidpointRounding.AwayFromZero); + for (var i = 1; i < Period; i++) + { + var current = Start.AddMonths(i); + var adj = RateAdjustments.FirstOrDefault(x => x.Key.AddMonths(1) <= current && x.Key.AddMonths(1) > current.AddMonths(-1)); + var newRate = adj.Value ?? list[i - 1].Rate; + var prepayment = Prepayments.Find(x => x.Date <= current && x.Date > current.AddMonths(-1)); + if (prepayment?.ChangeType is LoanType.EquivalentInterest) + { + list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod, current) + { + Prepayments = Prepayments, + RateAdjustments = RateAdjustments + }.PrepaymentInterest().Plans); + break; + } + + list.Add(new PaymentPlan() + { + Period = i, + Date = current, + LoanType = LoanType.EquivalentPrincipal + }); + list[i].Rate = newRate; + list[i].Repayment = prepayment?.Amount ?? 0; + list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2, MidpointRounding.AwayFromZero); + if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30) + { + var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays; + list[i].Interest = list[i - 1].Interest / days * (decimal)Math.Abs((adj.Key - list[i - 1].Date).TotalDays) + list[i].Interest / days * (decimal)Math.Abs((current - adj.Key).TotalDays); + } + + if (prepayment?.ReducePeriod == true) + { + list[i].PeriodReduce = (int)Math.Round(list[i].Repayment / (Loan / Period)); + list[i].RemainPeriod = list[i - 1].RemainPeriod - list[i].PeriodReduce - 1; + } + else + { + list[i].RemainPeriod = list[i - 1].RemainPeriod - 1; + } + + list[i].Amount = Math.Round(list[i - 1].Balance / (Period - i - list.Sum(p => p.PeriodReduce)), 2, MidpointRounding.AwayFromZero); + list[i].Payment = Math.Round(list[i].Amount + list[i].Interest, 2, MidpointRounding.AwayFromZero); + list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2, MidpointRounding.AwayFromZero); + if (list[i].Balance <= 0) + { + list[i].Payment += list[i].Balance; + break; + } + } + + var totalInterest = Loan * Rate / 12 * (Period + 1) / 2; + return new LoanResult(totalInterest, list); + } } /// @@ -201,17 +201,17 @@ private LoanResult PrepaymentPrincipal() /// public enum LoanType { - /// - /// 等额本息 - /// - [Description("等额本息")] - EquivalentInterest, - - /// - /// 等额本金 - /// - [Description("等额本金")] - EquivalentPrincipal, + /// + /// 等额本息 + /// + [Description("等额本息")] + EquivalentInterest, + + /// + /// 等额本金 + /// + [Description("等额本金")] + EquivalentPrincipal, } /// @@ -230,125 +230,125 @@ public record PrepaymentOption(DateTime Date, decimal Amount, bool ReducePeriod /// 还款计划 public record LoanResult(decimal TotalInterest, List Plans) { - /// - /// 总提前还款额 - /// - public decimal TotalRepayment => Plans.Sum(e => e.Repayment); - - /// - /// 实际总利息 - /// - public decimal ActualInterest => Plans.Sum(e => e.Interest); - - /// - /// 实际还款总额 - /// - public decimal ActualPayment => Plans.Sum(e => e.Payment + e.Repayment); - - /// - /// 节省利息 - /// - public decimal SavedInterest => TotalInterest - ActualInterest; - - public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal totalRepayment, out List paymentPlans) - { - totalInterest = TotalInterest; - actualInterest = ActualInterest; - totalRepayment = TotalRepayment; - paymentPlans = Plans; - } - - public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out List paymentPlans) - { - totalInterest = TotalInterest; - actualInterest = ActualInterest; - totalRepayment = TotalRepayment; - paymentPlans = Plans; - savedInterest = SavedInterest; - } - - public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out decimal actualPayment, out List paymentPlans) - { - totalInterest = TotalInterest; - actualInterest = ActualInterest; - totalRepayment = TotalRepayment; - paymentPlans = Plans; - savedInterest = SavedInterest; - actualPayment = ActualPayment; - } + /// + /// 总提前还款额 + /// + public decimal TotalRepayment => Plans.Sum(e => e.Repayment); + + /// + /// 实际总利息 + /// + public decimal ActualInterest => Plans.Sum(e => e.Interest); + + /// + /// 实际还款总额 + /// + public decimal ActualPayment => Plans.Sum(e => e.Payment + e.Repayment); + + /// + /// 节省利息 + /// + public decimal SavedInterest => TotalInterest - ActualInterest; + + public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal totalRepayment, out List paymentPlans) + { + totalInterest = TotalInterest; + actualInterest = ActualInterest; + totalRepayment = TotalRepayment; + paymentPlans = Plans; + } + + public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out List paymentPlans) + { + totalInterest = TotalInterest; + actualInterest = ActualInterest; + totalRepayment = TotalRepayment; + paymentPlans = Plans; + savedInterest = SavedInterest; + } + + public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out decimal actualPayment, out List paymentPlans) + { + totalInterest = TotalInterest; + actualInterest = ActualInterest; + totalRepayment = TotalRepayment; + paymentPlans = Plans; + savedInterest = SavedInterest; + actualPayment = ActualPayment; + } } public record PaymentPlan { - /// - /// 期数 - /// - public int Period { get; internal set; } = 12; - - /// - /// 还款日 - /// - public DateTime Date { get; internal set; } = DateTime.Now; - - /// - /// 月供 - /// - public decimal Payment { get; internal set; } - - /// - /// 年利率 - /// - public decimal Rate { get; internal set; } - - /// - /// 月还利息 - /// - public decimal Interest { get; internal set; } - - /// - /// 月还本金 - /// - public decimal Amount { get; internal set; } - - /// - /// 当期提前还款额 - /// - public decimal Repayment { get; internal set; } - - /// - /// 当期剩余本金 - /// - public decimal Balance { get; internal set; } - - /// - /// 当期剩余利息 - /// - public decimal RemainInterest => LoanType switch - { - LoanType.EquivalentInterest => Payment * (RemainPeriod - 1) - Balance - Repayment, - LoanType.EquivalentPrincipal => RemainPeriod * Interest - (RemainPeriod - 1) * RemainPeriod * Amount * (Rate / 12) / 2 - Interest, - _ => 0 - }; - - /// - /// 当期剩余利息(提前还款/利率调整前) - /// - public decimal OriginRemainInterest { get; internal set; } - - /// - /// 贷款类型(默认等额本息) - /// - public LoanType LoanType { get; internal set; } - - /// - /// 期数减少 - /// - internal int PeriodReduce { get; set; } - - /// - /// 剩余期数 - /// - internal int RemainPeriod { get; set; } + /// + /// 期数 + /// + public int Period { get; internal set; } = 12; + + /// + /// 还款日 + /// + public DateTime Date { get; internal set; } = DateTime.Now; + + /// + /// 月供 + /// + public decimal Payment { get; internal set; } + + /// + /// 年利率 + /// + public decimal Rate { get; internal set; } + + /// + /// 月还利息 + /// + public decimal Interest { get; internal set; } + + /// + /// 月还本金 + /// + public decimal Amount { get; internal set; } + + /// + /// 当期提前还款额 + /// + public decimal Repayment { get; internal set; } + + /// + /// 当期剩余本金 + /// + public decimal Balance { get; internal set; } + + /// + /// 当期剩余利息 + /// + public decimal RemainInterest => LoanType switch + { + LoanType.EquivalentInterest => Payment * (RemainPeriod - 1) - Balance - Repayment, + LoanType.EquivalentPrincipal => RemainPeriod * Interest - (RemainPeriod - 1) * RemainPeriod * Amount * (Rate / 12) / 2 - Interest, + _ => 0 + }; + + /// + /// 当期剩余利息(提前还款/利率调整前) + /// + public decimal OriginRemainInterest { get; internal set; } + + /// + /// 贷款类型(默认等额本息) + /// + public LoanType LoanType { get; internal set; } + + /// + /// 期数减少 + /// + internal int PeriodReduce { get; set; } + + /// + /// 剩余期数 + /// + internal int RemainPeriod { get; set; } } -#endif +#endif \ No newline at end of file