Skip to content

Commit fcbfa08

Browse files
committed
extract functions
1 parent 721d6e3 commit fcbfa08

File tree

1 file changed

+143
-109
lines changed

1 file changed

+143
-109
lines changed

src/Amortisation.fs

Lines changed: 143 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,100 @@ module Amortisation =
601601
p.Basic.InterestConfig.Cap.DailyAmount
602602
interestRounding
603603

604+
/// calculates new interest based on the interest method
605+
let calculateNewInterest
606+
interestMethod
607+
previousBalanceStatus
608+
cumulativeActuarialInterestM
609+
cappedActuarialInterestM
610+
initialInterestBalanceM
611+
actuarialInterestM //this can be higher than the capped actuarial interest because it can include an adjustment that sucks in all the lost interest from rounding
612+
=
613+
match interestMethod with
614+
| Interest.Method.Actuarial -> actuarialInterestM
615+
| Interest.Method.AddOn when previousBalanceStatus = ClosedBalance -> 0m<Cent>
616+
| Interest.Method.AddOn ->
617+
cumulativeActuarialInterestM + cappedActuarialInterestM
618+
|> fun i ->
619+
if i > initialInterestBalanceM then
620+
i - initialInterestBalanceM
621+
else
622+
0m<Cent>
623+
|> min cappedActuarialInterestM
624+
625+
/// calculates any new interest accrued since the previous item, according to the interest method supplied in the schedule parameters
626+
let calculateInterestAdjustment
627+
previousBalanceStatus
628+
currentGeneratedPayment
629+
settlement
630+
cappedNewInterestM
631+
cumulativeActuarialInterestM
632+
initialInterestBalanceM
633+
(basicParameters: BasicParameters)
634+
=
635+
match basicParameters.InterestConfig.Method with
636+
| Interest.Method.AddOn when
637+
previousBalanceStatus <> ClosedBalance
638+
&& (currentGeneratedPayment = ToBeGenerated || settlement <= 0L<Cent>)
639+
&& previousBalanceStatus <> RefundDue
640+
&& cappedNewInterestM = 0m<Cent> // cappedNewInterest check here avoids adding an interest adjustment twice (one for generated payment, one for final payment)
641+
->
642+
cumulativeActuarialInterestM - initialInterestBalanceM
643+
|> Interest.ignoreFractionalCents 1
644+
|> Interest.Cap.cappedAddedValue
645+
basicParameters.InterestConfig.Cap.TotalAmount
646+
basicParameters.Principal
647+
cumulativeActuarialInterestM
648+
| _ -> 0m<Cent>
649+
650+
/// apportions the fee
651+
let apportionFee (basicFeeConfig: Fee.BasicConfig voption) previousFeeBalance assignable principal feeTotal =
652+
match basicFeeConfig with
653+
| ValueSome feeConfig ->
654+
match feeConfig.FeeAmortisation with
655+
| Fee.FeeAmortisation.AmortiseBeforePrincipal -> min previousFeeBalance assignable
656+
| Fee.FeeAmortisation.AmortiseProportionately ->
657+
feePercentage principal feeTotal
658+
|> Percent.toDecimal
659+
|> fun m ->
660+
if 1m + m = 0m then
661+
0L<Cent>
662+
else
663+
decimal assignable * m / (1m + m)
664+
|> Cent.round RoundUp
665+
|> max 0L<Cent>
666+
|> min previousFeeBalance
667+
| ValueNone -> 0L<Cent>
668+
669+
/// determines the value of any fee rebate in the event of settlement, depending on settings
670+
let calculateFeeRebate
671+
(advancedFeeConfig: Fee.AdvancedConfig voption)
672+
scheduleConfig
673+
startDate
674+
feeTotal
675+
currentDay
676+
cumulativeFee
677+
=
678+
match advancedFeeConfig with
679+
| ValueSome feeConfig ->
680+
match feeConfig.SettlementRebate with
681+
| Fee.SettlementRebate.ProRata ->
682+
let originalFinalPaymentDay =
683+
scheduleConfig
684+
|> generatePaymentMap startDate
685+
|> Map.keys
686+
|> Seq.toArray
687+
|> Array.tryLast
688+
|> Option.defaultValue 0<OffsetDay>
689+
690+
calculateFee feeTotal currentDay originalFinalPaymentDay
691+
| Fee.SettlementRebate.ProRataRescheduled originalFinalPaymentDay ->
692+
calculateFee feeTotal currentDay originalFinalPaymentDay
693+
| Fee.SettlementRebate.Balance -> cumulativeFee
694+
| Fee.SettlementRebate.Zero -> 0L<Cent>
695+
| ValueNone -> 0L<Cent>
696+
697+
604698
/// calculates an amortisation schedule detailing how elements (principal, fee, interest and charges) are paid off over time
605699
let internal calculate (p: Parameters) initialStats (appliedPayments: Map<int<OffsetDay>, AppliedPayment>) =
606700
// get the evaluation day (the day the schedule is evaluated) based on the evaluation date in the schedule parameters
@@ -627,7 +721,7 @@ module Amortisation =
627721

