Skip to content

Commit

Permalink
maintain original simple interest for comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
simontreanor committed Sep 12, 2024
1 parent 2c0bb99 commit b63931b
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 36 deletions.
57 changes: 37 additions & 20 deletions src/Amortisation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ module Amortisation =
NewCharges: Charge array
/// the portion of the net effect assigned to the charges
ChargesPortion: int64<Cent>
/// the interest initially calculated at the start date
/// the simple interest initially calculated at the start date
OriginalSimpleInterest: decimal<Cent> voption
/// the interest initially calculated according to the interest method at the start date
ContractualInterest: decimal<Cent>
/// the simple interest accruable between the previous amortisation day and the current day
SimpleInterest: decimal<Cent>
Expand Down Expand Up @@ -92,6 +94,7 @@ module Amortisation =
NetEffect = 0L<Cent>
PaymentStatus = NoneScheduled
BalanceStatus = OpenBalance
OriginalSimpleInterest = ValueNone
ContractualInterest = 0m<Cent>
SimpleInterest = 0m<Cent>
NewInterest = 0m<Cent>
Expand Down Expand Up @@ -128,6 +131,8 @@ module Amortisation =
CumulativeInterestPortions: int64<Cent>
/// the total of simple interest accrued up to the current day
CumulativeSimpleInterest: decimal<Cent>
/// the total of original simple interest accrued up to the current day
CumulativeOriginalSimpleInterest: decimal<Cent>
}

/// a schedule showing the amortisation, itemising the effects of payments and calculating balances for each item, and producing some final statistics resulting from the calculations
Expand Down Expand Up @@ -254,19 +259,23 @@ module Amortisation =

let cappedSimpleInterest = if a.CumulativeSimpleInterest + simpleInterest >= totalInterestCap then totalInterestCap - a.CumulativeSimpleInterest else simpleInterest

let a' =
{ a with
CumulativeSimpleInterest = a.CumulativeSimpleInterest + cappedSimpleInterest
CumulativeOriginalSimpleInterest = a.CumulativeOriginalSimpleInterest + (ap.OriginalSimpleInterest |> ValueOption.defaultValue 0m<Cent>)
}

let newInterest =
match sp.Interest.Method with
| Interest.Method.AddOn ->
if ap.AppliedPaymentDay = finalAppliedPaymentDay then
let cumulativeInterestPortions = a.CumulativeInterestPortions |> Cent.toDecimalCent
a.CumulativeSimpleInterest + cappedSimpleInterest - (a.CumulativeInterest + a.CumulativeInterestAdjustments) |> Cent.fromDecimalCent interestRounding |> Cent.toDecimalCent
|> fun i -> if cumulativeInterestPortions + i >= totalInterestCap then totalInterestCap - cumulativeInterestPortions else i
else
0m<Cent>
| Interest.Method.Simple -> simpleInterest
| Interest.Method.Compound -> failwith "Compound interest method not yet implemented"
(ap.OriginalSimpleInterest |> ValueOption.defaultValue 0m<Cent>) - cappedSimpleInterest
// (* if ap.OriginalSimpleInterest.IsSome || ap.AppliedPaymentDay = finalAppliedPaymentDay then *) a'.CumulativeSimpleInterest - a'.CumulativeOriginalSimpleInterest (* else 0m<Cent> *)
| Interest.Method.Simple ->
simpleInterest
| Interest.Method.Compound ->
failwith "Compound interest method not yet implemented"

let cappedNewInterest = if a.CumulativeInterest + newInterest >= totalInterestCap then totalInterestCap - a.CumulativeInterest else newInterest
let cappedNewInterest = if a'.CumulativeInterest + newInterest >= totalInterestCap then totalInterestCap - a'.CumulativeInterest else newInterest

let confirmedPayments = ap.ActualPayments |> Array.sumBy(function { ActualPaymentStatus = ActualPaymentStatus.Confirmed ap } -> ap | { ActualPaymentStatus = ActualPaymentStatus.WriteOff ap } -> ap | _ -> 0L<Cent>)

Expand All @@ -281,14 +290,13 @@ module Amortisation =
let roundedInterestPortion = interestPortion |> Cent.fromDecimalCent interestRounding

