diff --git a/io/out/GeneratedDate.html b/io/out/GeneratedDate.html index 186a1775..a62f67e4 100644 --- a/io/out/GeneratedDate.html +++ b/io/out/GeneratedDate.html @@ -1 +1 @@ -

Generated: 2025-06-05 15:01:19 +01:00 using library version: 2.5.5

+

Generated: 2026-04-28 23:25:09 +00:00 using library version: 2.5.5

diff --git a/src/FSharp.Finance.Personal.fsproj b/src/FSharp.Finance.Personal.fsproj index 4e71d5e8..501e398e 100644 --- a/src/FSharp.Finance.Personal.fsproj +++ b/src/FSharp.Finance.Personal.fsproj @@ -24,6 +24,7 @@ + diff --git a/src/Mortgage.fs b/src/Mortgage.fs new file mode 100644 index 00000000..3bf43de4 --- /dev/null +++ b/src/Mortgage.fs @@ -0,0 +1,381 @@ +namespace FSharp.Finance.Personal + +open System + +/// functions for specialist mortgage product calculations: offset mortgages, equity release / lifetime mortgages, and shared ownership +module Mortgage = + + open Calculation + open DateDay + + // ------------------------------------------------------------------------- + // Offset mortgage + // ------------------------------------------------------------------------- + + /// parameters for an offset mortgage calculation + [] + type OffsetMortgageParameters = { + /// the outstanding mortgage balance (in base currency units, e.g. cents / pence) + MortgageBalance: int64 + /// the total balance held in linked savings / current accounts that offset the mortgage balance + LinkedSavingsBalance: int64 + /// the annual interest rate on the mortgage + AnnualInterestRate: Percent + /// the remaining term of the mortgage in months + RemainingTermMonths: int + /// the monthly payment amount (if already known); if not provided, it is derived from the full mortgage balance + MonthlyPayment: int64 voption + } + + /// result of an offset mortgage calculation + [] + type OffsetMortgageResult = { + /// the net balance on which interest is charged (mortgage balance minus linked savings) + NetInterestBearingBalance: int64 + /// the monthly interest saving due to the offset + MonthlyInterestSaving: int64 + /// the monthly payment on the net (offset) balance + EffectiveMonthlyPayment: int64 + /// estimated months saved off the remaining term if monthly payment is kept the same + EstimatedMonthsSaved: int + /// total interest payable over the remaining term on the net (offset) balance + TotalInterestOnNetBalance: int64 + /// total interest that would have been payable on the full mortgage balance + TotalInterestOnFullBalance: int64 + } + + /// calculates the monthly payment for a capital-and-interest (annuity) loan + let private calculateMonthlyPayment (principalCents: int64) (annualRatePercent: Percent) (termMonths: int) : int64 = + let r = Percent.toDecimal annualRatePercent / 12m + if r = 0m then + if termMonths = 0 then 0L + else Cent.round (RoundWith MidpointRounding.AwayFromZero) (decimal principalCents / decimal termMonths) + else + let decimalPow n x = decimal (Math.Pow(double x, double n)) + let factor = (1m + r) |> decimalPow termMonths + let payment = decimal principalCents * r * factor / (factor - 1m) + Cent.round (RoundWith MidpointRounding.AwayFromZero) payment + + /// estimates how many months a capital-and-interest loan with given balance, rate and payment will take to repay + let private estimateTermMonths (principalCents: int64) (annualRatePercent: Percent) (monthlyPaymentCents: int64) : int = + let r = Percent.toDecimal annualRatePercent / 12m + if r = 0m then + if monthlyPaymentCents = 0L then Int32.MaxValue + else int (Math.Ceiling(decimal principalCents / decimal monthlyPaymentCents)) + else + let p = decimal principalCents + let m = decimal monthlyPaymentCents + if m <= p * r then + // payment does not cover interest – loan never repays + Int32.MaxValue + else + // n = -ln(1 - r*P/M) / ln(1+r) + let n = -Math.Log(double (1m - r * p / m)) / Math.Log(double (1m + r)) + int (Math.Ceiling n) + + /// calculates total interest paid on a capital-and-interest loan + let private totalInterest (principalCents: int64) (annualRatePercent: Percent) (termMonths: int) : int64 = + let payment = calculateMonthlyPayment principalCents annualRatePercent termMonths + let total = decimal payment * decimal termMonths + Cent.round (RoundWith MidpointRounding.AwayFromZero) (total - decimal principalCents) + |> max 0L + + /// calculates offset mortgage metrics, including net interest-bearing balance, payment savings and term reduction + let calculateOffsetMortgage (p: OffsetMortgageParameters) : OffsetMortgageResult = + let netBalance = + (p.MortgageBalance - p.LinkedSavingsBalance) + |> max 0L + + let fullPayment = + match p.MonthlyPayment with + | ValueSome mp -> mp + | ValueNone -> calculateMonthlyPayment p.MortgageBalance p.AnnualInterestRate p.RemainingTermMonths + + let netPayment = calculateMonthlyPayment netBalance p.AnnualInterestRate p.RemainingTermMonths + + let monthlySaving = + Cent.round (RoundWith MidpointRounding.AwayFromZero) (decimal fullPayment - decimal netPayment) + |> max 0L + + let estimatedMonthsSaved = + if netBalance = 0L then + p.RemainingTermMonths + else + let monthsAtFullPayment = estimateTermMonths netBalance p.AnnualInterestRate fullPayment + p.RemainingTermMonths - monthsAtFullPayment |> max 0 + + let totalInterestNet = totalInterest netBalance p.AnnualInterestRate p.RemainingTermMonths + let totalInterestFull = totalInterest p.MortgageBalance p.AnnualInterestRate p.RemainingTermMonths + + { + NetInterestBearingBalance = netBalance + MonthlyInterestSaving = monthlySaving + EffectiveMonthlyPayment = netPayment + EstimatedMonthsSaved = estimatedMonthsSaved + TotalInterestOnNetBalance = totalInterestNet + TotalInterestOnFullBalance = totalInterestFull + } + + // ------------------------------------------------------------------------- + // Equity release / lifetime mortgage + // ------------------------------------------------------------------------- + + /// a voluntary partial repayment made at a specific date + [] + type PartialRepayment = { + /// the date on which the partial repayment is made + RepaymentDate: Date + /// the amount repaid (in base currency units, e.g. cents / pence) + RepaymentAmount: int64 + } + + /// parameters for an equity release / lifetime mortgage calculation + [] + type EquityReleaseParameters = { + /// the initial advance (in base currency units) + InitialAdvance: int64 + /// the date on which the advance is made + StartDate: Date + /// the annual roll-up (compound) interest rate + AnnualInterestRate: Percent + /// the projected end date for balance calculation (e.g. expected sale or death) + ProjectionDate: Date + /// any voluntary partial repayments made during the term + PartialRepayments: PartialRepayment array + /// the maximum property value at the start, used to model the no-negative-equity guarantee + PropertyValue: int64 voption + } + + /// result of an equity release projection + [] + type EquityReleaseResult = { + /// projected outstanding balance at the projection date (before NNEG cap) + ProjectedBalance: int64 + /// total interest rolled up over the projection period + TotalRolledUpInterest: int64 + /// projected balance after applying the no-negative-equity guarantee (capped at property value) + BalanceAfterNneg: int64 + /// whether the no-negative-equity guarantee was triggered (i.e. projected balance exceeded property value) + NnegTriggered: bool + } + + /// projects the balance of an equity release / lifetime mortgage at a future date, + /// accounting for daily compounding and any voluntary partial repayments + let calculateEquityRelease (p: EquityReleaseParameters) : EquityReleaseResult = + // materialise DateTime values once for efficient comparison + let projectionDateTime = p.ProjectionDate.ToDateTime() + + // sort repayments by date, materialising DateTime values once + let sortedRepayments = + p.PartialRepayments + |> Array.map (fun r -> r.RepaymentDate.ToDateTime(), r) + |> Array.sortBy fst + + // compound the balance from the start date to the projection date, + // applying any repayments on the relevant dates + let dailyRate = + Percent.toDecimal p.AnnualInterestRate / 365m + + let rec compound (currentDate: Date) (balance: decimal) (remainingRepayments: (DateTime * PartialRepayment) list) = + let currentDateTime = currentDate.ToDateTime() + if currentDateTime >= projectionDateTime then + balance + else + // apply any repayments due today + let repaidToday, remaining = + remainingRepayments + |> List.partition (fun (repaymentDt, _) -> repaymentDt <= currentDateTime) + let repaymentAmount = + repaidToday |> List.sumBy (fun (_, r) -> decimal r.RepaymentAmount) + let balanceAfterRepayment = max 0m (balance - repaymentAmount) + // accrue one day of interest + let newBalance = balanceAfterRepayment * (1m + dailyRate) + compound (currentDate.AddDays 1) newBalance remaining + + let projectedDecimal = + compound p.StartDate (decimal p.InitialAdvance) (sortedRepayments |> Array.toList) + + let projectedBalance = + Cent.round (RoundWith MidpointRounding.AwayFromZero) projectedDecimal + + let totalInterest = projectedBalance - p.InitialAdvance |> max 0L + + let nnegCap = + match p.PropertyValue with + | ValueSome pv -> pv + | ValueNone -> Int64.MaxValue * 1L + + let balanceAfterNneg = min projectedBalance nnegCap + let nnegTriggered = projectedBalance > nnegCap + + { + ProjectedBalance = projectedBalance + TotalRolledUpInterest = totalInterest + BalanceAfterNneg = balanceAfterNneg + NnegTriggered = nnegTriggered + } + + // ------------------------------------------------------------------------- + // Shared ownership + // ------------------------------------------------------------------------- + + /// a staircasing tranche: buying an additional share of the property + [] + type StaircasingTranche = { + /// the date on which the additional tranche is purchased + PurchaseDate: Date + /// the share being purchased (e.g. 0.25 for 25%) + AdditionalShare: Percent + /// the property value at the time of purchasing the additional tranche + PropertyValueAtPurchase: int64 + } + + /// parameters for a shared ownership calculation + [] + type SharedOwnershipParameters = { + /// the total property value at the start + PropertyValue: int64 + /// the share initially owned by the borrower (e.g. 0.25 for 25%) + InitialShare: Percent + /// the mortgage balance (covering the owned share) in base currency units + MortgageBalance: int64 + /// the annual mortgage interest rate + MortgageAnnualRate: Percent + /// the mortgage term in months + MortgageTermMonths: int + /// the annual rent charged on the unowned share (as a percentage of unowned share value) + AnnualRentRate: Percent + /// any planned staircasing tranches + StaircasingTranches: StaircasingTranche array + /// the evaluation horizon in months (used for total cost comparison) + HorizonMonths: int + } + + /// the blended monthly outgoing for a shared ownership product + [] + type SharedOwnershipMonthlyOutgoing = { + /// the monthly mortgage payment + MonthlyMortgagePayment: int64 + /// the monthly rent on the unowned share + MonthlyRent: int64 + /// the total blended monthly outgoing + TotalMonthlyOutgoing: int64 + } + + /// the result of a staircasing calculation + [] + type StaircasingResult = { + /// the cost of purchasing the additional tranche + TrancheCost: int64 + /// the owned share after purchasing the tranche + NewOwnedShare: Percent + /// the remaining unowned share + NewUnownedShare: Percent + /// the new monthly rent on the remaining unowned share (at the original rent rate) + NewMonthlyRent: int64 + /// the new monthly mortgage payment if the tranche is financed + NewMonthlyMortgagePayment: int64 + /// the new blended monthly outgoing + NewTotalMonthlyOutgoing: int64 + } + + /// the total-cost comparison result + [] + type TotalCostComparison = { + /// total cost over the horizon as a shared ownership occupier (mortgage payments + rent) + TotalSharedOwnershipCost: int64 + /// total cost over the horizon of an outright purchase (assumes mortgage payments only, no rent, same property value and rate) + TotalOutrightPurchaseCost: int64 + /// the difference (positive means shared ownership is more expensive over the horizon) + CostDifference: int64 + } + + /// calculates the blended monthly outgoing for a shared ownership product + let calculateSharedOwnershipOutgoing (p: SharedOwnershipParameters) : SharedOwnershipMonthlyOutgoing = + let mortgagePayment = + calculateMonthlyPayment p.MortgageBalance p.MortgageAnnualRate p.MortgageTermMonths + + let unownedShare = + let (Percent ownedShare) = p.InitialShare + (100m - ownedShare) / 100m + + let annualRent = + decimal p.PropertyValue * unownedShare * Percent.toDecimal p.AnnualRentRate + + let monthlyRent = + Cent.round (RoundWith MidpointRounding.AwayFromZero) (annualRent / 12m) + + { + MonthlyMortgagePayment = mortgagePayment + MonthlyRent = monthlyRent + TotalMonthlyOutgoing = mortgagePayment + monthlyRent + } + + /// calculates the cost and impact of purchasing an additional tranche (staircasing) + let calculateStaircasing + (p: SharedOwnershipParameters) + (tranche: StaircasingTranche) + (existingMortgageBalance: int64) + (remainingMortgageTermMonths: int) + : StaircasingResult = + let (Percent additionalSharePct) = tranche.AdditionalShare + let trancheCost = + Cent.round + (RoundWith MidpointRounding.AwayFromZero) + (decimal tranche.PropertyValueAtPurchase * additionalSharePct / 100m) + + let (Percent ownedSharePct) = p.InitialShare + let newOwnedSharePct = ownedSharePct + additionalSharePct |> min 100m + let newOwnedShare = Percent newOwnedSharePct + let newUnownedShare = Percent (100m - newOwnedSharePct) + + // new monthly rent on the remaining unowned share + let newAnnualRent = + decimal tranche.PropertyValueAtPurchase + * ((100m - newOwnedSharePct) / 100m) + * Percent.toDecimal p.AnnualRentRate + + let newMonthlyRent = + Cent.round (RoundWith MidpointRounding.AwayFromZero) (newAnnualRent / 12m) + + // new mortgage = existing balance + tranche cost, financed over remaining term at same rate + let newMortgageBalance = existingMortgageBalance + trancheCost + let newMortgagePayment = + calculateMonthlyPayment newMortgageBalance p.MortgageAnnualRate remainingMortgageTermMonths + + { + TrancheCost = trancheCost + NewOwnedShare = newOwnedShare + NewUnownedShare = newUnownedShare + NewMonthlyRent = newMonthlyRent + NewMonthlyMortgagePayment = newMortgagePayment + NewTotalMonthlyOutgoing = newMortgagePayment + newMonthlyRent + } + + /// estimates the total cost of shared ownership vs. outright purchase over a given horizon + let calculateTotalCostComparison (p: SharedOwnershipParameters) : TotalCostComparison = + let outgoing = calculateSharedOwnershipOutgoing p + + // shared ownership: mortgage payments + rent for the horizon + // (simplified: constant payment and rent throughout, ignoring staircasing) + let totalSharedOwnershipCost = + Cent.round + (RoundWith MidpointRounding.AwayFromZero) + (decimal (outgoing.TotalMonthlyOutgoing) * decimal p.HorizonMonths) + + // outright purchase comparison: mortgage payment on full property value, same rate and term + let outrightPayment = + calculateMonthlyPayment p.PropertyValue p.MortgageAnnualRate p.MortgageTermMonths + + let totalOutrightCost = + Cent.round + (RoundWith MidpointRounding.AwayFromZero) + (decimal outrightPayment * decimal p.HorizonMonths) + + let costDifference = totalSharedOwnershipCost - totalOutrightCost + + { + TotalSharedOwnershipCost = totalSharedOwnershipCost + TotalOutrightPurchaseCost = totalOutrightCost + CostDifference = costDifference + } diff --git a/tests/FSharp.Finance.Personal.Tests.fsproj b/tests/FSharp.Finance.Personal.Tests.fsproj index f7ddeeb6..bb34d6d3 100644 --- a/tests/FSharp.Finance.Personal.Tests.fsproj +++ b/tests/FSharp.Finance.Personal.Tests.fsproj @@ -26,6 +26,7 @@ + diff --git a/tests/MortgageTests.fs b/tests/MortgageTests.fs new file mode 100644 index 00000000..ddfa4522 --- /dev/null +++ b/tests/MortgageTests.fs @@ -0,0 +1,281 @@ +namespace FSharp.Finance.Personal.Tests + +open Xunit +open FsUnit.Xunit + +open FSharp.Finance.Personal + +module MortgageTests = + + open Calculation + open DateDay + open Mortgage + + // ------------------------------------------------------------------------- + // Offset mortgage tests + // ------------------------------------------------------------------------- + + [] + let OffsetMortgageTest001 () = + // £200,000 mortgage, £50,000 in savings, 3% annual rate, 20-year term + // Net balance = £150,000; verify monthly saving is positive + let p: OffsetMortgageParameters = { + MortgageBalance = 200_000_00L + LinkedSavingsBalance = 50_000_00L + AnnualInterestRate = Percent 3m + RemainingTermMonths = 240 + MonthlyPayment = ValueNone + } + + let result = calculateOffsetMortgage p + + // net balance should be £150,000 + result.NetInterestBearingBalance |> should equal 150_000_00L + + // full payment on £200,000 should be greater than net payment on £150,000 + result.EffectiveMonthlyPayment < result.EffectiveMonthlyPayment + result.MonthlyInterestSaving + |> should equal true + + // some months should be saved + result.EstimatedMonthsSaved |> should be (greaterThan 0) + + // total interest on net balance should be less than on full balance + result.TotalInterestOnNetBalance < result.TotalInterestOnFullBalance + |> should equal true + + [] + let OffsetMortgageTest002 () = + // savings exceed mortgage balance – net balance should be zero + let p: OffsetMortgageParameters = { + MortgageBalance = 100_000_00L + LinkedSavingsBalance = 150_000_00L + AnnualInterestRate = Percent 2.5m + RemainingTermMonths = 120 + MonthlyPayment = ValueNone + } + + let result = calculateOffsetMortgage p + + result.NetInterestBearingBalance |> should equal 0L + result.EstimatedMonthsSaved |> should equal 120 + + [] + let OffsetMortgageTest003 () = + // no linked savings – net balance equals mortgage balance and saving is zero + let p: OffsetMortgageParameters = { + MortgageBalance = 150_000_00L + LinkedSavingsBalance = 0L + AnnualInterestRate = Percent 4m + RemainingTermMonths = 180 + MonthlyPayment = ValueNone + } + + let result = calculateOffsetMortgage p + + result.NetInterestBearingBalance |> should equal 150_000_00L + result.MonthlyInterestSaving |> should equal 0L + result.EstimatedMonthsSaved |> should equal 0 + (result.TotalInterestOnNetBalance = result.TotalInterestOnFullBalance) |> should equal true + + [] + let OffsetMortgageTest004 () = + // explicit monthly payment provided + let p: OffsetMortgageParameters = { + MortgageBalance = 200_000_00L + LinkedSavingsBalance = 30_000_00L + AnnualInterestRate = Percent 3.5m + RemainingTermMonths = 300 + MonthlyPayment = ValueSome 100_000L // £1,000/month + } + + let result = calculateOffsetMortgage p + + result.NetInterestBearingBalance |> should equal 170_000_00L + // monthly saving should be positive since the full payment > net payment + result.MonthlyInterestSaving |> should be (greaterThanOrEqualTo 0L) + + // ------------------------------------------------------------------------- + // Equity release tests + // ------------------------------------------------------------------------- + + [] + let EquityReleaseTest001 () = + // £100,000 advance at 5% annual rate over 10 years, no repayments + let p: EquityReleaseParameters = { + InitialAdvance = 100_000_00L + StartDate = Date(2020, 1, 1) + AnnualInterestRate = Percent 5m + ProjectionDate = Date(2030, 1, 1) + PartialRepayments = [||] + PropertyValue = ValueNone + } + + let result = calculateEquityRelease p + + // projected balance should be greater than initial advance + result.ProjectedBalance > p.InitialAdvance |> should equal true + // total rolled-up interest should equal the difference + (result.TotalRolledUpInterest = result.ProjectedBalance - p.InitialAdvance) |> should equal true + // NNEG not triggered (no property value set) + result.NnegTriggered |> should equal false + + [] + let EquityReleaseTest002 () = + // NNEG triggered: property value smaller than projected balance + let p: EquityReleaseParameters = { + InitialAdvance = 100_000_00L + StartDate = Date(2000, 1, 1) + AnnualInterestRate = Percent 7m + ProjectionDate = Date(2040, 1, 1) + PartialRepayments = [||] + PropertyValue = ValueSome 200_000_00L + } + + let result = calculateEquityRelease p + + // after 40 years at 7%, £100k becomes ~£1.5M – well above £200k property value + result.NnegTriggered |> should equal true + result.BalanceAfterNneg |> should equal 200_000_00L + + [] + let EquityReleaseTest003 () = + // partial repayment reduces projected balance + let withoutRepayment: EquityReleaseParameters = { + InitialAdvance = 100_000_00L + StartDate = Date(2020, 1, 1) + AnnualInterestRate = Percent 5m + ProjectionDate = Date(2030, 1, 1) + PartialRepayments = [||] + PropertyValue = ValueNone + } + + let withRepayment: EquityReleaseParameters = { + withoutRepayment with + PartialRepayments = [| + { RepaymentDate = Date(2025, 1, 1); RepaymentAmount = 10_000_00L } + |] + } + + let balanceWithout = (calculateEquityRelease withoutRepayment).ProjectedBalance + let balanceWith = (calculateEquityRelease withRepayment).ProjectedBalance + + balanceWith < balanceWithout |> should equal true + + [] + let EquityReleaseTest004 () = + // start date equals projection date – balance should equal initial advance + let p: EquityReleaseParameters = { + InitialAdvance = 50_000_00L + StartDate = Date(2025, 6, 15) + AnnualInterestRate = Percent 4m + ProjectionDate = Date(2025, 6, 15) + PartialRepayments = [||] + PropertyValue = ValueNone + } + + let result = calculateEquityRelease p + + result.ProjectedBalance |> should equal p.InitialAdvance + result.TotalRolledUpInterest |> should equal 0L + + // ------------------------------------------------------------------------- + // Shared ownership tests + // ------------------------------------------------------------------------- + + [] + let SharedOwnershipTest001 () = + // £200,000 property, 50% share, mortgage on £100,000 at 3%, 25-year term + // rent on 50% unowned at 2.5% annual rent rate + let p: SharedOwnershipParameters = { + PropertyValue = 200_000_00L + InitialShare = Percent 50m + MortgageBalance = 100_000_00L + MortgageAnnualRate = Percent 3m + MortgageTermMonths = 300 + AnnualRentRate = Percent 2.5m + StaircasingTranches = [||] + HorizonMonths = 60 + } + + let outgoing = calculateSharedOwnershipOutgoing p + + // mortgage payment on £100k at 3% over 25 years + outgoing.MonthlyMortgagePayment |> should be (greaterThan 0L) + // monthly rent = £200,000 * 50% * 2.5% / 12 + let expectedRent = Cent.round (RoundWith System.MidpointRounding.AwayFromZero) (200_000_00m * 0.5m * 0.025m / 12m) + outgoing.MonthlyRent |> should equal expectedRent + // total = mortgage + rent + (outgoing.TotalMonthlyOutgoing = outgoing.MonthlyMortgagePayment + outgoing.MonthlyRent) + |> should equal true + + [] + let SharedOwnershipTest002 () = + // staircasing: buy an additional 25% tranche at a £210,000 property value + let p: SharedOwnershipParameters = { + PropertyValue = 200_000_00L + InitialShare = Percent 50m + MortgageBalance = 95_000_00L + MortgageAnnualRate = Percent 3m + MortgageTermMonths = 240 + AnnualRentRate = Percent 2.5m + StaircasingTranches = [||] + HorizonMonths = 60 + } + + let tranche: StaircasingTranche = { + PurchaseDate = Date(2026, 6, 1) + AdditionalShare = Percent 25m + PropertyValueAtPurchase = 210_000_00L + } + + let result = calculateStaircasing p tranche 95_000_00L 240 + + // tranche cost = £210,000 * 25% = £52,500 + result.TrancheCost |> should equal 52_500_00L + // new owned share = 50% + 25% = 75% + (result.NewOwnedShare = Percent 75m) |> should equal true + // new unowned share = 25% + (result.NewUnownedShare = Percent 25m) |> should equal true + // new rent should be lower (only 25% unowned) + let originalOutgoing = calculateSharedOwnershipOutgoing p + result.NewMonthlyRent < originalOutgoing.MonthlyRent |> should equal true + + [] + let SharedOwnershipTest003 () = + // total cost comparison: shared ownership vs outright purchase + let p: SharedOwnershipParameters = { + PropertyValue = 200_000_00L + InitialShare = Percent 40m + MortgageBalance = 80_000_00L + MortgageAnnualRate = Percent 3.5m + MortgageTermMonths = 300 + AnnualRentRate = Percent 2.75m + StaircasingTranches = [||] + HorizonMonths = 120 + } + + let result = calculateTotalCostComparison p + + result.TotalSharedOwnershipCost |> should be (greaterThan 0L) + result.TotalOutrightPurchaseCost |> should be (greaterThan 0L) + (result.CostDifference = result.TotalSharedOwnershipCost - result.TotalOutrightPurchaseCost) + |> should equal true + + [] + let SharedOwnershipTest004 () = + // 100% ownership: no rent, mortgage payment only + let p: SharedOwnershipParameters = { + PropertyValue = 150_000_00L + InitialShare = Percent 100m + MortgageBalance = 150_000_00L + MortgageAnnualRate = Percent 3m + MortgageTermMonths = 180 + AnnualRentRate = Percent 2.5m + StaircasingTranches = [||] + HorizonMonths = 60 + } + + let outgoing = calculateSharedOwnershipOutgoing p + + outgoing.MonthlyRent |> should equal 0L + (outgoing.TotalMonthlyOutgoing = outgoing.MonthlyMortgagePayment) |> should equal true