diff --git a/src/PaymentSchedule.fs b/src/PaymentSchedule.fs index b16caf5..62da10e 100644 --- a/src/PaymentSchedule.fs +++ b/src/PaymentSchedule.fs @@ -26,15 +26,29 @@ module PaymentSchedule = member x.Html = formatCent x.Value + /// a rescheduled payment, including the day on which the payment was created + [] + type RescheduledPayment = + { + /// the original payment amount + Value: int64 + /// the day on which the rescheduled payment was created + RescheduleDay: int + } + member x.Html = + formatCent x.Value + /// any original or rescheduled payment, affecting how any payment due is calculated [] type ScheduledPayment = { /// any original payment Original: OriginalPayment voption - /// any rescheduled payment + /// the payment relating to the latest rescheduling, if any /// > NB: if set to `ValueSome 0L` this indicates that the original payment is no longer due - Rescheduled: int64 voption + Rescheduled: RescheduledPayment voption + /// any payments relating to previous reschedulings *sorted in creation order*, if any + PreviousRescheduled: RescheduledPayment array voption /// any adjustment due to interest or charges being applied to the relevant payment rather than being amortised later Adjustment: int64 voption /// any reference numbers or other information pertaining to this payment @@ -42,9 +56,9 @@ module PaymentSchedule = } /// the total amount of the payment member x.Total = - match x.Original, x.Rescheduled with + match x.Original, x.Rescheduled with | _, ValueSome r -> - r + r.Value | ValueSome o, ValueNone -> o.Value | ValueNone, ValueNone -> @@ -63,6 +77,7 @@ module PaymentSchedule = { Original = ValueNone Rescheduled = ValueNone + PreviousRescheduled = ValueNone Adjustment = ValueNone Metadata = Map.empty } @@ -74,13 +89,14 @@ module PaymentSchedule = } /// HTML formatting to display the scheduled payment in a concise way member x.Html = + let previous = if x.PreviousRescheduled.IsSome then x.PreviousRescheduled.Value |> Array.map(fun pr -> $"r {formatCent pr.Value} ") |> Array.reduce (+) else "" match x.Original, x.Rescheduled with | ValueSome o, ValueSome r -> - $"""{formatCent o.Value} {formatCent r}""" + $"o {formatCent o.Value} {previous}r {formatCent r.Value}" | ValueSome o, ValueNone -> $"original {formatCent o.Value}" | ValueNone, ValueSome r -> - $"rescheduled {formatCent r}" + $"""{(if previous = "" then "rescheduled " else previous)}{formatCent r.Value}""" | ValueNone, ValueNone -> "" |> fun s -> @@ -200,15 +216,15 @@ module PaymentSchedule = type ScheduleType = /// an original schedule | Original - /// a schedule based on a previous one - | Rescheduled + /// a new schedule created after the original schedule, indicating the day it was created + | Rescheduled of RescheduleDay: int /// a regular schedule based on a unit-period config with a specific number of payments of a specified amount [] type FixedSchedule = { UnitPeriodConfig: UnitPeriod.Config PaymentCount: int - PaymentAmount: int64 + PaymentValue: int64 ScheduleType: ScheduleType } @@ -380,11 +396,11 @@ module PaymentSchedule = else generatePaymentSchedule rfs.PaymentCount ValueNone Direction.Forward rfs.UnitPeriodConfig |> Array.map (OffsetDay.fromDate startDate) |> Array.map(fun d -> - let originalAmount, rescheduledAmount = + let originalValue, rescheduledValue = match rfs.ScheduleType with - | ScheduleType.Original -> ValueSome rfs.PaymentAmount, ValueNone - | ScheduleType.Rescheduled -> ValueNone, ValueSome rfs.PaymentAmount - d, ScheduledPayment.Quick originalAmount rescheduledAmount + | ScheduleType.Original -> ValueSome rfs.PaymentValue, ValueNone + | ScheduleType.Rescheduled rescheduleDay -> ValueNone, ValueSome { Value = rfs.PaymentValue; RescheduleDay = rescheduleDay } + d, ScheduledPayment.Quick originalValue rescheduledValue ) ) |> Array.concat @@ -438,7 +454,7 @@ module PaymentSchedule = let mutable schedule = [||] - let toleranceSteps = ValueSome <| ToleranceSteps.forPaymentAmount paymentCount + let toleranceSteps = ValueSome <| ToleranceSteps.forPaymentValue paymentCount let calculateInterest interestMethod payment previousItem day = match interestMethod with @@ -470,7 +486,7 @@ module PaymentSchedule = TotalPrincipal = previousItem.TotalPrincipal + principalPortion } - let generatePaymentAmount firstItem interestMethod roughPayment = + let generatePaymentValue firstItem interestMethod roughPayment = let scheduledPayment = roughPayment |> Cent.round (ValueSome sp.Calculation.RoundingOptions.PaymentRounding) @@ -513,7 +529,7 @@ module PaymentSchedule = let solution = match sp.ScheduleConfig with | AutoGenerateSchedule _ -> - Array.solve (generatePaymentAmount initialItem sp.Interest.Method) 100 (totalAddOnInterest |> decimal |> calculateLevelPayment) toleranceOption toleranceSteps + Array.solve (generatePaymentValue initialItem sp.Interest.Method) 100 (totalAddOnInterest |> decimal |> calculateLevelPayment) toleranceOption toleranceSteps | FixedSchedules _ | CustomSchedule _ -> schedule <- @@ -542,7 +558,7 @@ module PaymentSchedule = |> ValueOption.map(fun p -> { p with Original = if p.Rescheduled.IsNone then p.Original |> ValueOption.map(fun o -> { o with Value = o.Value + si.PrincipalBalance }) else p.Original - Rescheduled = if p.Rescheduled.IsSome then p.Rescheduled |> ValueOption.map(fun r -> r + si.PrincipalBalance) else p.Rescheduled + Rescheduled = if p.Rescheduled.IsSome then p.Rescheduled |> ValueOption.map(fun r -> { r with Value = r.Value + si.PrincipalBalance }) else p.Rescheduled } ) let adjustedPrincipal = si.PrincipalPortion + si.PrincipalBalance @@ -562,7 +578,7 @@ module PaymentSchedule = let aprSolution = items |> Array.filter _.ScheduledPayment.IsSome - |> Array.map(fun si -> { Apr.TransferType = Apr.Payment; Apr.TransferDate = sp.StartDate.AddDays(int si.Day); Apr.Amount = si.ScheduledPayment.Value.Total }) + |> Array.map(fun si -> { Apr.TransferType = Apr.Payment; Apr.TransferDate = sp.StartDate.AddDays(int si.Day); Apr.Value = si.ScheduledPayment.Value.Total }) |> Apr.calculate sp.Calculation.AprMethod sp.Principal sp.StartDate let finalPayment = items @@ -588,25 +604,35 @@ module PaymentSchedule = | _ -> ValueNone - let mergeScheduledPayments rescheduleDay (scheduledPayments: (int * ScheduledPayment) array) = + let mergeScheduledPayments (scheduledPayments: (int * ScheduledPayment) array) = + let mutable previousRescheduleDay = ValueOption>.None scheduledPayments |> Array.groupBy fst - |> Array.map(fun (offsetDay, scheduledPayments) -> // for each day, there will only ever be a maximum of one original and one rescheduled payment + |> Array.map(fun (offsetDay, scheduledPayments) -> let original = scheduledPayments |> Array.tryFind (snd >> _.Original.IsSome) |> toValueOption |> ValueOption.map snd - let rescheduled = scheduledPayments |> Array.tryFind (snd >> _.Rescheduled.IsSome) |> toValueOption |> ValueOption.map snd + let rescheduled = scheduledPayments |> Array.filter (snd >> _.Rescheduled.IsSome) |> Array.map snd |> Array.sortByDescending _.Rescheduled.Value.RescheduleDay |> Array.toList + let latestRescheduling, previousReschedulings = + match rescheduled with + | r :: [] -> ValueSome r, ValueNone + | r :: pr -> ValueSome r, ValueSome pr + | _ -> ValueNone, ValueNone + let rescheduleDay = latestRescheduling |> ValueOption.map _.Rescheduled.Value.RescheduleDay |> ValueOption.orElse previousRescheduleDay + previousRescheduleDay <- rescheduleDay let scheduledPayment = - match original, rescheduled with + match original, latestRescheduling with | _, ValueSome r -> { Original = original |> ValueOption.bind _.Original Rescheduled = r.Rescheduled + PreviousRescheduled = previousReschedulings |> ValueOption.map (List.rev >> List.map _.Rescheduled.Value >> List.toArray) Adjustment = r.Adjustment Metadata = r.Metadata } | ValueSome o, ValueNone -> { Original = o.Original - Rescheduled = if offsetDay >= rescheduleDay then ValueSome 0L else ValueNone //overwrite original scheduled payments from start of rescheduled payments + Rescheduled = previousRescheduleDay |> ValueOption.bind(fun prd -> if offsetDay >= prd then ValueSome { Value = 0L; RescheduleDay = prd } else ValueNone) //overwrite original scheduled payments from start of rescheduled payments + PreviousRescheduled = ValueNone Adjustment = o.Adjustment Metadata = o.Metadata } diff --git a/src/Rescheduling.fs b/src/Rescheduling.fs index f3b989a..408ba68 100644 --- a/src/Rescheduling.fs +++ b/src/Rescheduling.fs @@ -15,8 +15,6 @@ module Rescheduling = /// the parameters used for setting up additional items for an existing schedule or new items for a new schedule [] type RescheduleParameters = { - /// which day the reschedule takes effect: existing payments after this day will be cancelled or replaced by rescheduled payments - RescheduleDay: int /// whether the fees should be pro-rated or due in full in the new schedule FeeSettlementRefund: Fee.SettlementRefund /// whether the plan is autogenerated or a manual plan provided @@ -48,11 +46,15 @@ module Rescheduling = |> Array.map(fun si -> si.Day, si.ScheduledPayment.Value) | ValueNone -> [||] - | FixedSchedules regularFixedSchedules -> - regularFixedSchedules - |> Array.map(fun rfs -> - UnitPeriod.generatePaymentSchedule rfs.PaymentCount ValueNone UnitPeriod.Direction.Forward rfs.UnitPeriodConfig - |> Array.map(fun d -> OffsetDay.fromDate sp.StartDate d, ScheduledPayment.Quick ValueNone (ValueSome rfs.PaymentAmount)) + | FixedSchedules fixedSchedules -> + fixedSchedules + |> Array.map(fun fs -> + let scheduledPayment = + match fs.ScheduleType with + | ScheduleType.Original -> ScheduledPayment.Quick (ValueSome fs.PaymentValue) ValueNone + | ScheduleType.Rescheduled rescheduleDay -> ScheduledPayment.Quick ValueNone (ValueSome { Value = fs.PaymentValue; RescheduleDay = rescheduleDay }) + UnitPeriod.generatePaymentSchedule fs.PaymentCount ValueNone UnitPeriod.Direction.Forward fs.UnitPeriodConfig + |> Array.map(fun d -> OffsetDay.fromDate sp.StartDate d, scheduledPayment) ) |> Array.concat | CustomSchedule payments -> @@ -67,7 +69,7 @@ module Rescheduling = // configure the parameters for the new schedule let spNew = { sp with - ScheduleConfig = [| oldPaymentSchedule; newPaymentSchedule |] |> Array.concat |> mergeScheduledPayments rp.RescheduleDay |> CustomSchedule + ScheduleConfig = [| oldPaymentSchedule; newPaymentSchedule |] |> Array.concat |> mergeScheduledPayments |> CustomSchedule FeeConfig.SettlementRefund = rp.FeeSettlementRefund ChargeConfig.ChargeHolidays = rp.ChargeHolidays Interest = diff --git a/tests/ActualPaymentTestsExtra.fs b/tests/ActualPaymentTestsExtra.fs index 1eae243..ff1f842 100644 --- a/tests/ActualPaymentTestsExtra.fs +++ b/tests/ActualPaymentTestsExtra.fs @@ -252,10 +252,10 @@ module ActualPaymentTestsExtra = Map [ 0, [| ActualPayment.QuickConfirmed 166_60L |] ] + let rescheduleDay = sp.AsOfDate |> OffsetDay.fromDate sp.StartDate let rp : RescheduleParameters = { - RescheduleDay = sp.AsOfDate |> OffsetDay.fromDate sp.StartDate FeeSettlementRefund = Fee.SettlementRefund.ProRata (ValueSome originalFinalPaymentDay') - PaymentSchedule = FixedSchedules [| { UnitPeriodConfig = UnitPeriod.Config.Weekly(2, Date(2022, 9, 1)); PaymentCount = 155; PaymentAmount = 20_00L; ScheduleType = ScheduleType.Rescheduled } |] + PaymentSchedule = FixedSchedules [| { UnitPeriodConfig = UnitPeriod.Config.Weekly(2, Date(2022, 9, 1)); PaymentCount = 155; PaymentValue = 20_00L; ScheduleType = ScheduleType.Rescheduled rescheduleDay } |] RateOnNegativeBalance = ValueNone PromotionalInterestRates = [||] ChargeHolidays = [||] @@ -273,7 +273,7 @@ module ActualPaymentTestsExtra = let expected = ValueSome (1969, { OffsetDate = Date(2027, 7, 29) Advances = [||] - ScheduledPayment = ScheduledPayment.Quick ValueNone (ValueSome 20_00L) + ScheduledPayment = ScheduledPayment.Quick ValueNone (ValueSome { Value = 20_00L; RescheduleDay = 176 }) Window = 141 PaymentDue = 9_80L ActualPayments = [||] diff --git a/tests/EdgeCaseTests.fs b/tests/EdgeCaseTests.fs index ec04840..c0a766b 100644 --- a/tests/EdgeCaseTests.fs +++ b/tests/EdgeCaseTests.fs @@ -635,11 +635,12 @@ module EdgeCaseTests = let originalFinalPaymentDay = ((Date(2024, 5, 22) - Date(2024, 2, 2)).Days) * 1 + let rescheduleDay = sp.AsOfDate |> OffsetDay.fromDate sp.StartDate + let (rp: RescheduleParameters) = { - RescheduleDay = sp.AsOfDate |> OffsetDay.fromDate sp.StartDate FeeSettlementRefund = Fee.SettlementRefund.ProRata (ValueSome originalFinalPaymentDay) PaymentSchedule = CustomSchedule <| Map [ - 58, ScheduledPayment.Quick ValueNone (ValueSome 5000L) + 58, ScheduledPayment.Quick ValueNone (ValueSome { Value = 5000L; RescheduleDay = rescheduleDay }) ] RateOnNegativeBalance = ValueSome (Interest.Rate.Annual (Percent 8m)) PromotionalInterestRates = [||] @@ -718,11 +719,12 @@ module EdgeCaseTests = let originalFinalPaymentDay = ((Date(2024, 5, 22) - Date(2024, 2, 2)).Days) * 1 + let rescheduleDay = sp.AsOfDate |> OffsetDay.fromDate sp.StartDate + let (rp: RescheduleParameters) = { - RescheduleDay = sp.AsOfDate |> OffsetDay.fromDate sp.StartDate FeeSettlementRefund = Fee.SettlementRefund.ProRata (ValueSome originalFinalPaymentDay) PaymentSchedule = CustomSchedule <| Map [ - 58, ScheduledPayment.Quick ValueNone (ValueSome 5000L) + 58, ScheduledPayment.Quick ValueNone (ValueSome { Value = 5000L; RescheduleDay = rescheduleDay }) ] RateOnNegativeBalance = ValueSome (Interest.Rate.Annual (Percent 8m)) PromotionalInterestRates = [||] diff --git a/tests/InterestFirstTests.fs b/tests/InterestFirstTests.fs index 449ab63..63f87f3 100644 --- a/tests/InterestFirstTests.fs +++ b/tests/InterestFirstTests.fs @@ -13,6 +13,7 @@ module InterestFirstTests = open Currency open DateDay open Formatting + open MapExtension open PaymentSchedule open Percentages open ValueOptionCE @@ -385,210 +386,212 @@ module InterestFirstTests = let initialInterestBalance = schedule |> ValueOption.map (fun s -> s.ScheduleItems[0].InterestBalance) |> ValueOption.defaultValue 0m initialInterestBalance |> should equal 362_34m - // [] - // let ``18) Realistic test 6045bd0ffc0f with correction on final day`` () = - // let sp = - // { scheduleParameters with - // AsOfDate = Date(2024, 9, 17) - // StartDate = Date(2023, 9, 22) - // Principal = 740_00L - // PaymentSchedule = FixedSchedules [| { UnitPeriodConfig = UnitPeriod.Config.Monthly(1, 2023, 9, 29); PaymentCount = 4; PaymentAmount = 293_82L } |] - // Parameters.Interest.RateOnNegativeBalance = ValueNone - // // PaymentSchedule = IrregularSchedule <| Map [ - // // // 14, CustomerPayment.ScheduledOriginal 33004L - // // // 37, CustomerPayment.ScheduledOriginal 33004L - // // // 68, CustomerPayment.ScheduledOriginal 33004L - // // // 98, CustomerPayment.ScheduledOriginal 33004L - // // 42, CustomerPayment.ScheduledRescheduled 20000L - // // // 49, CustomerPayment.ScheduledRescheduled 20000L - // // // 56, CustomerPayment.ScheduledRescheduled 20000L - // // // 63, CustomerPayment.ScheduledRescheduled 20000L - // // // 70, CustomerPayment.ScheduledRescheduled 20000L - // // // 77, CustomerPayment.ScheduledRescheduled 20000L - // // // 84, CustomerPayment.ScheduledRescheduled 20000L - // // // 91, CustomerPayment.ScheduledRescheduled 8000L - // // 56, CustomerPayment.ScheduledRescheduled 5000L - // // // 86, CustomerPayment.ScheduledRescheduled 5000L - // // // 117, CustomerPayment.ScheduledRescheduled 5000L - // // // 148, CustomerPayment.ScheduledRescheduled 5000L - // // // 177, CustomerPayment.ScheduledRescheduled 15000L - // // // 208, CustomerPayment.ScheduledRescheduled 15000L - // // // 238, CustomerPayment.ScheduledRescheduled 15000L - // // // 269, CustomerPayment.ScheduledRescheduled 15000L - // // // 299, CustomerPayment.ScheduledRescheduled 15000L - // // // 330, CustomerPayment.ScheduledRescheduled 15000L - // // // 361, CustomerPayment.ScheduledRescheduled 18000L - // // 119, CustomerPayment.ScheduledRescheduled 3500L - // // 126, CustomerPayment.ScheduledRescheduled 3500L - // // 133, CustomerPayment.ScheduledRescheduled 3500L - // // 140, CustomerPayment.ScheduledRescheduled 3500L - // // 147, CustomerPayment.ScheduledRescheduled 3500L - // // 154, CustomerPayment.ScheduledRescheduled 3500L - // // 161, CustomerPayment.ScheduledRescheduled 3500L - // // 168, CustomerPayment.ScheduledRescheduled 3500L - // // 175, CustomerPayment.ScheduledRescheduled 3500L - // // 182, CustomerPayment.ScheduledRescheduled 3500L - // // 189, CustomerPayment.ScheduledRescheduled 3500L - // // 196, CustomerPayment.ScheduledRescheduled 3500L - // // 203, CustomerPayment.ScheduledRescheduled 3500L - // // 210, CustomerPayment.ScheduledRescheduled 3500L - // // 217, CustomerPayment.ScheduledRescheduled 3500L - // // 224, CustomerPayment.ScheduledRescheduled 3500L - // // 231, CustomerPayment.ScheduledRescheduled 3500L - // // 238, CustomerPayment.ScheduledRescheduled 3500L - // // 245, CustomerPayment.ScheduledRescheduled 3500L - // // 252, CustomerPayment.ScheduledRescheduled 3500L - // // 259, CustomerPayment.ScheduledRescheduled 3500L - // // 266, CustomerPayment.ScheduledRescheduled 3500L - // // 273, CustomerPayment.ScheduledRescheduled 3500L - // // 280, CustomerPayment.ScheduledRescheduled 3500L - // // 287, CustomerPayment.ScheduledRescheduled 3500L - // // 294, CustomerPayment.ScheduledRescheduled 3500L - // // 301, CustomerPayment.ScheduledRescheduled 3500L - // // 308, CustomerPayment.ScheduledRescheduled 3500L - // // 315, CustomerPayment.ScheduledRescheduled 3500L - // // 322, CustomerPayment.ScheduledRescheduled 3500L - // // 329, CustomerPayment.ScheduledRescheduled 3500L - // // 336, CustomerPayment.ScheduledRescheduled 3500L - // // 343, CustomerPayment.ScheduledRescheduled 3500L - // // 350, CustomerPayment.ScheduledRescheduled 3500L - // // 357, CustomerPayment.ScheduledRescheduled 4000L - // // ] - // } - - // let actualPayments = - // Map [ - // // 14, [| ActualPayment.QuickFailed 33004L [||] |] - // // 14, [| ActualPayment.QuickFailed 33004L [||] |] - // // 14, [| ActualPayment.QuickFailed 33004L [||] |] - // // 42, [| ActualPayment.QuickFailed 20000L [||] |] - // 42, [| ActualPayment.QuickConfirmed 20000L |] - // // 56, [| ActualPayment.QuickFailed 5000L [||] |] - // 56, [| ActualPayment.QuickConfirmed 5000L |] - // // 86, [| ActualPayment.QuickFailed 5000L [||] |] - // // 86, [| ActualPayment.QuickFailed 5000L [||] |] - // // 86, [| ActualPayment.QuickFailed 5000L [||] |] - // // 89, [| ActualPayment.QuickFailed 5000L [||] |] - // // 89, [| ActualPayment.QuickFailed 5000L [||] |] - // // 89, [| ActualPayment.QuickFailed 5000L [||] |] - // // 92, [| ActualPayment.QuickFailed 5000L [||] |] - // // 92, [| ActualPayment.QuickFailed 5000L [||] |] - // // 92, [| ActualPayment.QuickFailed 5000L [||] |] - // // 119, [| ActualPayment.QuickFailed 3500L [||] |] - // // 119, [| ActualPayment.QuickFailed 3500L [||] |] - // // 119, [| ActualPayment.QuickFailed 3500L [||] |] - // // 122, [| ActualPayment.QuickFailed 3500L [||] |] - // // 122, [| ActualPayment.QuickFailed 3500L [||] |] - // // 122, [| ActualPayment.QuickFailed 3500L [||] |] - // // 125, [| ActualPayment.QuickFailed 3500L [||] |] - // // 125, [| ActualPayment.QuickFailed 3500L [||] |] - // // 125, [| ActualPayment.QuickFailed 3500L [||] |] - // // 126, [| ActualPayment.QuickFailed 3500L [||] |] - // // 126, [| ActualPayment.QuickFailed 3500L [||] |] - // // 126, [| ActualPayment.QuickFailed 3500L [||] |] - // // 129, [| ActualPayment.QuickFailed 3500L [||] |] - // // 129, [| ActualPayment.QuickFailed 3500L [||] |] - // // 129, [| ActualPayment.QuickFailed 3500L [||] |] - // // 132, [| ActualPayment.QuickFailed 3500L [||] |] - // // 132, [| ActualPayment.QuickFailed 3500L [||] |] - // // 132, [| ActualPayment.QuickFailed 3500L [||] |] - // // 132, [| ActualPayment.QuickFailed 3500L [||] |] - // // 133, [| ActualPayment.QuickFailed 3500L [||] |] - // 133, [| ActualPayment.QuickConfirmed 3500L |] - // // 133, [| ActualPayment.QuickFailed 3500L [||] |] - // // 136, [| ActualPayment.QuickFailed 3500L [||] |] - // // 136, [| ActualPayment.QuickFailed 3500L [||] |] - // // 136, [| ActualPayment.QuickFailed 3500L [||] |] - // // 139, [| ActualPayment.QuickFailed 3500L [||] |] - // // 139, [| ActualPayment.QuickFailed 3500L [||] |] - // // 139, [| ActualPayment.QuickFailed 3500L [||] |] - // // 140, [| ActualPayment.QuickFailed 3500L [||] |] - // // 140, [| ActualPayment.QuickFailed 3500L [||] |] - // // 140, [| ActualPayment.QuickFailed 3500L [||] |] - // // 143, [| ActualPayment.QuickFailed 3500L [||] |] - // 143, [| ActualPayment.QuickConfirmed 3500L |] - // 143, [| ActualPayment.QuickConfirmed 3500L |] - // // 146, [| ActualPayment.QuickFailed 3500L [||] |] - // // 146, [| ActualPayment.QuickFailed 3500L [||] |] - // // 146, [| ActualPayment.QuickFailed 3500L [||] |] - // // 147, [| ActualPayment.QuickFailed 3500L [||] |] - // // 147, [| ActualPayment.QuickFailed 3500L [||] |] - // 147, [| ActualPayment.QuickConfirmed 3500L |] - // // 150, [| ActualPayment.QuickFailed 3500L [||] |] - // // 150, [| ActualPayment.QuickFailed 3500L [||] |] - // // 150, [| ActualPayment.QuickFailed 3500L [||] |] - // // 153, [| ActualPayment.QuickFailed 3500L [||] |] - // // 153, [| ActualPayment.QuickFailed 3500L [||] |] - // // 153, [| ActualPayment.QuickFailed 3500L [||] |] - // // 154, [| ActualPayment.QuickFailed 3500L [||] |] - // 154, [| ActualPayment.QuickConfirmed 3500L |] - // 154, [| ActualPayment.QuickConfirmed 3500L |] - // // 161, [| ActualPayment.QuickFailed 3500L [||] |] - // // 161, [| ActualPayment.QuickFailed 3500L [||] |] - // // 161, [| ActualPayment.QuickFailed 3500L [||] |] - // 164, [| ActualPayment.QuickConfirmed 3500L |] - // // 168, [| ActualPayment.QuickFailed 3500L [||] |] - // // 168, [| ActualPayment.QuickFailed 3500L [||] |] - // // 168, [| ActualPayment.QuickFailed 3500L [||] |] - // // 171, [| ActualPayment.QuickFailed 3500L [||] |] - // // 171, [| ActualPayment.QuickFailed 3500L [||] |] - // // 171, [| ActualPayment.QuickFailed 3500L [||] |] - // // 174, [| ActualPayment.QuickFailed 3500L [||] |] - // // 174, [| ActualPayment.QuickFailed 3500L [||] |] - // // 174, [| ActualPayment.QuickFailed 3500L [||] |] - // // 175, [| ActualPayment.QuickFailed 3500L [||] |] - // 175, [| ActualPayment.QuickConfirmed 3500L |] - // 175, [| ActualPayment.QuickConfirmed 3500L |] - // // 182, [| ActualPayment.QuickFailed 3500L [||] |] - // // 182, [| ActualPayment.QuickFailed 3500L [||] |] - // 182, [| ActualPayment.QuickConfirmed 3500L |] - // // 189, [| ActualPayment.QuickFailed 3500L [||] |] - // 189, [| ActualPayment.QuickConfirmed 3500L |] - // // 196, [| ActualPayment.QuickFailed 3500L [||] |] - // // 196, [| ActualPayment.QuickFailed 3500L [||] |] - // // 196, [| ActualPayment.QuickFailed 3500L [||] |] - // // 199, [| ActualPayment.QuickFailed 3500L [||] |] - // // 199, [| ActualPayment.QuickFailed 3500L [||] |] - // // 199, [| ActualPayment.QuickFailed 3500L [||] |] - // // 202, [| ActualPayment.QuickFailed 3500L [||] |] - // // 202, [| ActualPayment.QuickFailed 3500L [||] |] - // // 202, [| ActualPayment.QuickFailed 3500L [||] |] - // // 203, [| ActualPayment.QuickFailed 3500L [||] |] - // 203, [| ActualPayment.QuickConfirmed 3500L |] - // // 203, [| ActualPayment.QuickFailed 3500L [||] |] - // // 206, [| ActualPayment.QuickFailed 3500L [||] |] - // // 206, [| ActualPayment.QuickFailed 3500L [||] |] - // 206, [| ActualPayment.QuickConfirmed 3500L |] - // 210, [| ActualPayment.QuickConfirmed 3500L |] - // // 217, [| ActualPayment.QuickFailed 3500L [||] |] - // 217, [| ActualPayment.QuickConfirmed 3500L |] - // 224, [| ActualPayment.QuickConfirmed 3500L |] - // 231, [| ActualPayment.QuickConfirmed 3500L |] - // 238, [| ActualPayment.QuickConfirmed 3500L |] - // 245, [| ActualPayment.QuickConfirmed 3500L |] - // 252, [| ActualPayment.QuickConfirmed 3500L |] - // 259, [| ActualPayment.QuickConfirmed 3500L |] - // 266, [| ActualPayment.QuickConfirmed 3500L |] - // 273, [| ActualPayment.QuickConfirmed 3500L |] - // // 280, [| ActualPayment.QuickFailed 3500L [||] |] - // 280, [| ActualPayment.QuickConfirmed 3500L |] - // 287, [| ActualPayment.QuickConfirmed 3500L |] - // 294, [| ActualPayment.QuickConfirmed 3500L |] - // 301, [| ActualPayment.QuickConfirmed 3500L |] - // 308, [| ActualPayment.QuickConfirmed 3500L |] - // 314, [| ActualPayment.QuickConfirmed 25000L |] - // 315, [| ActualPayment.QuickConfirmed 1L |] - // ] - - // let schedule = - // actualPayments - // |> Amortisation.generate sp IntendedPurpose.Statement false - - // schedule |> ValueOption.iter (_.ScheduleItems >> Formatting.outputListToHtml "out/InterestFirstTest018.md" false) - - // let totalInterestPortions = schedule |> ValueOption.map (fun s -> s.ScheduleItems |> Array.sumBy _.InterestPortion) |> ValueOption.defaultValue 0L - - // totalInterestPortions |> should equal 740_00L + [] + let ``18) Realistic test 6045bd0ffc0f with correction on final day`` () = + let sp = + { scheduleParameters with + AsOfDate = Date(2024, 9, 17) + StartDate = Date(2023, 9, 22) + Principal = 740_00L + Parameters.Interest.RateOnNegativeBalance = ValueNone + ScheduleConfig = [| + 14, ScheduledPayment.Quick (ValueSome 33004L) ValueNone + 37, ScheduledPayment.Quick (ValueSome 33004L) ValueNone + 68, ScheduledPayment.Quick (ValueSome 33004L) ValueNone + 98, ScheduledPayment.Quick (ValueSome 33004L) ValueNone + 42, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 49, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 56, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 63, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 70, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 77, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 84, ScheduledPayment.Quick ValueNone (ValueSome { Value = 20000L; RescheduleDay = 19 }) + 91, ScheduledPayment.Quick ValueNone (ValueSome { Value = 8000L; RescheduleDay = 19 }) + 56, ScheduledPayment.Quick ValueNone (ValueSome { Value = 5000L; RescheduleDay = 47 }) + 86, ScheduledPayment.Quick ValueNone (ValueSome { Value = 5000L; RescheduleDay = 47 }) + 117, ScheduledPayment.Quick ValueNone (ValueSome { Value = 5000L; RescheduleDay = 47 }) + 148, ScheduledPayment.Quick ValueNone (ValueSome { Value = 5000L; RescheduleDay = 47 }) + 177, ScheduledPayment.Quick ValueNone (ValueSome { Value = 15000L; RescheduleDay = 47 }) + 208, ScheduledPayment.Quick ValueNone (ValueSome { Value = 15000L; RescheduleDay = 47 }) + 238, ScheduledPayment.Quick ValueNone (ValueSome { Value = 15000L; RescheduleDay = 47 }) + 269, ScheduledPayment.Quick ValueNone (ValueSome { Value = 15000L; RescheduleDay = 47 }) + 299, ScheduledPayment.Quick ValueNone (ValueSome { Value = 15000L; RescheduleDay = 47 }) + 330, ScheduledPayment.Quick ValueNone (ValueSome { Value = 15000L; RescheduleDay = 47 }) + 361, ScheduledPayment.Quick ValueNone (ValueSome { Value = 18000L; RescheduleDay = 47 }) + 119, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 126, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 133, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 140, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 147, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 154, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 161, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 168, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 175, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 182, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 189, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 196, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 203, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 210, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 217, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 224, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 231, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 238, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 245, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 252, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 259, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 266, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 273, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 280, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 287, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 294, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 301, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 308, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 315, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 322, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 329, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 336, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 343, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 350, ScheduledPayment.Quick ValueNone (ValueSome { Value = 3500L; RescheduleDay = 115 }) + 357, ScheduledPayment.Quick ValueNone (ValueSome { Value = 4000L; RescheduleDay = 115 }) + |] + |> mergeScheduledPayments + |> CustomSchedule + } + + let actualPayments = + [| + 14, [| ActualPayment.QuickFailed 33004L [||] |] + 14, [| ActualPayment.QuickFailed 33004L [||] |] + 14, [| ActualPayment.QuickFailed 33004L [||] |] + 42, [| ActualPayment.QuickFailed 20000L [||] |] + 42, [| ActualPayment.QuickConfirmed 20000L |] + 56, [| ActualPayment.QuickFailed 5000L [||] |] + 56, [| ActualPayment.QuickConfirmed 5000L |] + 86, [| ActualPayment.QuickFailed 5000L [||] |] + 86, [| ActualPayment.QuickFailed 5000L [||] |] + 86, [| ActualPayment.QuickFailed 5000L [||] |] + 89, [| ActualPayment.QuickFailed 5000L [||] |] + 89, [| ActualPayment.QuickFailed 5000L [||] |] + 89, [| ActualPayment.QuickFailed 5000L [||] |] + 92, [| ActualPayment.QuickFailed 5000L [||] |] + 92, [| ActualPayment.QuickFailed 5000L [||] |] + 92, [| ActualPayment.QuickFailed 5000L [||] |] + 119, [| ActualPayment.QuickFailed 3500L [||] |] + 119, [| ActualPayment.QuickFailed 3500L [||] |] + 119, [| ActualPayment.QuickFailed 3500L [||] |] + 122, [| ActualPayment.QuickFailed 3500L [||] |] + 122, [| ActualPayment.QuickFailed 3500L [||] |] + 122, [| ActualPayment.QuickFailed 3500L [||] |] + 125, [| ActualPayment.QuickFailed 3500L [||] |] + 125, [| ActualPayment.QuickFailed 3500L [||] |] + 125, [| ActualPayment.QuickFailed 3500L [||] |] + 126, [| ActualPayment.QuickFailed 3500L [||] |] + 126, [| ActualPayment.QuickFailed 3500L [||] |] + 126, [| ActualPayment.QuickFailed 3500L [||] |] + 129, [| ActualPayment.QuickFailed 3500L [||] |] + 129, [| ActualPayment.QuickFailed 3500L [||] |] + 129, [| ActualPayment.QuickFailed 3500L [||] |] + 132, [| ActualPayment.QuickFailed 3500L [||] |] + 132, [| ActualPayment.QuickFailed 3500L [||] |] + 132, [| ActualPayment.QuickFailed 3500L [||] |] + 132, [| ActualPayment.QuickFailed 3500L [||] |] + 133, [| ActualPayment.QuickFailed 3500L [||] |] + 133, [| ActualPayment.QuickConfirmed 3500L |] + 133, [| ActualPayment.QuickFailed 3500L [||] |] + 136, [| ActualPayment.QuickFailed 3500L [||] |] + 136, [| ActualPayment.QuickFailed 3500L [||] |] + 136, [| ActualPayment.QuickFailed 3500L [||] |] + 139, [| ActualPayment.QuickFailed 3500L [||] |] + 139, [| ActualPayment.QuickFailed 3500L [||] |] + 139, [| ActualPayment.QuickFailed 3500L [||] |] + 140, [| ActualPayment.QuickFailed 3500L [||] |] + 140, [| ActualPayment.QuickFailed 3500L [||] |] + 140, [| ActualPayment.QuickFailed 3500L [||] |] + 143, [| ActualPayment.QuickFailed 3500L [||] |] + 143, [| ActualPayment.QuickConfirmed 3500L |] + 143, [| ActualPayment.QuickConfirmed 3500L |] + 146, [| ActualPayment.QuickFailed 3500L [||] |] + 146, [| ActualPayment.QuickFailed 3500L [||] |] + 146, [| ActualPayment.QuickFailed 3500L [||] |] + 147, [| ActualPayment.QuickFailed 3500L [||] |] + 147, [| ActualPayment.QuickFailed 3500L [||] |] + 147, [| ActualPayment.QuickConfirmed 3500L |] + 150, [| ActualPayment.QuickFailed 3500L [||] |] + 150, [| ActualPayment.QuickFailed 3500L [||] |] + 150, [| ActualPayment.QuickFailed 3500L [||] |] + 153, [| ActualPayment.QuickFailed 3500L [||] |] + 153, [| ActualPayment.QuickFailed 3500L [||] |] + 153, [| ActualPayment.QuickFailed 3500L [||] |] + 154, [| ActualPayment.QuickFailed 3500L [||] |] + 154, [| ActualPayment.QuickConfirmed 3500L |] + 154, [| ActualPayment.QuickConfirmed 3500L |] + 161, [| ActualPayment.QuickFailed 3500L [||] |] + 161, [| ActualPayment.QuickFailed 3500L [||] |] + 161, [| ActualPayment.QuickFailed 3500L [||] |] + 164, [| ActualPayment.QuickConfirmed 3500L |] + 168, [| ActualPayment.QuickFailed 3500L [||] |] + 168, [| ActualPayment.QuickFailed 3500L [||] |] + 168, [| ActualPayment.QuickFailed 3500L [||] |] + 171, [| ActualPayment.QuickFailed 3500L [||] |] + 171, [| ActualPayment.QuickFailed 3500L [||] |] + 171, [| ActualPayment.QuickFailed 3500L [||] |] + 174, [| ActualPayment.QuickFailed 3500L [||] |] + 174, [| ActualPayment.QuickFailed 3500L [||] |] + 174, [| ActualPayment.QuickFailed 3500L [||] |] + 175, [| ActualPayment.QuickFailed 3500L [||] |] + 175, [| ActualPayment.QuickConfirmed 3500L |] + 175, [| ActualPayment.QuickConfirmed 3500L |] + 182, [| ActualPayment.QuickFailed 3500L [||] |] + 182, [| ActualPayment.QuickFailed 3500L [||] |] + 182, [| ActualPayment.QuickConfirmed 3500L |] + 189, [| ActualPayment.QuickFailed 3500L [||] |] + 189, [| ActualPayment.QuickConfirmed 3500L |] + 196, [| ActualPayment.QuickFailed 3500L [||] |] + 196, [| ActualPayment.QuickFailed 3500L [||] |] + 196, [| ActualPayment.QuickFailed 3500L [||] |] + 199, [| ActualPayment.QuickFailed 3500L [||] |] + 199, [| ActualPayment.QuickFailed 3500L [||] |] + 199, [| ActualPayment.QuickFailed 3500L [||] |] + 202, [| ActualPayment.QuickFailed 3500L [||] |] + 202, [| ActualPayment.QuickFailed 3500L [||] |] + 202, [| ActualPayment.QuickFailed 3500L [||] |] + 203, [| ActualPayment.QuickFailed 3500L [||] |] + 203, [| ActualPayment.QuickConfirmed 3500L |] + 203, [| ActualPayment.QuickFailed 3500L [||] |] + 206, [| ActualPayment.QuickFailed 3500L [||] |] + 206, [| ActualPayment.QuickFailed 3500L [||] |] + 206, [| ActualPayment.QuickConfirmed 3500L |] + 210, [| ActualPayment.QuickConfirmed 3500L |] + 217, [| ActualPayment.QuickFailed 3500L [||] |] + 217, [| ActualPayment.QuickConfirmed 3500L |] + 224, [| ActualPayment.QuickConfirmed 3500L |] + 231, [| ActualPayment.QuickConfirmed 3500L |] + 238, [| ActualPayment.QuickConfirmed 3500L |] + 245, [| ActualPayment.QuickConfirmed 3500L |] + 252, [| ActualPayment.QuickConfirmed 3500L |] + 259, [| ActualPayment.QuickConfirmed 3500L |] + 266, [| ActualPayment.QuickConfirmed 3500L |] + 273, [| ActualPayment.QuickConfirmed 3500L |] + 280, [| ActualPayment.QuickFailed 3500L [||] |] + 280, [| ActualPayment.QuickConfirmed 3500L |] + 287, [| ActualPayment.QuickConfirmed 3500L |] + 294, [| ActualPayment.QuickConfirmed 3500L |] + 301, [| ActualPayment.QuickConfirmed 3500L |] + 308, [| ActualPayment.QuickConfirmed 3500L |] + 314, [| ActualPayment.QuickConfirmed 25000L |] + 315, [| ActualPayment.QuickConfirmed 1L |] + |] + |> Map.ofArrayWithArrayMerge + + let schedule = + actualPayments + |> Amortisation.generate sp IntendedPurpose.Statement false + + schedule |> ValueOption.iter (_.ScheduleItems >> Formatting.outputMapToHtml "out/InterestFirstTest018.md" false) + + let totalInterestPortions = schedule |> ValueOption.map (fun s -> s.ScheduleItems |> Map.values |> Seq.sumBy _.InterestPortion) |> ValueOption.defaultValue 0L + + totalInterestPortions |> should equal 740_00L [] let ``19) Realistic test 6045bd0ffc0f`` () = @@ -642,29 +645,31 @@ module InterestFirstTests = totalInterestPortions |> should equal 740_00L - // [] - // let ``20) Realistic test 6045bd123363 with correction on final day`` () = - // let sp = - // { scheduleParameters with - // AsOfDate = Date(2024, 9, 17) - // StartDate = Date(2023, 1, 14) - // Principal = 100_00L - // PaymentSchedule = FixedSchedules [| { UnitPeriodConfig = UnitPeriod.Config.Monthly(1, 2023, 2, 3); PaymentCount = 4; PaymentAmount = 42_40L } |] - // } + [] + let ``20) Realistic test 6045bd123363 with correction on final day`` () = + let sp = + { scheduleParameters with + AsOfDate = Date(2024, 9, 17) + StartDate = Date(2023, 1, 14) + Principal = 100_00L + ScheduleConfig = FixedSchedules [| { UnitPeriodConfig = UnitPeriod.Config.Monthly(1, 2023, 2, 3); PaymentCount = 4; PaymentValue = 42_40L; ScheduleType = ScheduleType.Original } |] + } - // let actualPayments = [| - // 20, [| ActualPayment.QuickConfirmed 116_00L |] - // |] + let actualPayments = + [| + 20, [| ActualPayment.QuickConfirmed 116_00L |] + |] + |> Map.ofArray - // let schedule = - // actualPayments - // |> Amortisation.generate sp IntendedPurpose.Statement false + let schedule = + actualPayments + |> Amortisation.generate sp IntendedPurpose.Statement false - // schedule |> ValueOption.iter (_.ScheduleItems >> Formatting.outputListToHtml "out/InterestFirstTest020.md" false) + schedule |> ValueOption.iter (_.ScheduleItems >> Formatting.outputMapToHtml "out/InterestFirstTest020.md" false) - // let totalInterestPortions = schedule |> ValueOption.map (fun s -> s.ScheduleItems |> Array.sumBy _.InterestPortion) |> ValueOption.defaultValue 0L + let totalInterestPortions = schedule |> ValueOption.map (fun s -> s.ScheduleItems |> Map.values |> Seq.sumBy _.InterestPortion) |> ValueOption.defaultValue 0L - // totalInterestPortions |> should equal 16_00L + totalInterestPortions |> should equal 16_00L [] let ``21) Realistic test 6045bd123363`` () = diff --git a/tests/UnitPeriodConfigTests.fs b/tests/UnitPeriodConfigTests.fs index d2d318e..6ec9deb 100644 --- a/tests/UnitPeriodConfigTests.fs +++ b/tests/UnitPeriodConfigTests.fs @@ -427,12 +427,12 @@ module UnitPeriodConfigTests = [| Map.toArray originalScheduledPayments [| - 201, ScheduledPayment.Quick ValueNone (ValueSome 12489L) - 232, ScheduledPayment.Quick ValueNone (ValueSome 12489L) - 262, ScheduledPayment.Quick ValueNone (ValueSome 12489L) - 293, ScheduledPayment.Quick ValueNone (ValueSome 12489L) - 323, ScheduledPayment.Quick ValueNone (ValueSome 12489L) - 354, ScheduledPayment.Quick ValueNone (ValueSome 79109L) + 201, ScheduledPayment.Quick ValueNone (ValueSome { Value = 12489L; RescheduleDay = 198 }) + 232, ScheduledPayment.Quick ValueNone (ValueSome { Value = 12489L; RescheduleDay = 198 }) + 262, ScheduledPayment.Quick ValueNone (ValueSome { Value = 12489L; RescheduleDay = 198 }) + 293, ScheduledPayment.Quick ValueNone (ValueSome { Value = 12489L; RescheduleDay = 198 }) + 323, ScheduledPayment.Quick ValueNone (ValueSome { Value = 12489L; RescheduleDay = 198 }) + 354, ScheduledPayment.Quick ValueNone (ValueSome { Value = 79109L; RescheduleDay = 198 }) |] |] |> Array.concat