Skip to content

Commit

Permalink
Merge pull request #214 from theopensystemslab/oz/test-helpers
Browse files Browse the repository at this point in the history
test: test helper functions
  • Loading branch information
zz-hh-aa authored Jan 9, 2025
2 parents 16500af + 5fbe59c commit d85d585
Show file tree
Hide file tree
Showing 6 changed files with 745 additions and 116 deletions.
67 changes: 67 additions & 0 deletions app/models/Lifetime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Lifetime } from "./Lifetime";
import { createTestProperty, createTestLifetime } from "./testHelpers";

let lifetime = createTestLifetime();

beforeEach(() => {
lifetime = createTestLifetime();
})

it("can be instantiated", () => {
expect(lifetime).toBeInstanceOf(Lifetime);
})

it("creates an array with the correct number of years", () => {
expect(lifetime.lifetimeData).toHaveLength(40)
})

it("reduces mortgage payments to 0 after the mortgage term is reached", () => {
expect(lifetime.lifetimeData[35].newbuildHouseMortgageYearly).toBe(0);
expect(lifetime.lifetimeData[34].marketLandMortgageYearly).toBe(0);
expect(lifetime.lifetimeData[33].fairholdLandMortgageYearly).toBe(0);
expect(lifetime.lifetimeData[32].marketLandMortgageYearly).toBe(0);
})

describe("resale values", () => {
it("correctly calculates for a newbuild house", () => {
// Test newbuild (age 0)
lifetime = createTestLifetime({
property: createTestProperty({ age: 0 })
});
expect(lifetime.lifetimeData[0].depreciatedHouseResaleValue).toBe(186560);
});
it("correctly calculates for a 10-year old house", () => { // Test 10-year-old house
lifetime = createTestLifetime({
property: createTestProperty({
age: 10,
newBuildPricePerMetre: 2120,
size: 88
})
});
// Calculate expected depreciation running `calculateDepreciatedBuildPrice()` method on its own
const houseY10 = createTestProperty({
age: 10,
newBuildPricePerMetre: 2120,
size: 88
})
const depreciatedHouseY10 = houseY10.calculateDepreciatedBuildPrice()
expect(lifetime.lifetimeData[0].depreciatedHouseResaleValue).toBe(depreciatedHouseY10);
});
it("depreciates the house over time", () => {
// Test value changes over time
expect(lifetime.lifetimeData[0].depreciatedHouseResaleValue).toBeGreaterThan(
lifetime.lifetimeData[10].depreciatedHouseResaleValue);
})
});


it("correctly ages the house", () => {
lifetime = createTestLifetime({
property: createTestProperty({ age: 10 })
})

expect(lifetime.lifetimeData[0].houseAge).toBe(10);
expect(lifetime.lifetimeData[5].houseAge).toBe(15);
expect(lifetime.lifetimeData[20].houseAge).toBe(30);
expect(lifetime.lifetimeData[39].houseAge).toBe(49);
})
112 changes: 83 additions & 29 deletions app/models/Lifetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export interface LifetimeData {
marketHouseRentYearly: number;
// we will need the below for newbuilds & retrofits, and oldbuilds
// gasBillYearly: number;
depreciatedHouseResaleValue: number;
fairholdLandPurchaseResaleValue: number;
houseAge: number;
[key: number]: number;
}
/**
Expand All @@ -48,53 +51,87 @@ export class Lifetime {
constructor(params: LifetimeParams) {
this.lifetimeData = this.calculateLifetime(params);
}

/**
* The function loops through and calculates all values for period set by yearsForecast,
* pushing the results to the lifetime array (one object per-year)
*/

