namespace JiDangJia.Core;
public record FeeBreakdown(decimal Interest,decimal Storage,decimal Service,decimal Penalty=0)
{
public decimal Total => Fees.Round(Interest+Storage+Service+Penalty);
}
public static class Fees
{
public static decimal Round(decimal n) => decimal.Round(n,2,MidpointRounding.AwayFromZero);
// 原版按 30 天折算月费;固定利息为整期金额,固定保管费按期计费。
public static FeeBreakdown Period(DbRow ticket,int days,int unitDays=0)
{
if(days<=0) throw new ArgumentException("周期必须大于零");
decimal p=ticket.Money("principal"), months=days/30m;
var interest=ticket.Money("fixedInterest")>0?ticket.Money("fixedInterest"):p*ticket.Money("monthlyRate")/100*months;
var storage=ticket.Money("storageFixedFee")>0?ticket.Money("storageFixedFee")*days/(unitDays>0?unitDays:days):p*ticket.Money("storageFeeRate")/100*months;
return new(Round(interest),Round(storage),Round(p*ticket.Money("feeRate")/100*months));
}
public static int CurrentPeriod(DbRow ticket,IReadOnlyList<DbRow> renews)
{
var last=renews.OrderBy(r=>r.Text("createdAt")).LastOrDefault();
return Math.Max(1,last is null?(ticket.Date("dueDate")-ticket.Date("pawnDate")).Days:(last.Date("newDueDate")-last.Date("oldDueDate")).Days);
}
public static FeeBreakdown Redeem(DbRow ticket,DateTime date,decimal penaltyRate,int periodDays)
{
if(date.Date<ticket.Date("pawnDate").Date) throw new ArgumentException("赎当日期不能早于开当日期");
var fee=ticket.Text("feeChargeMode")!="POSTPAID"?new FeeBreakdown(0,0,0):Period(ticket,periodDays,periodDays);
int overdue=Math.Max(0,(date.Date-ticket.Date("dueDate").Date).Days);
return fee with { Penalty=Round(ticket.Money("principal")*penaltyRate/100*overdue) };
}
public static decimal DeadCost(DbRow ticket,DateTime date,IReadOnlyList<DbRow> renews)
{
int days=Math.Max(5,(date.Date-ticket.Date("pawnDate").Date).Days),originalDays=Math.Max(1,(ticket.Date("dueDate")-ticket.Date("pawnDate")).Days);
if(renews.Count>0) originalDays=Math.Max(1,(renews.OrderBy(r=>r.Text("createdAt")).First().Date("oldDueDate")-ticket.Date("pawnDate")).Days);
decimal collected=ticket.Text("feeChargeMode")!="POSTPAID"?Period(ticket,originalDays,originalDays).Total:0;
collected+=renews.Sum(r=>r.Money("fee"));
return Round(ticket.Money("principal")+Math.Max(0,Period(ticket,days,originalDays).Total-collected));
}
public static DateTime RenewDue(DateTime due,int days) => days%30==0?due.AddMonths(days/30):due.AddDays(days);
}
namespace JiDangJia.Core;
public record FeeBreakdown(decimal Interest,decimal Storage,decimal Service,decimal Penalty=0)
{
public decimal Total => Fees.Round(Interest+Storage+Service+Penalty);
}
public static class Fees
{
public static decimal Round(decimal n) => decimal.Round(n,2,MidpointRounding.AwayFromZero);
// 原版按 30 天折算月费;固定利息为整期金额,固定保管费按期计费。
public static FeeBreakdown Period(DbRow ticket,int days,int unitDays=0)
{
if(days<=0) throw new ArgumentException("周期必须大于零");
decimal p=ticket.Money("principal"), months=days/30m;
var interest=ticket.Money("fixedInterest")>0?ticket.Money("fixedInterest"):p*ticket.Money("monthlyRate")/100*months;
var storage=ticket.Money("storageFixedFee")>0?ticket.Money("storageFixedFee")*days/(unitDays>0?unitDays:days):p*ticket.Money("storageFeeRate")/100*months;
return new(Round(interest),Round(storage),Round(p*ticket.Money("feeRate")/100*months));
}
public static int CurrentPeriod(DbRow ticket,IReadOnlyList<DbRow> renews)
{
var last=renews.OrderBy(r=>r.Text("createdAt")).LastOrDefault();
return Math.Max(1,last is null?(ticket.Date("dueDate")-ticket.Date("pawnDate")).Days:(last.Date("newDueDate")-last.Date("oldDueDate")).Days);
}
public static FeeBreakdown Redeem(DbRow ticket,DateTime date,decimal penaltyRate,int periodDays)
{
if(date.Date<ticket.Date("pawnDate").Date) throw new ArgumentException("赎当日期不能早于开当日期");
var fee=ticket.Text("feeChargeMode")!="POSTPAID"?new FeeBreakdown(0,0,0):Period(ticket,periodDays,periodDays);
int overdue=Math.Max(0,(date.Date-ticket.Date("dueDate").Date).Days);
return fee with { Penalty=Round(ticket.Money("principal")*penaltyRate/100*overdue) };
}
public static decimal DeadCost(DbRow ticket,DateTime date,IReadOnlyList<DbRow> renews)
{
int days=Math.Max(5,(date.Date-ticket.Date("pawnDate").Date).Days),originalDays=Math.Max(1,(ticket.Date("dueDate")-ticket.Date("pawnDate")).Days);
if(renews.Count>0) originalDays=Math.Max(1,(renews.OrderBy(r=>r.Text("createdAt")).First().Date("oldDueDate")-ticket.Date("pawnDate")).Days);
decimal collected=ticket.Text("feeChargeMode")!="POSTPAID"?Period(ticket,originalDays,originalDays).Total:0;
collected+=renews.Sum(r=>r.Money("fee"));
return Round(ticket.Money("principal")+Math.Max(0,Period(ticket,days,originalDays).Total-collected));
}
public static DateTime RenewDue(DateTime due,int days) => days%30==0?due.AddMonths(days/30):due.AddDays(days);
}