628722
// generate the amortisation schedule
629723
|> Array.scan
630-
(fun ((previousDay, previous), a) (currentDay, current) ->
724+
(fun ((previousDay, previous), totals) (currentDay, current) ->
631725
// determine the window and increment every time a new scheduled payment is due
632726
let window =
633727
if ScheduledPayment.isSome current.ScheduledPayment then
@@ -649,43 +743,25 @@ module Amortisation =
649743

650744
// of the actual payments made on the day, sum any that are confirmed or written off
651745
let confirmedPaymentTotal =
652-
current.ActualPayments
653-
|> Array.sumBy (fun ap ->
654-
match ap.ActualPaymentStatus with
655-
| ActualPaymentStatus.Confirmed ap -> ap
656-
| ActualPaymentStatus.WriteOff ap -> ap
657-
| _ -> 0L<Cent>
658-
)
746+
current.ActualPayments |> Array.sumBy ActualPayment.totalConfirmedOrWrittenOff
659747

660748
// cap the actuarial interest against the total interest cap
661749
let cappedActuarialInterestM =
662750
Interest.Cap.cappedAddedValue
663751
p.Basic.InterestConfig.Cap.TotalAmount
664752
p.Basic.Principal
665-
a.CumulativeActuarialInterestM
753+
totals.CumulativeActuarialInterestM
666754
actuarialInterestM
667755

668-
// apply the cumulative actuarial interest to the accumulator
669-
let accumulator = {
670-
a with
671-
CumulativeActuarialInterestM = a.CumulativeActuarialInterestM + cappedActuarialInterestM
672-
}
673-
674756
// calculate any new interest accrued since the previous item, according to the interest method supplied in the schedule parameters
675757
let newInterestM =
676-
match p.Basic.InterestConfig.Method with
677-
| Interest.Method.AddOn ->
678-
if previous.BalanceStatus <> ClosedBalance then
679-
a.CumulativeActuarialInterestM + cappedActuarialInterestM
680-
|> fun i ->
681-
if i > initialInterestBalanceM then
682-
i - initialInterestBalanceM
683-
else
684-
0m<Cent>
685-
|> min cappedActuarialInterestM
686-
else
687-
0m<Cent>
688-
| Interest.Method.Actuarial -> actuarialInterestM
758+
calculateNewInterest
759+
p.Basic.InterestConfig.Method
760+
previous.BalanceStatus
761+
totals.CumulativeActuarialInterestM
762+
cappedActuarialInterestM
763+
initialInterestBalanceM
764+
actuarialInterestM
689765

690766
// ignore small amounts of interest that have accumulated by the last day of the schedule, with the allowance being proportional to the length of the schedule
691767
let calculateSettlementReduction m =
@@ -700,7 +776,7 @@ module Amortisation =
700776
Interest.Cap.cappedAddedValue
701777
p.Basic.InterestConfig.Cap.TotalAmount
702778
p.Basic.Principal
703-
a.CumulativeInterest
779+
totals.CumulativeInterest
704780
newInterestM
705781

706782
let sr = calculateSettlementReduction cni |> max 0m<Cent>
@@ -712,12 +788,7 @@ module Amortisation =
712788

713789
// of the actual payments made on the day, sum any that are still pending
714790
let pendingPaymentTotal =
715-
current.ActualPayments
716-
|> Array.sumBy (fun ap ->
717-
match ap.ActualPaymentStatus with
718-
| ActualPaymentStatus.Pending ap -> ap
719-
| _ -> 0L<Cent>
720-
)
791+
current.ActualPayments |> Array.sumBy ActualPayment.totalPending
721792

722793
// apportion the interest
723794
let interestPortionM =
@@ -732,20 +803,22 @@ module Amortisation =
732803
let interestPortionL = interestPortionM |> Cent.fromDecimalCent interestRounding
733804

734805
// update the accumulator
735-
let accumulator = {
736-
accumulator with
806+
let totals' = {
807+
totals with
808+
CumulativeActuarialInterestM = totals.CumulativeActuarialInterestM + cappedActuarialInterestM
737809
CumulativeScheduledPayments =
738-
a.CumulativeScheduledPayments + ScheduledPayment.total current.ScheduledPayment
810+
totals.CumulativeScheduledPayments
811+
+ ScheduledPayment.total current.ScheduledPayment
739812
CumulativeActualPayments =
740-
a.CumulativeActualPayments + confirmedPaymentTotal + pendingPaymentTotal
741-
CumulativeInterest = a.CumulativeInterest + cappedNewInterestM
813+
totals.CumulativeActualPayments + confirmedPaymentTotal + pendingPaymentTotal
814+
CumulativeInterest = totals.CumulativeInterest + cappedNewInterestM
742815
}
743816

744817
// keep track of any excess payments made to offset against future payments due
745818
let extraPaymentsBalance =
746-
a.CumulativeActualPayments
747-
- a.CumulativeScheduledPayments
748-
- a.CumulativeGeneratedPayments
819+
totals.CumulativeActualPayments
820+
- totals.CumulativeScheduledPayments
821+
- totals.CumulativeGeneratedPayments
749822

750823
// get the payment due
751824
let paymentDue =
@@ -788,13 +861,12 @@ module Amortisation =
788861

789862
// get the rounded cumulative actuarial interest
790863
let cumulativeActuarialInterestL =
791-
accumulator.CumulativeActuarialInterestM
792-
|> Cent.fromDecimalCent interestRounding
864+
totals'.CumulativeActuarialInterestM |> Cent.fromDecimalCent interestRounding
793865

794866
// get the basic settlement figure
795867
let settlement =
796868
p.Basic.Principal + cumulativeActuarialInterestL
797-
- accumulator.CumulativeActualPayments
869+
- totals'.CumulativeActualPayments
798870
|> fun s ->
799871
if abs s < int64 appliedPaymentCount * 1L<Cent> then
800872
0L<Cent>
@@ -810,25 +882,14 @@ module Amortisation =
810882

811883
// determine whether an interest adjustment is required based on the difference between cumulative actuarial interest and the initial interest balance
812884
let interestAdjustmentM =
813-
match p.Basic.InterestConfig.Method with
814-
| Interest.Method.AddOn when previous.BalanceStatus <> ClosedBalance ->
815-
let interestAdjustment =
816-
if
817-
(current.GeneratedPayment = ToBeGenerated || settlement <= 0L<Cent>)
818-
&& previous.BalanceStatus <> RefundDue
819-
&& cappedNewInterestM = 0m<Cent>
820-
then // cappedNewInterest check here avoids adding an interest adjustment twice (one for generated payment, one for final payment)
821-
accumulator.CumulativeActuarialInterestM - initialInterestBalanceM
822-
|> Interest.ignoreFractionalCents 1
823-
|> Interest.Cap.cappedAddedValue
824-
p.Basic.InterestConfig.Cap.TotalAmount
825-
p.Basic.Principal
826-
accumulator.CumulativeActuarialInterestM
827-
else
828-
0m<Cent>
829-
830-
interestAdjustment
831-
| _ -> 0m<Cent>
885+
calculateInterestAdjustment
886+
previous.BalanceStatus
887+
current.GeneratedPayment
888+
settlement
889+
cappedNewInterestM
890+
totals'.CumulativeActuarialInterestM
891+
initialInterestBalanceM
892+
p.Basic
832893

833894
// refine the capped new interest value using any interest adjustment
834895
let cappedNewInterestM' = cappedNewInterestM + interestAdjustmentM
@@ -844,7 +905,7 @@ module Amortisation =
844905
|> Interest.Cap.cappedAddedValue
845906
p.Basic.InterestConfig.Cap.TotalAmount
846907
p.Basic.Principal
847-
(Cent.toDecimalCent a.CumulativeInterestPortions)
908+
(Cent.toDecimalCent totals.CumulativeInterestPortions)
848909
|> Cent.fromDecimalCent interestRounding
849910

850911
// determine how much of the net effect can be apportioned and whether any immediate adjustments need to be made to the scheduled payment due to charges and interest, depending on settings
@@ -864,43 +925,17 @@ module Amortisation =
864925

865926
// apportion the fee
866927
let feePortion =
867-
match p.Basic.FeeConfig with
868-
| ValueSome feeConfig ->
869-
match feeConfig.FeeAmortisation with
870-
| Fee.FeeAmortisation.AmortiseBeforePrincipal -> min previous.FeeBalance assignable
871-
| Fee.FeeAmortisation.AmortiseProportionately ->
872-
feePercentage p.Basic.Principal feeTotal
873-
|> Percent.toDecimal
874-
|> fun m ->
875-
if (1m + m) = 0m then
876-
0L<Cent>
877-
else
878-
decimal assignable * m / (1m + m)
879-
|> Cent.round RoundUp
880-
|> max 0L<Cent>
881-
|> min previous.FeeBalance
882-
| ValueNone -> 0L<Cent>
928+
apportionFee p.Basic.FeeConfig previous.FeeBalance assignable p.Basic.Principal feeTotal
883929

884930
// determine the value of any fee rebate in the event of settlement, depending on settings
885931
let feeRebateIfSettled =
886-
match p.Advanced.FeeConfig with
887-
| ValueSome feeConfig ->
888-
match feeConfig.SettlementRebate with
889-
| Fee.SettlementRebate.ProRata ->
890-
let originalFinalPaymentDay =
891-
p.Basic.ScheduleConfig
892-
|> generatePaymentMap p.Basic.StartDate
893-
|> Map.keys
894-
|> Seq.toArray
895-
|> Array.tryLast
896-
|> Option.defaultValue 0<OffsetDay>
897-
898-
calculateFee feeTotal currentDay originalFinalPaymentDay
899-
| Fee.SettlementRebate.ProRataRescheduled originalFinalPaymentDay ->
900-
calculateFee feeTotal currentDay originalFinalPaymentDay
901-
| Fee.SettlementRebate.Balance -> a.CumulativeFee
902-
| Fee.SettlementRebate.Zero -> 0L<Cent>
903-
| ValueNone -> 0L<Cent>
932+
calculateFeeRebate
933+
p.Advanced.FeeConfig
934+
p.Basic.ScheduleConfig
935+
p.Basic.StartDate
936+
feeTotal
937+
currentDay
938+
totals'.CumulativeFee
904939

905940
// refine the settlement figure depending on the interest method
906941
let generatedSettlementPayment' =
@@ -1119,17 +1154,16 @@ module Amortisation =
11191154
| ToBeGenerated, SettlementDay.SettlementOnEvaluationDay -> createScheduleItem false
11201155

11211156
// refine the accumulator values
1122-
let accumulator' = {
1123-
accumulator with
1124-
CumulativeScheduledPayments =
1125-
accumulator.CumulativeScheduledPayments + scheduledPaymentAdjustment
1126-
CumulativeGeneratedPayments = a.CumulativeGeneratedPayments + generatedPayment
1127-
CumulativeFee = a.CumulativeFee + feePortion'
1128-
CumulativeInterest = accumulator.CumulativeInterest - interestRoundingDifferenceM
1129-
CumulativeInterestPortions = a.CumulativeInterestPortions + scheduleItem.InterestPortion
1157+
let totals'' = {
1158+
totals' with
1159+
CumulativeScheduledPayments = totals'.CumulativeScheduledPayments + scheduledPaymentAdjustment
1160+
CumulativeGeneratedPayments = totals.CumulativeGeneratedPayments + generatedPayment
1161+
CumulativeFee = totals.CumulativeFee + feePortion'
1162+
CumulativeInterest = totals'.CumulativeInterest - interestRoundingDifferenceM
1163+
CumulativeInterestPortions = totals.CumulativeInterestPortions + scheduleItem.InterestPortion
11301164
}
11311165
// return the values for the next scan iteration
1132-
(offsetDay, scheduleItem), accumulator'
1166+
(offsetDay, scheduleItem), totals''
11331167
)
11341168
(
11351169
// initialise the values for the scan

0 commit comments

Comments
 (0)