@@ -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 -> 0 m< Cent>
616+ | Interest.Method.AddOn ->
617+ cumulativeActuarialInterestM + cappedActuarialInterestM
618+ |> fun i ->
619+ if i > initialInterestBalanceM then
620+ i - initialInterestBalanceM
621+ else
622+ 0 m< 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 <= 0 L< Cent>)
639+ && previousBalanceStatus <> RefundDue
640+ && cappedNewInterestM = 0 m< 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+ | _ -> 0 m< 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 1 m + m = 0 m then
661+ 0 L< Cent>
662+ else
663+ decimal assignable * m / ( 1 m + m)
664+ |> Cent.round RoundUp
665+ |> max 0 L< Cent>
666+ |> min previousFeeBalance
667+ | ValueNone -> 0 L< 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 -> 0 L< Cent>
695+ | ValueNone -> 0 L< 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- | _ -> 0 L< 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- 0 m< Cent>
685- |> min cappedActuarialInterestM
686- else
687- 0 m< 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 0 m< 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- | _ -> 0 L< 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 * 1 L< Cent> then
800872 0 L< 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 <= 0 L< Cent>)
818- && previous.BalanceStatus <> RefundDue
819- && cappedNewInterestM = 0 m< 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- 0 m< Cent>
829-
830- interestAdjustment
831- | _ -> 0 m< 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 ( 1 m + m) = 0 m then
876- 0 L< Cent>
877- else
878- decimal assignable * m / ( 1 m + m)
879- |> Cent.round RoundUp
880- |> max 0 L< Cent>
881- |> min previous.FeeBalance
882- | ValueNone -> 0 L< 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 -> 0 L< Cent>
903- | ValueNone -> 0 L< 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