let accumulator =
{ a with
CumulativeScheduledPayments = a.CumulativeScheduledPayments + ap.ScheduledPayment.Value
CumulativeActualPayments = a.CumulativeActualPayments + confirmedPayments + pendingPayments
CumulativeInterest = a.CumulativeInterest + cappedNewInterest
CumulativeSimpleInterest = a.CumulativeSimpleInterest + cappedSimpleInterest
{ a' with
CumulativeScheduledPayments = a'.CumulativeScheduledPayments + ap.ScheduledPayment.Value
CumulativeActualPayments = a'.CumulativeActualPayments + confirmedPayments + pendingPayments
CumulativeInterest = a'.CumulativeInterest + cappedNewInterest
}

let extraPaymentsBalance = a.CumulativeActualPayments - a.CumulativeScheduledPayments - a.CumulativeGeneratedPayments
let extraPaymentsBalance = a'.CumulativeActualPayments - a'.CumulativeScheduledPayments - a'.CumulativeGeneratedPayments

let paymentDue =
if si.BalanceStatus = ClosedBalance || si.BalanceStatus = RefundDue then
Expand Down Expand Up @@ -404,7 +412,7 @@ module Amortisation =
| ValueSome ofpd ->
calculateFees ofpd
| Fees.SettlementRefund.Balance ->
a.CumulativeFees
a'.CumulativeFees

let feesRefund = Cent.max 0L<Cent> feesRefundIfSettled

Expand Down Expand Up @@ -470,6 +478,7 @@ module Amortisation =
NetEffect = netEffect + generatedSettlementPayment''
PaymentStatus = paymentStatus
BalanceStatus = ClosedBalance
OriginalSimpleInterest = ap.OriginalSimpleInterest
ContractualInterest = contractualInterest
SimpleInterest = cappedSimpleInterest
NewInterest = cappedNewInterest'
Expand Down Expand Up @@ -536,6 +545,7 @@ module Amortisation =
NetEffect = netEffect'
PaymentStatus = paymentStatus
BalanceStatus = balanceStatus
OriginalSimpleInterest = ap.OriginalSimpleInterest
ContractualInterest = contractualInterest
SimpleInterest = cappedSimpleInterest
NewInterest = cappedNewInterest'
Expand All @@ -556,11 +566,11 @@ module Amortisation =
let accumulator' =
{ accumulator with
CumulativeScheduledPayments = accumulator.CumulativeScheduledPayments + scheduledPaymentAmendment
CumulativeGeneratedPayments = a.CumulativeGeneratedPayments + generatedPayment
CumulativeFees = a.CumulativeFees + feesPortion'
CumulativeGeneratedPayments = a'.CumulativeGeneratedPayments + generatedPayment
CumulativeFees = a'.CumulativeFees + feesPortion'
CumulativeInterest = accumulator.CumulativeInterest - interestRoundingDifference
CumulativeInterestAdjustments = accumulator.CumulativeInterestAdjustments + interestAdjustment
CumulativeInterestPortions = a.CumulativeInterestPortions + scheduleItem.InterestPortion
CumulativeInterestPortions = a'.CumulativeInterestPortions + scheduleItem.InterestPortion
}

scheduleItem, accumulator'
Expand All @@ -583,6 +593,7 @@ module Amortisation =
CumulativeInterestAdjustments = 0m<Cent>
CumulativeInterestPortions = 0L<Cent>
CumulativeSimpleInterest = 0m<Cent>
CumulativeOriginalSimpleInterest = 0m<Cent>
}
)
|> fun a -> if (a |> Array.filter(fun (si, _) -> si.OffsetDay = 0<OffsetDay>) |> Array.length = 2) then a |> Array.tail else a
Expand Down Expand Up @@ -640,6 +651,12 @@ module Amortisation =
match scheduleType with
| ScheduleType.Original -> ScheduledPayment { ScheduledPaymentType = ScheduledPaymentType.Original si.Payment.Value; Metadata = Map.empty }
| ScheduleType.Rescheduled -> ScheduledPayment { ScheduledPaymentType = ScheduledPaymentType.Rescheduled si.Payment.Value; Metadata = Map.empty }
OriginalSimpleInterest =
match scheduleType, sp.Interest.Method with
| ScheduleType.Original, Interest.Method.AddOn ->
ValueSome si.SimpleInterest
| _ ->
ValueNone
ContractualInterest =
match scheduleType, sp.Interest.Method with
| ScheduleType.Original, Interest.Method.AddOn ->
Expand Down
27 changes: 25 additions & 2 deletions src/AppliedPayment.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ module AppliedPayment =
NetEffect: int64<Cent>
/// the payment status based on the payments made on the current day
PaymentStatus: CustomerPaymentStatus
/// the original, contractually calculated interest
/// the simple interest initially calculated at the start date
OriginalSimpleInterest: decimal<Cent> voption
/// the interest initially calculated according to the interest method at the start date
ContractualInterest: decimal<Cent> voption
}