/**
* The `calculateLifetime` method creates an array and populates it with instances of the `LifetimeData` object.
* It initialises all variables (except for the mortgage values) using values from `params`, before iterating on them in a `for` loop.
* The mortgages are initialised as empty variables and then given values in a `for` loop that ensures that the year is within the `mortgageTerm`
* @param params
* @returns
*/
private calculateLifetime(params: LifetimeParams): LifetimeData[] {
const lifetime: LifetimeData[] = [];

// // initialise properties; all properties with default value of 0 will be updated in the loop
// this.incomeYearly = params.incomeYearly;
// this.affordabilityThresholdIncome = params.incomeYearly * params.affordabilityThresholdIncomePercentage;
// this.newbuildHouseMortgageYearly = 0;
// this.depreciatedHouseMortgageYearly = 0;
// this.fairholdLandMortgageYearly = 0;
// this.marketLandMortgageYearly = 0;
// this.fairholdLandRentYearly = 0;
// this.maintenanceCost = params.property.newBuildPrice * params.maintenanceCostPercentage;
// this.marketLandRentYearly = 0;
// this.marketHouseRentYearly = 0;

// initialise mortgage values
let newbuildHouseMortgageYearlyIterative;
let depreciatedHouseMortgageYearlyIterative;
let fairholdLandMortgageYearlyIterative;
let marketLandMortgageYearlyIterative;

// initialise non-mortgage variables;
/** Increases yearly by `ForecastParameters.incomeGrowthPerYear`*/
let incomeYearlyIterative = params.incomeYearly;
/** 35% (or the value of `affordabilityThresholdIncomePercentage`) multiplied by `incomeYearly`*/
let affordabilityThresholdIncomeIterative =
incomeYearlyIterative * params.affordabilityThresholdIncomePercentage;
/** Increases yearly by `ForecastParameters.propertyPriceGrowthPerYear`*/
let averageMarketPriceIterative = params.property.averageMarketPrice;
/** Increases yearly by `ForecastParameters.constructionPriceGrowthPerYear`*/
let newBuildPriceIterative = params.property.newBuildPrice;
/** Divides `landPrice` by `averageMarketPrice` anew each year as they appreciate */
let landToTotalRatioIterative =
params.property.landPrice / params.property.averageMarketPrice;
/** Subtracts `newBuildPriceIterative` from `averageMarketPriceIterative` */
let landPriceIterative = params.property.landPrice;
/** Increases yearly by `ForecastParameters.rentGrowthPerYear`*/
let marketRentYearlyIterative =
params.marketRent.averageRentMonthly * MONTHS_PER_YEAR;
/** Increases yearly by `ForecastParameters.rentGrowthPerYear`*/
let marketRentAffordabilityIterative =
marketRentYearlyIterative / incomeYearlyIterative
/** Uses the `landToTotalRatioIterative` to calculate the percentage of market rent that goes towards land as the market inflates */
let marketRentLandYearlyIterative =
marketRentYearlyIterative * landToTotalRatioIterative;
/** Subtracts `marketRentLandYearlyIterative` from inflating `marketRentYearlyIterative` */
let marketRentHouseYearlyIterative =
marketRentYearlyIterative - marketRentLandYearlyIterative;
/** The percentage (`ForecastParameters.maintenancePercentage`) is kept steady, and is multiplied by the `newBuildPriceIterative` as it inflates */
let maintenanceCostIterative =
params.maintenancePercentage * newBuildPriceIterative;
/** Each loop a new `Property` instance is created, this is updated by running the `calculateDepreciatedBuildPrice()` method on `iterativeProperty` */
let depreciatedHouseResaleValueIterative = params.property.depreciatedBuildPrice;
/** Resale value increases with `ForecastParameters.constructionPriceGrowthPerYear` */
let fairholdLandPurchaseResaleValueIterative = params.fairholdLandPurchase.discountedLandPrice;
/** Initialises as user input house age and increments by one */
let houseAgeIterative = params.property.age;

// Initialise mortgage variables
/** Assuming a constant interest rate, this figures stays the same until the mortgage term (`marketPurchase.houseMortgage.termYears`) is reached */
let newbuildHouseMortgageYearlyIterative = params.marketPurchase.houseMortgage.yearlyPaymentBreakdown[0].yearlyPayment;
/** Assuming a constant interest rate, this figures stays the same until the mortgage term (`marketPurchase.houseMortgage.termYears`) is reached. `termyears` is the same across tenures, so it doesn't matter that it comes from a `marketPurchase` object here */
let depreciatedHouseMortgageYearlyIterative = params.fairholdLandPurchase.depreciatedHouseMortgage.yearlyPaymentBreakdown[0].yearlyPayment;
/** Assuming a constant interest rate, this figures stays the same until the mortgage term (`marketPurchase.houseMortgage.termYears`) is reached. `termyears` is the same across tenures, so it doesn't matter that it comes from a `marketPurchase` object here */
let fairholdLandMortgageYearlyIterative = params.fairholdLandPurchase.discountedLandMortgage.yearlyPaymentBreakdown[0].yearlyPayment;
/** Assuming a constant interest rate, this figures stays the same until the mortgage term (`marketPurchase.houseMortgage.termYears`) is reached. `termyears` is the same across tenures, so it doesn't matter that it comes from a `marketPurchase` object here */
let marketLandMortgageYearlyIterative = params.marketPurchase.landMortgage.yearlyPaymentBreakdown[0].yearlyPayment;

// New instance of FairholdLandRent class
let fairholdLandRentIterative = new Fairhold({
affordability: marketRentAffordabilityIterative,
landPriceOrRent: marketRentLandYearlyIterative,
}).discountedLandPriceOrRent;

// Push the Y0 values before they start being iterated-upon
lifetime.push({
incomeYearly: incomeYearlyIterative,
affordabilityThresholdIncome: affordabilityThresholdIncomeIterative,
newbuildHouseMortgageYearly: newbuildHouseMortgageYearlyIterative,
depreciatedHouseMortgageYearly: depreciatedHouseMortgageYearlyIterative,
fairholdLandMortgageYearly: fairholdLandMortgageYearlyIterative,
marketLandMortgageYearly: marketLandMortgageYearlyIterative,
fairholdLandRentYearly: fairholdLandRentIterative,
maintenanceCost: maintenanceCostIterative,
marketLandRentYearly: marketRentLandYearlyIterative,
marketHouseRentYearly: marketRentHouseYearlyIterative,
depreciatedHouseResaleValue: depreciatedHouseResaleValueIterative,
fairholdLandPurchaseResaleValue: fairholdLandPurchaseResaleValueIterative,
houseAge: houseAgeIterative,
});

for (let i = 0; i < params.yearsForecast - 1; i++) {
// The 0th round has already been calculated and pushed above
for (let i = 1; i <= params.yearsForecast - 1; i++) {
incomeYearlyIterative =
incomeYearlyIterative * (1 + params.incomeGrowthPerYear);
affordabilityThresholdIncomeIterative =
Expand All @@ -117,6 +154,20 @@ export class Lifetime {
marketRentYearlyIterative - marketRentLandYearlyIterative;
maintenanceCostIterative =
newBuildPriceIterative * params.maintenancePercentage;

/**
* A new instance of the `Property` class is needed each loop in order to
re-calculate the depreciated house value as the house gets older
*/
const iterativeProperty = new Property({
...params.property,
age: houseAgeIterative
});
/* Use the `calculateDepreciatedBuildPrice()` method on the new `Property` class
to calculate an updated depreciated house value */
depreciatedHouseResaleValueIterative = iterativeProperty.calculateDepreciatedBuildPrice()
fairholdLandPurchaseResaleValueIterative = fairholdLandPurchaseResaleValueIterative * (1 + params.constructionPriceGrowthPerYear) // TODO: replace variable name with cpiGrowthPerYear
houseAgeIterative += 1

// If the mortgage term ongoing (if `i` is less than the term), calculate yearly mortgage payments
if (i < params.marketPurchase.houseMortgage.termYears - 1) {
Expand All @@ -132,14 +183,14 @@ export class Lifetime {
marketLandMortgageYearlyIterative = 0;
}

/* A new instance of the Fairhold class is needed here in order to
/* A new instance of the Fairhold class is needed in each loop in order to
re-calculate land rent values per-year (with updating local house prices
and income levels)*/
const fairholdLandRentIterative = new Fairhold({
fairholdLandRentIterative = new Fairhold({
affordability: marketRentAffordabilityIterative,
landPriceOrRent: marketRentLandYearlyIterative,
}).discountedLandPriceOrRent;

lifetime.push({
incomeYearly: incomeYearlyIterative,
affordabilityThresholdIncome: affordabilityThresholdIncomeIterative,
Expand All @@ -151,6 +202,9 @@ export class Lifetime {
maintenanceCost: maintenanceCostIterative,
marketLandRentYearly: marketRentLandYearlyIterative,
marketHouseRentYearly: marketRentHouseYearlyIterative,
depreciatedHouseResaleValue: depreciatedHouseResaleValueIterative,
fairholdLandPurchaseResaleValue: fairholdLandPurchaseResaleValueIterative,
houseAge: houseAgeIterative,
});
}
return lifetime;
Expand Down
Loading

0 comments on commit d85d585

Please sign in to comment.