Skip to content

Commit

Permalink
fix max payment count
Browse files Browse the repository at this point in the history
  • Loading branch information
simontreanor committed Jun 24, 2024
1 parent ae57c93 commit 9160999
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/FSharp.Finance.Personal.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>FSharp.Finance.Personal</PackageId>
<Version>1.0.1</Version>
<Version>1.0.2</Version>
<Authors>Simon Treanor</Authors>
<PackageDescription>F# Personal Finance Library</PackageDescription>
<RepositoryUrl>https://github.com/simontreanor/FSharp.Finance.Personal</RepositoryUrl>
Expand Down
17 changes: 8 additions & 9 deletions src/UnitPeriod.fs
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,14 @@ module UnitPeriod =
| invalidConfig -> fix invalidConfig

/// generates a suggested number of payments to constrain the loan within a certain duration
let maxPaymentCount (maxDuration: int<DurationDay>) (config: Config) (startDate: Date) =
let offset y m td = ((TrackingDay.toDate y m td) - startDate).Days |> fun f -> int f * 1<DurationDay>
let maxPaymentCount (maxDuration: int<DurationDay>) (config: Config) =
match config with
| Single d -> maxDuration - offset d.Year d.Month d.Day
| Daily d -> maxDuration - offset d.Year d.Month d.Day
| Weekly (multiple, d) when multiple > 0 -> (maxDuration - offset d.Year d.Month d.Day) / (multiple * 7)
| SemiMonthly (y, m, d1, _) -> (maxDuration - offset y m (int d1)) / 15
| Monthly (multiple, y, m, d) when multiple > 0 -> (maxDuration - offset y m (int d)) / (multiple * 30)
| _ -> 0<DurationDay>
| Single _ -> 1.
| Daily _ -> float maxDuration
| Weekly (multiple, _) when multiple > 0 -> ((float maxDuration) / (float multiple * 7.))
| SemiMonthly _ -> ((float maxDuration) / 15.)
| Monthly (multiple, _, _, _) when multiple > 0 -> ((float maxDuration) / (float multiple * 30.))
| _ -> 1.
|> int

/// direction in which to generate the schedule: forward works forwards from a given date and reverse works backwards
Expand All @@ -222,7 +221,7 @@ module UnitPeriod =
if count = 0 then [||] else
let limitedCount =
match maxDuration with
| ValueSome { Duration.Length = d; Duration.FromDate = fd } -> maxPaymentCount d unitPeriodConfig fd
| ValueSome { Duration.Length = d; Duration.FromDate = fd } -> min count (maxPaymentCount d unitPeriodConfig)
| ValueNone -> count
let adjustMonthEnd (monthEndTrackingDay: int) (d: Date) =
if d.Day > 15 && monthEndTrackingDay > 28 then
Expand Down
54 changes: 52 additions & 2 deletions tests/PaymentScheduleTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1463,7 +1463,7 @@ module PaymentScheduleTests =
StartDate = Date(2024, 5, 8)
Principal = 1000_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2024, 5, 8),
UnitPeriodConfig = UnitPeriod.Monthly(1, 2024, 5, 18),
PaymentCount = 7,
MaxDuration = ValueSome { Length = 184<DurationDay>; FromDate = Date(2024, 5, 8) }
)
Expand Down Expand Up @@ -1498,5 +1498,55 @@ module PaymentScheduleTests =
return schedule.LevelPayment, schedule.FinalPayment
}

let expected = ValueSome (251_08L<Cent>, 250_99L<Cent>)
let expected = ValueSome (290_73L<Cent>, 290_71L<Cent>)
actual |> should equal expected

[<Fact>]
let ``4) Payment count must not be exceeded`` () =
let sp = {
AsOfDate = Date(2024, 6, 24)
StartDate = Date(2024, 6, 24)
Principal = 100_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2024, 7, 4),
PaymentCount = 4,
MaxDuration = ValueSome { Length = 190<DurationDay>; FromDate = Date(2024, 6, 24) }
)
FeesAndCharges = {
Fees = [||]
FeesAmortisation = Fees.FeeAmortisation.AmortiseProportionately
FeesSettlementRefund = Fees.SettlementRefund.ProRata ValueNone
Charges = [| Charge.LatePayment (Amount.Percentage(Percent 5m, ValueSome(Amount.Restriction.UpperLimit 750L<Cent>), ValueSome (Round MidpointRounding.ToEven))) |]
ChargesHolidays = [||]
ChargesGrouping = OneChargeTypePerDay
LatePaymentGracePeriod = 3<DurationDay>
}
Interest = {
StandardRate = Interest.Rate.Daily (Percent 0.8m)
Cap = Interest.Cap.none
InitialGracePeriod = 1<DurationDay>
PromotionalRates = [||]
RateOnNegativeBalance = ValueNone
}
Calculation = {
AprMethod = Apr.CalculationMethod.UnitedKingdom 3
RoundingOptions = {
ChargesRounding = Round MidpointRounding.ToEven
FeesRounding = Round MidpointRounding.ToEven
InterestRounding = Round MidpointRounding.ToEven
PaymentRounding = Round MidpointRounding.ToEven
}
MinimumPayment = DeferOrWriteOff 50L<Cent>
PaymentTimeout = 3<DurationDay>
}
}

let actual =
voption {
let! schedule = sp |> calculate BelowZero
schedule.Items |> Formatting.outputListToHtml "out/PaymentSchedule004.md"
return schedule.LevelPayment, schedule.FinalPayment
}

let expected = ValueSome (36_48L<Cent>, 36_44L<Cent>)
actual |> should equal expected

0 comments on commit 9160999

Please sign in to comment.