Expand Down Expand Up @@ -123,6 +125,16 @@ module AppliedPayment =
)
| AllChargesApplied -> cc

let originalSimpleInterest =
let ii = payments |> Array.map (_.OriginalSimpleInterest >> toOption)
if ii |> Array.isEmpty || ii |> Array.forall _.IsNone then
ValueNone
else
ii
|> Array.choose id
|> Array.sum
|> ValueSome

let contractualInterest =
let ii = payments |> Array.map (_.ContractualInterest >> toOption)
if ii |> Array.isEmpty || ii |> Array.forall _.IsNone then
Expand All @@ -133,7 +145,17 @@ module AppliedPayment =
|> Array.sum
|> ValueSome

{ AppliedPaymentDay = offsetDay; ScheduledPayment = scheduledPayment'; ActualPayments = actualPayments'; GeneratedPayment = generatedPayment'; IncurredCharges = charges; NetEffect = netEffect; PaymentStatus = paymentStatus; ContractualInterest = contractualInterest }
{
AppliedPaymentDay = offsetDay
ScheduledPayment = scheduledPayment'
ActualPayments = actualPayments'
GeneratedPayment = generatedPayment'
IncurredCharges = charges
NetEffect = netEffect
PaymentStatus = paymentStatus
OriginalSimpleInterest = originalSimpleInterest
ContractualInterest = contractualInterest
}
)

let appliedPayments day generatedPayment paymentStatus =
Expand All @@ -150,6 +172,7 @@ module AppliedPayment =
IncurredCharges = [||]
NetEffect = 0L<Cent>
PaymentStatus = paymentStatus
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}
payments
Expand Down
10 changes: 9 additions & 1 deletion src/CustomerPayments.fs
Original file line number Diff line number Diff line change
Expand Up @@ -88,44 +88,52 @@ module CustomerPayments =
PaymentDay: int<OffsetDay>
/// the details of the payment
PaymentDetails: CustomerPaymentDetails
/// if a scheduled payment, the original contractually calculated interest
/// the original simple interest
OriginalSimpleInterest: decimal<Cent> voption
/// the original, contractually calculated interest
ContractualInterest: decimal<Cent> voption
}
with
static member ScheduledOriginal paymentDay amount =
{
PaymentDay = paymentDay
PaymentDetails = ScheduledPayment { ScheduledPaymentType = ScheduledPaymentType.Original amount; Metadata = Map.empty }
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}
static member ScheduledRescheduled paymentDay amount =
{
PaymentDay = paymentDay
PaymentDetails = ScheduledPayment { ScheduledPaymentType = ScheduledPaymentType.Rescheduled amount; Metadata = Map.empty }
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}
static member ActualConfirmed paymentDay amount =
{
PaymentDay = paymentDay
PaymentDetails = ActualPayment { ActualPaymentStatus = ActualPaymentStatus.Confirmed amount; Metadata = Map.empty }
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}
static member ActualPending paymentDay amount =
{
PaymentDay = paymentDay
PaymentDetails = ActualPayment { ActualPaymentStatus = ActualPaymentStatus.Pending amount; Metadata = Map.empty }
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}
static member ActualFailed paymentDay amount charges =
{
PaymentDay = paymentDay
PaymentDetails = ActualPayment { ActualPaymentStatus = ActualPaymentStatus.Failed (amount, charges); Metadata = Map.empty }
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}
static member ActualWriteOff paymentDay amount =
{
PaymentDay = paymentDay
PaymentDetails = ActualPayment { ActualPaymentStatus = ActualPaymentStatus.WriteOff amount; Metadata = Map.empty }
OriginalSimpleInterest = ValueNone
ContractualInterest = ValueNone
}

