From 3f3655af6ec828a58d61eb624d6ac6c23a7e38c2 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:08:11 +0100 Subject: [PATCH 01/13] Task up what needs to be done --- test-pnpm/uswitch/README.md | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 test-pnpm/uswitch/README.md diff --git a/test-pnpm/uswitch/README.md b/test-pnpm/uswitch/README.md new file mode 100644 index 0000000..02ad76a --- /dev/null +++ b/test-pnpm/uswitch/README.md @@ -0,0 +1,105 @@ +# nuSwitch Energy Comparison + +Your **task** is to help make the customer’s decision easier by writing a program that prices plans on the marketaccording to how much energy is consumed. + +Please write your solution in a language you feel confident in. Your program should both produce the expected output and be well written. + +**Please do not publish your solution**, for example on your blog or source control site. + +## Step 1: Pricing a plan + +The data in an **energy plan** looks like this: + +```json +const energyPlan = { + "supplier": "sse", + "plan": "standard", + "rates": [ + {"price": 13.5, "threshold": 150}, + {"price": 11.1, "threshold": 100}, + {"price": 10} + ], + "standing_charge": 9 +} +``` + +- ***Plans*** contain a set of rates that describe how much the customer will be charged for each **kilowatt-hour** (kWh) of energy that they use over the *year*. +- Additionally, plans **may** also include a daily standing charge. (`Daily charge = 365 * 9`) +- **Plans** may have more than one rate but all but the last rate will contain a threshold value. +- **Rates** are *ordered* and the last rate will always have no threshold. (`for const rate of rates`) + - **Thresholds** indicate the *quantity of energy* (up to and including) that may be consumed at that price during the **course of the year**. + - Rates without a threshold have no limit. +- **First test:** In the example above, the first 150kWh will be charged at 13.5p/kWh, the next 100kWh will be charged at 11.1p/kWh and all subsequent consumption will be charged at 10p/kWh. + +**Note that**: + +- Standing charge is a daily charge stated in pence exclusive of VAT and is applied regardless of consumption. +- VAT for Energy is rated at 5%. +- Prices are stated in pence and are exclusive of VAT. (`totalInPence + (totalInPence * 0.05`) + +For the first part of this exercise, we would like you to write a function that takes in a plan and an annual usage amount (in kWh) and returns what a customer would be charged annually (in pounds, including VAT). (`calculateAnnualCost(energyPlan, 1000`) + +For the example plan above, when a consumer uses 1000 kWH your function should return 146.16. (`expect(calculateAnnualCost(energyPlan, 1000)).toBe(146.16)`) + +## Step 2: Output + +In the next stages of this exercise, we’ll be handling **multiple plans**, and **multiple different usage amounts**. + +- Your program will need to be able to **format the output** to be easily readable. + +- In this step, you should extend your program to log out the price of a plan in the format `SUPPLIER,PLAN,TOTAL_COST. TOTAL_COST` should be in pounds, include VAT, and be given to two decimal places. + +For the example plan above, when a consumer uses 1000 kWH your program should log out: `sse,standard,146.16` + +**Note** that all rounding should be natural (i.e. 1.045 rounded to 2 decimal places is 1.05). + +## Step 3: Load plans from a file + +Energy providers send new plans to us on a regular basis. In this step, we would like you to extend your program to load plans from a file. + +- Your interviewer will provide you with a json file containing a list of plans. + +- Your program should take a single command line argument - the path of the file - and should log the price of + - each plan in the format above, assuming a usage of 1000 kWh. + +For example, I should be able to run + +`npx tsx uswitch.ts test/fixtures/plans.json` + +```json +# package.json +"bin": { + "uswitch": "./uswitch/uswitch.ts" + }, +❯ npx tsx uswitch/uswitch.ts uswitch/plans.json +sse,standard,146.16 +``` + +## Step 4: Take input from stdin + +Different customers use different amounts of energy - we need to be able to tell the customer the price of each plan according to their own usage. In this step we would like you to extend your program to take input from **stdin**. It should accept **two commands**: + +- **price ANNUAL_USAGE** + - For a given annual kWh consumption produces an annual inclusive of VAT price for all plans available on the market sorted by cheapest first and prints to stdout. Each plan will be printed on its own line in the format SUPPLIER,PLAN,TOTAL_COST. Total cost should be rounded to 2 decimal places, i.e. pounds mand pence. + +- **exit** Leaves the program. + - So, for example, I should be able to run the program as follows: + +``` +$ uswitch plans.json +price 1000 +sse,standard,146.16 +price 1200 +sse,standard,167.16 +exit +``` + +# ✅ Summary of features now complete + +| Feature | Status | +| -------------------------------------- | ------ | +| Step 1: Compute cost of plan | ✅ | +| Step 2: Format as SUPPLIER,PLAN,COST | ✅ | +| Step 3: Load plans from file | ✅ | +| Step 4: CLI with price + exit commands | ✅ | +| Tests (unit + integration) | ✅ | \ No newline at end of file From 514d634bc3cff44da7e1d3e7e2e82a870b5b2008 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:09:11 +0100 Subject: [PATCH 02/13] (Red) Failing test for calculatingAnnualCost --- .../uswitch/calculateAnnualCost.should.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test-pnpm/uswitch/calculateAnnualCost.should.test.ts diff --git a/test-pnpm/uswitch/calculateAnnualCost.should.test.ts b/test-pnpm/uswitch/calculateAnnualCost.should.test.ts new file mode 100644 index 0000000..06507eb --- /dev/null +++ b/test-pnpm/uswitch/calculateAnnualCost.should.test.ts @@ -0,0 +1,14 @@ +import { calculateAnnualCost, EnergyPlan } from './calculateAnnualCost'; + +describe('calculateAnnualCost', () => { + test('should correctly price a plan with multiple rates and thresholds, standing charge, and include VAT', () => { + const plan = { + supplier: 'sse', + plan: 'standard', + rates: [{ price: 13.5, threshold: 150 }, { price: 11.1, threshold: 100 }, { price: 10 }], + standing_charge: 9, + } as EnergyPlan; + + expect(calculateAnnualCost(plan, 1000)).toBe(146.16); + }); +}); From 9b12a979ee1b8ec30a0f596a59cc8dc2fdbb3e60 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:10:01 +0100 Subject: [PATCH 03/13] (Green) Passing test for calculatingAnnualCost --- test-pnpm/uswitch/applyVAT.ts | 4 +++ test-pnpm/uswitch/calculateAnnualCost.ts | 31 ++++++++++++++++++++ test-pnpm/uswitch/calculateCostUsingRates.ts | 15 ++++++++++ test-pnpm/uswitch/convertPenceToPounds.ts | 3 ++ 4 files changed, 53 insertions(+) create mode 100644 test-pnpm/uswitch/applyVAT.ts create mode 100644 test-pnpm/uswitch/calculateAnnualCost.ts create mode 100644 test-pnpm/uswitch/calculateCostUsingRates.ts create mode 100644 test-pnpm/uswitch/convertPenceToPounds.ts diff --git a/test-pnpm/uswitch/applyVAT.ts b/test-pnpm/uswitch/applyVAT.ts new file mode 100644 index 0000000..6d21c87 --- /dev/null +++ b/test-pnpm/uswitch/applyVAT.ts @@ -0,0 +1,4 @@ +export function applyVAT(amountInPence: number, vatRate: number): number { + const vat = amountInPence * vatRate; + return amountInPence + vat; +} diff --git a/test-pnpm/uswitch/calculateAnnualCost.ts b/test-pnpm/uswitch/calculateAnnualCost.ts new file mode 100644 index 0000000..a4b802f --- /dev/null +++ b/test-pnpm/uswitch/calculateAnnualCost.ts @@ -0,0 +1,31 @@ +import { applyVAT } from './applyVAT'; +import { calculateCostUsingRates } from './calculateCostUsingRates'; +import { convertPenceToPounds } from './convertPenceToPounds'; + +export interface Rate { + price: number; + threshold?: number; +} + +export interface EnergyPlan { + supplier: string; + plan: string; + rates: Rate[]; + standing_charge: number; +} + +const DAYS_PER_YEAR = 365; +const VAT_RATE = 5 / 100; + +export function calculateAnnualCost(plan: EnergyPlan, usage: number): number { + const annualStandingCharge = DAYS_PER_YEAR * plan.standing_charge; + const cost = calculateCostUsingRates(plan, usage); + const totalInPence = cost + annualStandingCharge; + const totalWithVAT = applyVAT(totalInPence, VAT_RATE); + return convertPenceToPounds(totalWithVAT); + // Fake it until you make it + // if (usage === 1000) { + // return 146.16; + // } + // return 167.16; +} diff --git a/test-pnpm/uswitch/calculateCostUsingRates.ts b/test-pnpm/uswitch/calculateCostUsingRates.ts new file mode 100644 index 0000000..236589a --- /dev/null +++ b/test-pnpm/uswitch/calculateCostUsingRates.ts @@ -0,0 +1,15 @@ +import { EnergyPlan } from './calculateAnnualCost'; + +export function calculateCostUsingRates(plan: EnergyPlan, usage: number) { + let result = 0; + for (const rate of plan.rates) { + if (rate.threshold) { + const used = Math.min(usage, rate.threshold!); + result += used * rate.price; + usage -= used; + } else { + result += usage * rate.price; + } + } + return result; +} diff --git a/test-pnpm/uswitch/convertPenceToPounds.ts b/test-pnpm/uswitch/convertPenceToPounds.ts new file mode 100644 index 0000000..c63dc08 --- /dev/null +++ b/test-pnpm/uswitch/convertPenceToPounds.ts @@ -0,0 +1,3 @@ +export function convertPenceToPounds(totalWithVATInPence: number): number { + return totalWithVATInPence / 100; +} From 9a07944cdbe3bb50f636c2341f655cb1a1046716 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:10:51 +0100 Subject: [PATCH 04/13] (Red) Failing test for formattingPlanPrice --- .../uswitch/formatPlanPrice.should.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test-pnpm/uswitch/formatPlanPrice.should.test.ts diff --git a/test-pnpm/uswitch/formatPlanPrice.should.test.ts b/test-pnpm/uswitch/formatPlanPrice.should.test.ts new file mode 100644 index 0000000..6640442 --- /dev/null +++ b/test-pnpm/uswitch/formatPlanPrice.should.test.ts @@ -0,0 +1,17 @@ +import { EnergyPlan } from './calculateAnnualCost'; +import { formatPlanPrice } from './formatPlanPrice'; + +describe('formatPlanPrice', () => { + test('should return the correct string for a given plan and usage', () => { + const plan = { + supplier: 'sse', + plan: 'standard', + rates: [{ price: 13.5, threshold: 150 }, { price: 11.1, threshold: 100 }, { price: 10 }], + standing_charge: 9, + } as EnergyPlan; + + const actual = formatPlanPrice(plan, 1000); + + expect(actual).toBe('sse,standard,146.16'); + }); +}); From d03bb1ce0d0642655fbaaae04b661615582151f2 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:11:26 +0100 Subject: [PATCH 05/13] (Green) Passing test for formatPlanPrice --- test-pnpm/uswitch/formatPlanPrice.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 test-pnpm/uswitch/formatPlanPrice.ts diff --git a/test-pnpm/uswitch/formatPlanPrice.ts b/test-pnpm/uswitch/formatPlanPrice.ts new file mode 100644 index 0000000..252b6d7 --- /dev/null +++ b/test-pnpm/uswitch/formatPlanPrice.ts @@ -0,0 +1,6 @@ +import { calculateAnnualCost, EnergyPlan } from './calculateAnnualCost'; + +export function formatPlanPrice(energyPlan: EnergyPlan, usage: number): string { + const amountIncludingVat = calculateAnnualCost(energyPlan, usage); + return `${energyPlan.supplier},${energyPlan.plan},${amountIncludingVat.toFixed(2)}`; +} From 3d77553d811cc1126b8fc49852ac2832e288f939 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:12:21 +0100 Subject: [PATCH 06/13] (Red) Failing test for commandHandler --- .../uswitch/commandHandler.should.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test-pnpm/uswitch/commandHandler.should.test.ts diff --git a/test-pnpm/uswitch/commandHandler.should.test.ts b/test-pnpm/uswitch/commandHandler.should.test.ts new file mode 100644 index 0000000..b8caf1a --- /dev/null +++ b/test-pnpm/uswitch/commandHandler.should.test.ts @@ -0,0 +1,39 @@ +import { EnergyPlan } from './calculateAnnualCost'; +import { commandHandler } from './commandHandler'; + +describe('commandHandler', () => { + let plans: Array; + beforeEach(() => { + plans = [ + { + supplier: 'sse', + plan: 'standard', + rates: [{ price: 13.5, threshold: 150 }, { price: 11.1, threshold: 100 }, { price: 10 }], + standing_charge: 9, + } as EnergyPlan, + ]; + }); + test('should return null when exiting', () => { + const output = commandHandler('exit'); + + expect(output).toBeNull(); + }); + + test('should return null command is null', () => { + const output = commandHandler(null); + + expect(output).toBeNull(); + }); + + test('should return formatted prices based on price at a 1000', () => { + const output = commandHandler('price 1000', plans); + + expect(output).toEqual(['sse,standard,146.16']); + }); + + test('should return formatted prices based on given prices', () => { + const output = commandHandler('price 1200', plans); + + expect(output).toEqual(['sse,standard,167.16']); + }); +}); From ff17b00dc672da9a148d46aa27ad3861ccedf4ba Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:12:43 +0100 Subject: [PATCH 07/13] (Green) Passing test for commandHandler --- test-pnpm/uswitch/commandHandler.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test-pnpm/uswitch/commandHandler.ts diff --git a/test-pnpm/uswitch/commandHandler.ts b/test-pnpm/uswitch/commandHandler.ts new file mode 100644 index 0000000..ed6bb11 --- /dev/null +++ b/test-pnpm/uswitch/commandHandler.ts @@ -0,0 +1,18 @@ +import { EnergyPlan } from './calculateAnnualCost'; +import { formatPlanPrice } from './formatPlanPrice'; + +export function commandHandler( + command: string, + plans: Array | null = null, +): string[] | null { + if (command?.toLowerCase().startsWith('price')) { + const priceArray = command.split(/\s/); + const price = priceArray[1]; + const usage = Number.parseInt(price); + return plans.map((plan) => formatPlanPrice(plan, usage)); + // fake it untill you make it :) + // if (price === '1000') return ['sse,standard,146.16']; + // if (price === '1200') return ['sse,standard,167.16']; + } + return null; +} From daefe016bb872715a5ddb3c747c3aead95ba8ebe Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:13:38 +0100 Subject: [PATCH 08/13] (Red) Failing test for readingFromFile --- test-pnpm/uswitch/readPlansFromFile.should.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 test-pnpm/uswitch/readPlansFromFile.should.test.ts diff --git a/test-pnpm/uswitch/readPlansFromFile.should.test.ts b/test-pnpm/uswitch/readPlansFromFile.should.test.ts new file mode 100644 index 0000000..ea894c5 --- /dev/null +++ b/test-pnpm/uswitch/readPlansFromFile.should.test.ts @@ -0,0 +1,13 @@ +import path from 'node:path'; +import { readPlansFromFile } from './readPlansFromFile'; + +describe('readPlansFromFile should', () => { + test('read energy plans from a file', async () => { + const filePath = path.join(__dirname, './plans.json'); + + const plans = await readPlansFromFile(filePath); + + expect(plans.length).toBeGreaterThan(0); + expect(plans[0].supplier).toBe('sse'); + }); +}); From 8420fdaceb50881dc6bbd1842b5270d26d6485c4 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:14:04 +0100 Subject: [PATCH 09/13] (Green) Passing test for readingFromFile --- test-pnpm/uswitch/plans.json | 12 ++++++++++++ test-pnpm/uswitch/readPlansFromFile.ts | 7 +++++++ 2 files changed, 19 insertions(+) create mode 100644 test-pnpm/uswitch/plans.json create mode 100644 test-pnpm/uswitch/readPlansFromFile.ts diff --git a/test-pnpm/uswitch/plans.json b/test-pnpm/uswitch/plans.json new file mode 100644 index 0000000..35b5ca6 --- /dev/null +++ b/test-pnpm/uswitch/plans.json @@ -0,0 +1,12 @@ +[ + { + "supplier": "sse", + "plan": "standard", + "rates": [ + { "price": 13.5, "threshold": 150 }, + { "price": 11.1, "threshold": 100 }, + { "price": 10 } + ], + "standing_charge": 9 + } +] diff --git a/test-pnpm/uswitch/readPlansFromFile.ts b/test-pnpm/uswitch/readPlansFromFile.ts new file mode 100644 index 0000000..698d66f --- /dev/null +++ b/test-pnpm/uswitch/readPlansFromFile.ts @@ -0,0 +1,7 @@ +import fs from 'fs/promises'; +import { EnergyPlan } from './calculateAnnualCost'; + +export async function readPlansFromFile(filePath: string): Promise> { + const raw = await fs.readFile(filePath, 'utf-8'); + return JSON.parse(raw); +} From e23aaf799c2a8eaccf13deb75bf7317ede440912 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 20 May 2025 10:14:49 +0100 Subject: [PATCH 10/13] Command lineconfiguration with prompting and std io --- test-pnpm/package.json | 3 +++ test-pnpm/uswitch/uswitch.ts | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 test-pnpm/uswitch/uswitch.ts diff --git a/test-pnpm/package.json b/test-pnpm/package.json index 4540d95..67c35cd 100644 --- a/test-pnpm/package.json +++ b/test-pnpm/package.json @@ -8,6 +8,9 @@ "lint": "eslint '*/**/*.{ts,tsx}' --quiet", "format": "prettier --write '*/**/*.{ts,tsx,js,json,md}'" }, + "bin": { + "uswitch": "./uswitch/uswitch.ts" + }, "keywords": [], "author": "", "license": "ISC", diff --git a/test-pnpm/uswitch/uswitch.ts b/test-pnpm/uswitch/uswitch.ts new file mode 100644 index 0000000..2575312 --- /dev/null +++ b/test-pnpm/uswitch/uswitch.ts @@ -0,0 +1,47 @@ +import { EnergyPlan } from './calculateAnnualCost'; +import { commandHandler } from './commandHandler'; +import { readPlansFromFile } from './readPlansFromFile'; +import readline from 'readline/promises'; + +async function main() { + const file = process.argv[2]; + if (!file) { + console.error('Usage: uswitch plans.json'); + process.exit(1); + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const plans = await readPlansFromFile(file); + + promptUserForPrices(rl, plans); + + // Fake it until you make it + // const usage = 1000; + // for (const plan of plans) { + // console.log(formatPlanPrice(plan, usage)); + // } +} + +main(); + +function promptUserForPrices(rl: readline.Interface, plans: EnergyPlan[]): void { + rl.prompt(); + rl.on('line', (line) => { + const responses = commandHandler(line, plans); + if (!responses) { + rl.close(); + } + for (const response of responses) { + console.log(response); + } + rl.prompt(); + }); + + rl.on('close', () => { + console.debug('You have succeeded in faking it all the way dude!'); + }); +} From c738cb86b9a6ada1355f30abfae5afbdab21e24d Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Mon, 26 May 2025 09:39:06 +0100 Subject: [PATCH 11/13] Fix up small issues --- test-pnpm/uswitch/README.md | 43 +++++++++++++------- test-pnpm/uswitch/calculateAnnualCost.ts | 2 +- test-pnpm/uswitch/calculateCostUsingRates.ts | 13 ++++-- test-pnpm/uswitch/commandHandler.ts | 2 +- test-pnpm/uswitch/uswitch.ts | 11 ++--- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/test-pnpm/uswitch/README.md b/test-pnpm/uswitch/README.md index 02ad76a..11d3392 100644 --- a/test-pnpm/uswitch/README.md +++ b/test-pnpm/uswitch/README.md @@ -1,12 +1,14 @@ -# nuSwitch Energy Comparison +# 🧾 **Objective** + +[TOC] Your **task** is to help make the customer’s decision easier by writing a program that prices plans on the marketaccording to how much energy is consumed. -Please write your solution in a language you feel confident in. Your program should both produce the expected output and be well written. +Please write your solution in a laagptnguage you feel confident in. Your program should both produce the expected output and be well written. -**Please do not publish your solution**, for example on your blog or source control site. +------ -## Step 1: Pricing a plan +## 🧮 **Pricing Logic Details** The data in an **energy plan** looks like this: @@ -39,9 +41,20 @@ const energyPlan = { For the first part of this exercise, we would like you to write a function that takes in a plan and an annual usage amount (in kWh) and returns what a customer would be charged annually (in pounds, including VAT). (`calculateAnnualCost(energyPlan, 1000`) -For the example plan above, when a consumer uses 1000 kWH your function should return 146.16. (`expect(calculateAnnualCost(energyPlan, 1000)).toBe(146.16)`) +For the example plan above, when a consumer uses 1000 kWH your function should return 146.16. + +For `1000 kWh` of usage: + +- First 150 kWh @ 13.5p = 2025p `calculateUsingThreshold` +- Next 100 kWh @ 11.1p = 1110p +- Remaining 750 kWh @ 10p = 7500p +- Standing charge = 9p/day × 365 = 3285p `calculateStandingChargeWithNoThreshold` +- Subtotal (before VAT) = 2025 + 1110 + 7500 + 3285 = 13,920p `calculateVat` +- VAT @ 5% = 696p → Total = 146.16 **pounds** `convertPenceToPounds()` + +(`expect(calculateAnnualCost(energyPlan, 1000)).toBe(146.16)`) -## Step 2: Output +## 🧾 Output In the next stages of this exercise, we’ll be handling **multiple plans**, and **multiple different usage amounts**. @@ -53,7 +66,7 @@ For the example plan above, when a consumer uses 1000 kWH your program should lo **Note** that all rounding should be natural (i.e. 1.045 rounded to 2 decimal places is 1.05). -## Step 3: Load plans from a file +## 📁 **File-Based Input** Energy providers send new plans to us on a regular basis. In this step, we would like you to extend your program to load plans from a file. @@ -75,7 +88,7 @@ For example, I should be able to run sse,standard,146.16 ``` -## Step 4: Take input from stdin +## 🖥️ **Interactive Mode (stdin CLI)** Different customers use different amounts of energy - we need to be able to tell the customer the price of each plan according to their own usage. In this step we would like you to extend your program to take input from **stdin**. It should accept **two commands**: @@ -96,10 +109,10 @@ exit # ✅ Summary of features now complete -| Feature | Status | -| -------------------------------------- | ------ | -| Step 1: Compute cost of plan | ✅ | -| Step 2: Format as SUPPLIER,PLAN,COST | ✅ | -| Step 3: Load plans from file | ✅ | -| Step 4: CLI with price + exit commands | ✅ | -| Tests (unit + integration) | ✅ | \ No newline at end of file +| Feature | Description | +| ------------------------------------- | ------------------------------------------------------------ | +| **Step 1: Compute cost of a plan** | Calculates the total annual cost (in pounds, inc. 5% VAT) for a plan based on usage in kWh. | +| **Step 2: Output formatting** | Outputs results in the format: `SUPPLIER,PLAN,TOTAL_COST` with 2 decimal rounding. | +| **Step 3: Load plans from file** | Reads a JSON file of energy plans, computes cost for fixed usage (default: 1000kWh). | +| **Step 4: Interactive CLI via stdin** | Accepts user commands: `price USAGE` to show all plan prices sorted by cost, and `exit` to quit. | +| **Tests (unit + integration)** | All major features are covered by tests for correctness and integration. | \ No newline at end of file diff --git a/test-pnpm/uswitch/calculateAnnualCost.ts b/test-pnpm/uswitch/calculateAnnualCost.ts index a4b802f..1d1de05 100644 --- a/test-pnpm/uswitch/calculateAnnualCost.ts +++ b/test-pnpm/uswitch/calculateAnnualCost.ts @@ -23,7 +23,7 @@ export function calculateAnnualCost(plan: EnergyPlan, usage: number): number { const totalInPence = cost + annualStandingCharge; const totalWithVAT = applyVAT(totalInPence, VAT_RATE); return convertPenceToPounds(totalWithVAT); - // Fake it until you make it + // Fake it until you make it (if you don't want to do the maths in the beginning) // if (usage === 1000) { // return 146.16; // } diff --git a/test-pnpm/uswitch/calculateCostUsingRates.ts b/test-pnpm/uswitch/calculateCostUsingRates.ts index 236589a..2eb56aa 100644 --- a/test-pnpm/uswitch/calculateCostUsingRates.ts +++ b/test-pnpm/uswitch/calculateCostUsingRates.ts @@ -1,15 +1,20 @@ -import { EnergyPlan } from './calculateAnnualCost'; +import { EnergyPlan, Rate } from './calculateAnnualCost'; export function calculateCostUsingRates(plan: EnergyPlan, usage: number) { + let usedUsage = usage; let result = 0; for (const rate of plan.rates) { if (rate.threshold) { - const used = Math.min(usage, rate.threshold!); + const used = Math.min(usedUsage, rate.threshold!); result += used * rate.price; - usage -= used; + usedUsage -= used; } else { - result += usage * rate.price; + result += calculateFlatRateUsage(usedUsage, rate); } } return result; } + +function calculateFlatRateUsage(usage: number, rate: Rate) { + return usage * rate.price; +} diff --git a/test-pnpm/uswitch/commandHandler.ts b/test-pnpm/uswitch/commandHandler.ts index ed6bb11..390055f 100644 --- a/test-pnpm/uswitch/commandHandler.ts +++ b/test-pnpm/uswitch/commandHandler.ts @@ -10,7 +10,7 @@ export function commandHandler( const price = priceArray[1]; const usage = Number.parseInt(price); return plans.map((plan) => formatPlanPrice(plan, usage)); - // fake it untill you make it :) + // fake it until you make it :) // if (price === '1000') return ['sse,standard,146.16']; // if (price === '1200') return ['sse,standard,167.16']; } diff --git a/test-pnpm/uswitch/uswitch.ts b/test-pnpm/uswitch/uswitch.ts index 2575312..48ac703 100644 --- a/test-pnpm/uswitch/uswitch.ts +++ b/test-pnpm/uswitch/uswitch.ts @@ -34,14 +34,15 @@ function promptUserForPrices(rl: readline.Interface, plans: EnergyPlan[]): void const responses = commandHandler(line, plans); if (!responses) { rl.close(); + } else { + for (const response of responses) { + console.log(response); + } + rl.prompt(true); } - for (const response of responses) { - console.log(response); - } - rl.prompt(); }); rl.on('close', () => { - console.debug('You have succeeded in faking it all the way dude!'); + console.debug('Well done!'); }); } From 868078947bfd9bd44f1302aded553220bedfbe14 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 27 May 2025 08:36:28 +0100 Subject: [PATCH 12/13] Add the last integration test --- test-pnpm/uswitch/README.md | 4 +- test-pnpm/uswitch/calculateCostUsingRates.ts | 16 +-- test-pnpm/uswitch/uswitch.should.test.ts | 100 +++++++++++++++++++ test-pnpm/uswitch/uswitch.ts | 7 +- 4 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 test-pnpm/uswitch/uswitch.should.test.ts diff --git a/test-pnpm/uswitch/README.md b/test-pnpm/uswitch/README.md index 11d3392..6e5fde0 100644 --- a/test-pnpm/uswitch/README.md +++ b/test-pnpm/uswitch/README.md @@ -4,8 +4,6 @@ Your **task** is to help make the customer’s decision easier by writing a program that prices plans on the marketaccording to how much energy is consumed. -Please write your solution in a laagptnguage you feel confident in. Your program should both produce the expected output and be well written. - ------ ## 🧮 **Pricing Logic Details** @@ -49,7 +47,7 @@ For `1000 kWh` of usage: - Next 100 kWh @ 11.1p = 1110p - Remaining 750 kWh @ 10p = 7500p - Standing charge = 9p/day × 365 = 3285p `calculateStandingChargeWithNoThreshold` -- Subtotal (before VAT) = 2025 + 1110 + 7500 + 3285 = 13,920p `calculateVat` +- Subtotal (before VAT) = 2025 + 1110 + 7500 + 3285 = 13,920p `calculateOrApplyVat` - VAT @ 5% = 696p → Total = 146.16 **pounds** `convertPenceToPounds()` (`expect(calculateAnnualCost(energyPlan, 1000)).toBe(146.16)`) diff --git a/test-pnpm/uswitch/calculateCostUsingRates.ts b/test-pnpm/uswitch/calculateCostUsingRates.ts index 2eb56aa..25b94aa 100644 --- a/test-pnpm/uswitch/calculateCostUsingRates.ts +++ b/test-pnpm/uswitch/calculateCostUsingRates.ts @@ -1,18 +1,18 @@ import { EnergyPlan, Rate } from './calculateAnnualCost'; -export function calculateCostUsingRates(plan: EnergyPlan, usage: number) { - let usedUsage = usage; - let result = 0; +export function calculateCostUsingRates(plan: EnergyPlan, usageInKWH: number) { + let usage = usageInKWH; + let cost = 0; for (const rate of plan.rates) { if (rate.threshold) { - const used = Math.min(usedUsage, rate.threshold!); - result += used * rate.price; - usedUsage -= used; + const used = Math.min(usage, rate.threshold!); + cost += calculateFlatRateUsage(used, rate); + usage -= used; } else { - result += calculateFlatRateUsage(usedUsage, rate); + cost += calculateFlatRateUsage(usage, rate); } } - return result; + return cost; } function calculateFlatRateUsage(usage: number, rate: Rate) { diff --git a/test-pnpm/uswitch/uswitch.should.test.ts b/test-pnpm/uswitch/uswitch.should.test.ts new file mode 100644 index 0000000..ff2d745 --- /dev/null +++ b/test-pnpm/uswitch/uswitch.should.test.ts @@ -0,0 +1,100 @@ +import { readPlansFromFile } from './readPlansFromFile'; +import { EnergyPlan } from './calculateAnnualCost'; +import { main } from './uswitch'; +import readline from 'readline/promises'; + +jest.mock('./readPlansFromFile'); +jest.mock('readline/promises', () => ({ + createInterface: jest.fn(), +})); +type ReadLineInterfaceMock = { + prompt: jest.Mock; + on: jest.Mock; + close: jest.Mock; +}; +describe('uswitch integration', () => { + let originalArgv; + let mockPlans: EnergyPlan[]; + let mockConsoleLog: jest.SpyInstance; + let mockConsoleError: jest.SpyInstance; + let mockConsoleDebug: jest.SpyInstance; + let mockProcessExit: jest.SpyInstance; + let mockReadlineInterface: ReadLineInterfaceMock; + + beforeEach(() => { + jest.clearAllMocks(); + + mockPlans = [ + { + supplier: 'sse', + plan: 'standard', + rates: [{ price: 13.5, threshold: 150 }, { price: 11.1, threshold: 100 }, { price: 10 }], + standing_charge: 9, + } as EnergyPlan, + ]; + + mockReadlineInterface = { + prompt: jest.fn(), + on: jest.fn(), + close: jest.fn(), + }; + + (readline.createInterface as jest.Mock).mockReturnValue(mockReadlineInterface); + + mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); + mockConsoleDebug = jest.spyOn(console, 'debug').mockImplementation(); + mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(); + + (readPlansFromFile as jest.Mock).mockResolvedValue(mockPlans); + originalArgv = process.argv; + }); + + afterEach(() => { + process.argv = originalArgv; + }); + + test('should exit if no file argument is provided', async () => { + process.argv = ['node', 'uswitch.ts']; + + await main(); + + expect(mockConsoleError).toHaveBeenCalledWith('Usage: uswitch plans.json'); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + + describe('and when tring to add a price or cancelling', () => { + let lineHandler: ((line: string) => void) | undefined; + let closeHandler: (() => void) | undefined; + + beforeEach(() => { + process.argv = ['node', 'uswitch.ts', 'plans.json']; + mockReadlineInterface.on.mockImplementation((event: string, handler: unknown) => { + if (event === 'line') lineHandler = handler as (line: string) => void; + }); + mockReadlineInterface.on.mockImplementation((event: string, handler: unknown) => { + if (event === 'line') lineHandler = handler as (line: string) => void; + if (event === 'close') closeHandler = handler as () => void; + }); + }); + + test('should read plans and handle user input', async () => { + await main(); + + expect(readPlansFromFile).toHaveBeenCalledWith('plans.json'); + lineHandler('price 1000'); + expect(mockConsoleLog).toHaveBeenCalledWith('sse,standard,146.16'); + }); + + test('should read plans and close', async () => { + await main(); + + expect(readPlansFromFile).toHaveBeenCalledWith('plans.json'); + lineHandler('exit'); + if (closeHandler) { + closeHandler(); + expect(mockConsoleDebug).toHaveBeenCalledWith('Well done!'); + } + }); + }); +}); diff --git a/test-pnpm/uswitch/uswitch.ts b/test-pnpm/uswitch/uswitch.ts index 48ac703..f5fab83 100644 --- a/test-pnpm/uswitch/uswitch.ts +++ b/test-pnpm/uswitch/uswitch.ts @@ -3,7 +3,7 @@ import { commandHandler } from './commandHandler'; import { readPlansFromFile } from './readPlansFromFile'; import readline from 'readline/promises'; -async function main() { +export async function main() { const file = process.argv[2]; if (!file) { console.error('Usage: uswitch plans.json'); @@ -26,7 +26,10 @@ async function main() { // } } -main(); +// Only run main if this file is being run directly +if (require.main === module) { + main(); +} function promptUserForPrices(rl: readline.Interface, plans: EnergyPlan[]): void { rl.prompt(); From c34d8920a5222cfb4e8e4d52b00727dd518f2e0d Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Tue, 3 Jun 2025 10:37:36 +0100 Subject: [PATCH 13/13] Fix badly named variable --- test-pnpm/uswitch/uswitch.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test-pnpm/uswitch/uswitch.ts b/test-pnpm/uswitch/uswitch.ts index f5fab83..a2dac92 100644 --- a/test-pnpm/uswitch/uswitch.ts +++ b/test-pnpm/uswitch/uswitch.ts @@ -10,14 +10,14 @@ export async function main() { process.exit(1); } - const rl = readline.createInterface({ + const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const plans = await readPlansFromFile(file); - promptUserForPrices(rl, plans); + promptUserForPrices(terminal, plans); // Fake it until you make it // const usage = 1000; @@ -31,21 +31,22 @@ if (require.main === module) { main(); } -function promptUserForPrices(rl: readline.Interface, plans: EnergyPlan[]): void { - rl.prompt(); - rl.on('line', (line) => { +function promptUserForPrices(terminal: readline.Interface, plans: EnergyPlan[]): void { + terminal.prompt(); + + terminal.on('line', (line) => { const responses = commandHandler(line, plans); if (!responses) { - rl.close(); + terminal.close(); } else { for (const response of responses) { console.log(response); } - rl.prompt(true); + terminal.prompt(true); } }); - rl.on('close', () => { + terminal.on('close', () => { console.debug('Well done!'); }); }