From 47988818abfb043674a6ebd7fb3163e5c5ba5dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Fri, 26 Jul 2024 21:37:34 +0100 Subject: [PATCH] feat: Improve types in Mortgage and FairholdLandPurchase --- app/models/Mortgage.test.ts | 6 +- app/models/Mortgage.ts | 138 +++++++++++++--------- app/models/constants.ts | 1 + app/models/tenure/FairholdLandPurchase.ts | 119 ++++++++----------- 4 files changed, 131 insertions(+), 133 deletions(-) create mode 100644 app/models/constants.ts diff --git a/app/models/Mortgage.test.ts b/app/models/Mortgage.test.ts index 650bbc7..e3da8ab 100644 --- a/app/models/Mortgage.test.ts +++ b/app/models/Mortgage.test.ts @@ -4,7 +4,7 @@ it("can be instantiated", () => { const mortgage = new Mortgage({ propertyValue: 100, interestRate: 0.05, - termOfTheMortgage: 25, + mortgageTerm: 25, initialDeposit: 0.1, }); expect(mortgage).toBeDefined(); @@ -14,7 +14,7 @@ it("correctly calculates the amount of the mortgage ", () => { const mortgage = new Mortgage({ propertyValue: 100, interestRate: 0.05, - termOfTheMortgage: 25, + mortgageTerm: 25, initialDeposit: 0.1, }); @@ -25,7 +25,7 @@ it("correctly calculates the amount of monthly payment ", () => { const mortgage = new Mortgage({ propertyValue: 100, interestRate: 0.05, - termOfTheMortgage: 25, + mortgageTerm: 25, initialDeposit: 0.1, }); expect(mortgage.monthlyPayment).toBeCloseTo(0.53); diff --git a/app/models/Mortgage.ts b/app/models/Mortgage.ts index d38012c..f844c6a 100644 --- a/app/models/Mortgage.ts +++ b/app/models/Mortgage.ts @@ -1,95 +1,115 @@ +import { MONTHS_PER_YEAR } from "./constants"; + +const DEFAULT_INTEREST_RATE = 0.06; +const DEFAULT_MORTGAGE_TERM = 30; +const DEFAULT_INITIAL_DEPOSIT = 0.15; + +type MortgageBreakdown = { + yearlyPayment: number; + cumulativePaid: number; + remainingBalance: number; +}[]; + export class Mortgage { - propertyValue: number; //value of the property for the mortgage - interestRate: number; // interest rate of the mortgage in percentage e.r, 0.05=5% - termYears: number; // number of years of the mortgage - initialDeposit: number; // initial deposit of the value of the mortgage in percentage e.g. 0.15 =15% deposit - principal?: number; // amount of the morgage requested - monthlyPayment?: number; // monthly rate of the mortgage - totalMortgageCost?: number; // total cost of the mortgage - yearlyPaymentBreakdown?: { - yearlyPayment: number; - cumulativePaid: number; - remainingBalance: number; - }[]; // yearly breakdown of the mortgage + propertyValue: number; + /** + * This value is given as a percentage. For example, 0.05 represents a 5% rate + */ + interestRate: number; + termYears: number; + /** + * This value is given as a percentage. For example, 0.15 represents a 15% deposit + */ + initialDeposit: number; + /** + * The principle is the value of the property, minus the deposit + */ + principal: number; + monthlyPayment: number; + totalMortgageCost: number; + yearlyPaymentBreakdown: MortgageBreakdown; + constructor({ propertyValue, - interestRate = 0.06, - termOfTheMortgage = 30, - initialDeposit = 0.15, + interestRate = DEFAULT_INTEREST_RATE, + mortgageTerm = DEFAULT_MORTGAGE_TERM, + initialDeposit = DEFAULT_INITIAL_DEPOSIT, }: { propertyValue: number; interestRate?: number; - termOfTheMortgage?: number; + mortgageTerm?: number; initialDeposit?: number; }) { this.propertyValue = propertyValue; this.initialDeposit = initialDeposit; this.interestRate = interestRate; - this.termYears = termOfTheMortgage; - this.calculateAmountOfTheMortgage(); // calculate the amount of the mortgage - this.calculateMonthlyMortgagePayment(); // calculate the montly payment - this.calculateYearlyPaymentBreakdown(); // calculate the yearly breakdown; - } + this.termYears = mortgageTerm; + this.principal = this.calculateMortgagePrinciple(); + + const { monthlyPayment, totalMortgageCost } = + this.calculateMonthlyMortgagePayment(); + this.monthlyPayment = monthlyPayment; + this.totalMortgageCost = totalMortgageCost; - calculateAmountOfTheMortgage() { - this.principal = this.propertyValue * (1 - this.initialDeposit); // calculate the amount of the mortgage by removing the deposit - return this.principal; + this.yearlyPaymentBreakdown = this.calculateYearlyPaymentBreakdown(); } - calculateMonthlyMortgagePayment() { - const monthlyInterestRate = this.interestRate / 12; // Convert annual interest rate to monthly rate - const numberOfPayments = this.termYears * 12; // Convert term in years to total number of payments - if (this.principal !== undefined) { - const monthlyPayment = - (this.principal * - monthlyInterestRate * - Math.pow(1 + monthlyInterestRate, numberOfPayments)) / - (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1); // Calculate the monthly payment - this.monthlyPayment = parseFloat(monthlyPayment.toFixed(2)); // Store monthly payment rounded to 2 decimal places in class property - this.totalMortgageCost = this.monthlyPayment * numberOfPayments; // total cost of the mortgage - return this.monthlyPayment; - } else { - throw new Error("amountOfTheMortgage is undefined"); - } + private calculateMortgagePrinciple() { + const principal = this.propertyValue * (1 - this.initialDeposit); + return principal; } - calculateYearlyPaymentBreakdown() { - if (this.monthlyPayment == undefined || this.totalMortgageCost == undefined) - throw new Error("monthlyPayment or totalMortgageCost is undefined"); + private calculateMonthlyMortgagePayment() { + const monthlyInterestRate = this.interestRate / MONTHS_PER_YEAR; + const numberOfPayments = this.termYears * MONTHS_PER_YEAR; + + let monthlyPayment = + (this.principal * + monthlyInterestRate * + Math.pow(1 + monthlyInterestRate, numberOfPayments)) / + (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1); + monthlyPayment = parseFloat(monthlyPayment.toFixed(2)); + + const totalMortgageCost = monthlyPayment * numberOfPayments; + + return { monthlyPayment, totalMortgageCost }; + } + private calculateYearlyPaymentBreakdown() { let yearlyPayment = - this.initialDeposit * this.propertyValue + this.monthlyPayment * 12; + this.initialDeposit * this.propertyValue + + this.monthlyPayment * MONTHS_PER_YEAR; let cumulativePaid = - this.initialDeposit * this.propertyValue + this.monthlyPayment * 12; - let remainingBalance = this.totalMortgageCost - this.monthlyPayment * 12; + this.initialDeposit * this.propertyValue + + this.monthlyPayment * MONTHS_PER_YEAR; + let remainingBalance = + this.totalMortgageCost - this.monthlyPayment * MONTHS_PER_YEAR; - interface mortgageBreakdownTypes { - yearlyPayment: number; - cumulativePaid: number; - remainingBalance: number; - } - let yearlyPaymentBreakdown: mortgageBreakdownTypes[] = [ + let yearlyPaymentBreakdown: MortgageBreakdown = [ { yearlyPayment: yearlyPayment, cumulativePaid: cumulativePaid, remainingBalance: remainingBalance, }, - ]; // initialize the yearlyPaymentBreakdown + ]; for (let i = 0; i < this.termYears - 1; i++) { if (i != this.termYears - 1) { - yearlyPayment = this.monthlyPayment * 12; // calculate the yearly payment + yearlyPayment = this.monthlyPayment * MONTHS_PER_YEAR; } else { - yearlyPayment = remainingBalance; // last year just pay the remaining balance + // last year just pay the remaining balance + yearlyPayment = remainingBalance; } - cumulativePaid = cumulativePaid + yearlyPayment; // calculate the updated cumulative paid - remainingBalance = remainingBalance - yearlyPayment; // calculate the updated remaining balance + cumulativePaid = cumulativePaid + yearlyPayment; + remainingBalance = remainingBalance - yearlyPayment; + yearlyPaymentBreakdown.push({ yearlyPayment: yearlyPayment, cumulativePaid: cumulativePaid, remainingBalance: remainingBalance, - }); // add the current yearly payment to the yearlyPaymentBreakdown + }); } - this.yearlyPaymentBreakdown = yearlyPaymentBreakdown; // set the yearlyPaymentBreakdown + + return yearlyPaymentBreakdown; } -} \ No newline at end of file +} diff --git a/app/models/constants.ts b/app/models/constants.ts new file mode 100644 index 0000000..fd20255 --- /dev/null +++ b/app/models/constants.ts @@ -0,0 +1 @@ +export const MONTHS_PER_YEAR = 12; \ No newline at end of file diff --git a/app/models/tenure/FairholdLandPurchase.ts b/app/models/tenure/FairholdLandPurchase.ts index 613d0d3..2e8ee50 100644 --- a/app/models/tenure/FairholdLandPurchase.ts +++ b/app/models/tenure/FairholdLandPurchase.ts @@ -1,21 +1,24 @@ import { Fairhold } from "../Fairhold"; import { Mortgage } from "../Mortgage"; +type Lifetime = { + maintenanceCost: number; + landMortgagePaymentYearly: number; + houseMortgagePaymentYearly: number; +}[]; + export class FairholdLandPurchase { - discountedLandPrice?: number; - discountedLandMortgage?: Mortgage; - depreciatedHouseMortgage?: Mortgage; - lifetime?: { - maintenanceCost: number; - landMortgagePaymentYearly: number; - houseMortgagePaymentYearly: number; - }[]; + discountedLandPrice: number; + discountedLandMortgage: Mortgage; + depreciatedHouseMortgage: Mortgage; + lifetime: Lifetime; + constructor({ - newBuildPrice, // new build price of the property - depreciatedBuildPrice, // depreciated building price - constructionPriceGrowthPerYear, // construction price growth per year - yearsForecast, // years forecast - maintenanceCostPercentage, // maintenance cost percentage + newBuildPrice, + depreciatedBuildPrice, + constructionPriceGrowthPerYear, + yearsForecast, + maintenanceCostPercentage, fairhold, }: { newBuildPrice: number; @@ -27,25 +30,8 @@ export class FairholdLandPurchase { affordability: number; fairhold: Fairhold; }) { - this.calculateFairholdDiscount(fairhold); // calculate the fairhold discountLand - this.calculateMortgage(depreciatedBuildPrice); // calculate the mortgage - this.calculateLifetime( - newBuildPrice, - maintenanceCostPercentage, - yearsForecast, - constructionPriceGrowthPerYear - ); // calculate the lifetime - } - - calculateFairholdDiscount(fairhold: Fairhold) { - let discountedLandPrice = fairhold.calculateDiscountedPriceOrRent(); // calculate the discounted land price - this.discountedLandPrice = discountedLandPrice; // discounted land price - } + this.discountedLandPrice = fairhold.calculateDiscountedPriceOrRent(); - calculateMortgage(depreciatedBuildPrice: number) { - if (this.discountedLandPrice == undefined) { - throw new Error("discountedLandPrice is not defined"); - } this.discountedLandMortgage = new Mortgage({ propertyValue: this.discountedLandPrice, }); @@ -53,9 +39,16 @@ export class FairholdLandPurchase { this.depreciatedHouseMortgage = new Mortgage({ propertyValue: depreciatedBuildPrice, }); + + this.lifetime = this.calculateLifetime( + newBuildPrice, + maintenanceCostPercentage, + yearsForecast, + constructionPriceGrowthPerYear + ); } - calculateLifetime( + private calculateLifetime( newBuildPrice: number, maintenanceCostPercentage: number, yearsForecast: number, @@ -63,61 +56,44 @@ export class FairholdLandPurchase { ) { let newBuildPriceIterative = newBuildPrice; let maintenanceCostIterative = maintenanceCostPercentage * newBuildPrice; - // retrieve the mortgage payments for the first year - if ( - this.depreciatedHouseMortgage === undefined || - this.depreciatedHouseMortgage.yearlyPaymentBreakdown === undefined - ) { - throw new Error("depreciatedHouseMortgage is undefined"); - } - interface mortgageBreakdownTypes { - yearlyPayment: number; - cumulativePaid: number; - remainingBalance: number; - } - const houseMortgagePaymentYearly = this.depreciatedHouseMortgage - .yearlyPaymentBreakdown as mortgageBreakdownTypes[]; - let houseMortgagePaymentYearlyIterative = - houseMortgagePaymentYearly[0].yearlyPayment; // find the first year + const houseMortgagePaymentYearly = + this.depreciatedHouseMortgage.yearlyPaymentBreakdown; - if ( - this.discountedLandMortgage === undefined || - this.discountedLandMortgage.yearlyPaymentBreakdown === undefined - ) { - throw new Error("depreciatedHouseMortgage is undefined"); - } + // find the first year + let houseMortgagePaymentYearlyIterative = + houseMortgagePaymentYearly[0].yearlyPayment; - const landMortgagePaymentYearly = this.discountedLandMortgage - .yearlyPaymentBreakdown as mortgageBreakdownTypes[]; + const landMortgagePaymentYearly = + this.discountedLandMortgage.yearlyPaymentBreakdown; + // find the first year let landMortgagePaymentYearlyIterative = - landMortgagePaymentYearly[0].yearlyPayment; // find the first year - - interface lifetimeTypes { - maintenanceCost: number; - landMortgagePaymentYearly: number; - houseMortgagePaymentYearly: number; - } + landMortgagePaymentYearly[0].yearlyPayment; - let lifetime: lifetimeTypes[] = [ + let lifetime: Lifetime = [ { maintenanceCost: maintenanceCostIterative, landMortgagePaymentYearly: landMortgagePaymentYearlyIterative, houseMortgagePaymentYearly: houseMortgagePaymentYearlyIterative, }, - ]; // initialize the forecast + ]; + for (let i = 0; i < yearsForecast - 1; i++) { + // calculate the new build price at a given year newBuildPriceIterative = - newBuildPriceIterative * (1 + constructionPriceGrowthPerYear); // calculate the new build price at a given year + newBuildPriceIterative * (1 + constructionPriceGrowthPerYear); + // set the current maintenance cost maintenanceCostIterative = - newBuildPriceIterative * maintenanceCostPercentage; // set the current maintenance cost + newBuildPriceIterative * maintenanceCostPercentage; if (i < houseMortgagePaymentYearly.length - 1) { + // find the first year houseMortgagePaymentYearlyIterative = - houseMortgagePaymentYearly[i + 1].yearlyPayment; // find the first year + houseMortgagePaymentYearly[i + 1].yearlyPayment; + // find the first year landMortgagePaymentYearlyIterative = - landMortgagePaymentYearly[i + 1].yearlyPayment; // find the first year + landMortgagePaymentYearly[i + 1].yearlyPayment; } else { houseMortgagePaymentYearlyIterative = 0; landMortgagePaymentYearlyIterative = 0; @@ -127,8 +103,9 @@ export class FairholdLandPurchase { maintenanceCost: maintenanceCostIterative, landMortgagePaymentYearly: landMortgagePaymentYearlyIterative, houseMortgagePaymentYearly: houseMortgagePaymentYearlyIterative, - }); // add the current price to the new build price forecast + }); } - this.lifetime = lifetime; // save the object + + return lifetime; } }