Expand Down
14 changes: 8 additions & 6 deletions src/PaymentSchedule.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module PaymentSchedule =
/// the scheduled payment amount
Payment: int64<Cent> voption
/// the simple interest accrued since the previous payment
SimpleInterest: int64<Cent>
SimpleInterest: decimal<Cent>
/// the interest portion paid off by the payment
InterestPortion: int64<Cent>
/// the principal portion paid off by the payment
Expand All @@ -29,7 +29,7 @@ module PaymentSchedule =
/// the principal balance carried forward
PrincipalBalance: int64<Cent>
/// the total simple interest accrued from the start date to the current date
TotalSimpleInterest: int64<Cent>
TotalSimpleInterest: decimal<Cent>
/// the total interest payable from the start date to the current date
TotalInterest: int64<Cent>
/// the total principal payable from the start date to the current date
Expand Down Expand Up @@ -201,7 +201,7 @@ module PaymentSchedule =

let calculateLevelPayment interest = if paymentCount = 0 then 0m else (decimal sp.Principal + decimal fees + interest) / decimal paymentCount

let initialItem = { Day = 0<OffsetDay>; Payment = ValueNone; SimpleInterest = 0L<Cent>; InterestPortion = 0L<Cent>; PrincipalPortion = 0L<Cent>; InterestBalance = totalAddOnInterest; PrincipalBalance = sp.Principal + fees; TotalSimpleInterest = 0L<Cent>; TotalInterest = 0L<Cent>; TotalPrincipal = 0L<Cent> }
let initialItem = { Day = 0<OffsetDay>; Payment = ValueNone; SimpleInterest = 0m<Cent>; InterestPortion = 0L<Cent>; PrincipalPortion = 0L<Cent>; InterestBalance = totalAddOnInterest; PrincipalBalance = sp.Principal + fees; TotalSimpleInterest = 0m<Cent>; TotalInterest = 0L<Cent>; TotalPrincipal = 0L<Cent> }

let mutable schedule = [||]

Expand All @@ -211,18 +211,20 @@ module PaymentSchedule =
match interestMethod with
| Interest.Method.Simple ->
let dailyRates = Interest.dailyRates sp.StartDate false sp.Interest.StandardRate sp.Interest.PromotionalRates previousItem.Day day
let simpleInterest = Interest.calculate previousItem.PrincipalBalance sp.Interest.Cap.Daily (ValueSome sp.Calculation.RoundingOptions.InterestRounding) dailyRates |> decimal |> Cent.round (ValueSome sp.Calculation.RoundingOptions.InterestRounding)
if previousItem.TotalSimpleInterest + simpleInterest >= totalInterestCap then totalInterestCap - previousItem.TotalInterest else simpleInterest
let simpleInterest = Interest.calculate previousItem.PrincipalBalance sp.Interest.Cap.Daily (ValueSome sp.Calculation.RoundingOptions.InterestRounding) dailyRates
let totalInterestCap' = totalInterestCap |> Cent.toDecimalCent
if previousItem.TotalSimpleInterest + simpleInterest >= totalInterestCap' then totalInterestCap' - previousItem.TotalSimpleInterest else simpleInterest
| Interest.Method.Compound -> failwith "Compound interest method not yet implemented"
| Interest.Method.AddOn ->
if payment <= previousItem.InterestBalance then
payment
else
previousItem.InterestBalance
|> Cent.toDecimalCent

let generateItem interestMethod payment previousItem day =
let simpleInterest = calculateInterest Interest.Method.Simple payment previousItem day
let interestPortion = calculateInterest interestMethod payment previousItem day
let interestPortion = calculateInterest interestMethod payment previousItem day |> Cent.fromDecimalCent (ValueSome sp.Calculation.RoundingOptions.InterestRounding)
let principalPortion = payment - interestPortion
{
Day = day
Expand Down
Loading

0 comments on commit b63931b

Please sign in to comment.