Skip to content

Commit 9d1bd5e

Browse files
sharevbjacob-alfordCopilot
committed
feat(new tool): implement amortization-calculator tool (#245)
This PR adds a new tool in the finance category called Amortization Calculator. It's used for calculating the amount of interest and principle that are a part of a payment to an amortized loan, like a car-loan or home-mortgage. --------- Co-authored-by: Jacob Alford <github.scouting378@passmail.net> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 23fbd21 commit 9d1bd5e

File tree

6 files changed

+1088
-0
lines changed

6 files changed

+1088
-0
lines changed

locales/en.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,30 @@ tools:
12101210
placeholder-put-your-full-prompt-here: Put your full prompt here...
12111211
label-character-length-for-each-chunk: Character length for each chunk
12121212
title-divided-prompts: Divided prompts
1213+
amortization-calculator:
1214+
title: Amortization Calculator
1215+
description: Calculate loan amortization schedules with monthly payment breakdowns
1216+
footer: Amortization Calculator
1217+
texts:
1218+
title-loan-parameters: Loan Parameters
1219+
title-overview: Overview
1220+
title-amortization-schedule: Amortization Schedule
1221+
label-loan-amount: Loan Amount
1222+
label-loan-term: Loan Term
1223+
label-interest-rate: Interest Rate
1224+
label-displayed-currency: Displayed Currency
1225+
label-end-of-year: "End of Year {year}"
1226+
label-year: Year
1227+
label-month: Month
1228+
label-interest: Interest
1229+
label-principal: Principal
1230+
label-ending-balance: Ending Balance
1231+
label-monthly-payment: Monthly Payment
1232+
label-total-of-payments: Total of Payments
1233+
label-total-interest: Total Interest
1234+
message-loan-amount-must-be-positive: Loan amount must be a positive value
1235+
message-loan-term-must-be-positive-integer: Loan term must be a positive integer
1236+
message-interest-rate-must-be-between-1-and-100: Interest rate must be a positive value between 0 and 100
12131237
angle-converter:
12141238
title: Angle Units Converter
12151239
description: Convert values between angle units
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.describe('Tool - Amortization Calculator', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/amortization-calculator');
6+
});
7+
8+
test('Has correct title', async ({ page }) => {
9+
await expect(page).toHaveTitle('Amortization Calculator - IT Tools');
10+
});
11+
12+
test('Calculates amortization schedule with default values', async ({ page }) => {
13+
const loanAmountInput = page.getByLabel('Loan Amount');
14+
const loanTermInput = page.getByLabel('Loan Term');
15+
const interestRateInput = page.getByLabel('Interest Rate');
16+
17+
await expect(loanAmountInput).toHaveValue('200000');
18+
await expect(loanTermInput).toHaveValue('360');
19+
await expect(interestRateInput).toHaveValue('6');
20+
21+
const monthlyPaymentCell = page.locator('tbody tr td').first();
22+
await expect(monthlyPaymentCell).toContainText('$1,199.10');
23+
24+
const totalPaymentsCell = page.locator('tbody tr td').nth(1);
25+
await expect(totalPaymentsCell).toContainText('$431,676.38');
26+
});
27+
28+
test('Updates calculation when loan amount changes', async ({ page }) => {
29+
const loanAmountInput = page.getByLabel('Loan Amount');
30+
31+
await loanAmountInput.clear();
32+
await loanAmountInput.fill('100000');
33+
34+
const monthlyPaymentCell = page.locator('tbody tr td').first();
35+
await expect(monthlyPaymentCell).toContainText('$599.55');
36+
37+
const totalPaymentsCell = page.locator('tbody tr td').nth(1);
38+
await expect(totalPaymentsCell).toContainText('$215,838.19');
39+
});
40+
41+
test('Updates calculation when loan term changes', async ({ page }) => {
42+
const loanTermInput = page.getByLabel('Loan Term');
43+
44+
await loanTermInput.clear();
45+
await loanTermInput.fill('180');
46+
47+
const monthlyPaymentCell = page.locator('tbody tr td').first();
48+
await expect(monthlyPaymentCell).toContainText('$1,687.71');
49+
});
50+
51+
test('Updates calculation when interest rate changes', async ({ page }) => {
52+
const interestRateInput = page.getByLabel('Interest Rate');
53+
54+
await interestRateInput.clear();
55+
await interestRateInput.fill('4.5');
56+
57+
const monthlyPaymentCell = page.locator('tbody tr td').first();
58+
await expect(monthlyPaymentCell).toContainText('$1,013.37');
59+
});
60+
61+
test('Displays amortization schedule table', async ({ page }) => {
62+
const scheduleTable = page.locator('table').nth(1);
63+
64+
await expect(scheduleTable.locator('thead th').nth(0)).toContainText('Month');
65+
await expect(scheduleTable.locator('thead th').nth(1)).toContainText('Interest');
66+
await expect(scheduleTable.locator('thead th').nth(2)).toContainText('Principal');
67+
await expect(scheduleTable.locator('thead th').nth(3)).toContainText('Ending Balance');
68+
69+
const firstMonthRow = scheduleTable.locator('tbody tr').first();
70+
await expect(firstMonthRow.locator('td').nth(0)).toContainText('1');
71+
await expect(firstMonthRow.locator('td').nth(1)).toContainText('$1,000.00');
72+
await expect(firstMonthRow.locator('td').nth(2)).toContainText('$199.10');
73+
await expect(firstMonthRow.locator('td').nth(3)).toContainText('$199,800.90');
74+
});
75+
76+
test('Shows validation error for invalid loan amount', async ({ page }) => {
77+
const loanAmountInput = page.getByLabel('Loan Amount');
78+
79+
await loanAmountInput.clear();
80+
await loanAmountInput.fill('-1000');
81+
await loanAmountInput.blur();
82+
83+
await expect(page.locator('text=Loan amount must be a positive value')).toBeVisible();
84+
});
85+
86+
test('Shows validation error for zero loan amount', async ({ page }) => {
87+
const loanAmountInput = page.getByLabel('Loan Amount');
88+
89+
await loanAmountInput.clear();
90+
await loanAmountInput.fill('0');
91+
await loanAmountInput.blur();
92+
93+
await expect(page.locator('text=Loan amount must be a positive value')).toBeVisible();
94+
});
95+
96+
test('Shows validation error for non-integer loan term', async ({ page }) => {
97+
const loanTermInput = page.getByLabel('Loan Term');
98+
99+
await loanTermInput.clear();
100+
await loanTermInput.fill('12.5');
101+
await loanTermInput.blur();
102+
103+
await expect(page.locator('text=Loan term must be a positive integer')).toBeVisible();
104+
});
105+
106+
test('Shows validation error for negative loan term', async ({ page }) => {
107+
const loanTermInput = page.getByLabel('Loan Term');
108+
109+
await loanTermInput.clear();
110+
await loanTermInput.fill('-12');
111+
await loanTermInput.blur();
112+
113+
await expect(page.locator('text=Loan term must be a positive integer')).toBeVisible();
114+
});
115+
116+
test('Shows validation error for interest rate above 100', async ({ page }) => {
117+
const interestRateInput = page.getByLabel('Interest Rate');
118+
119+
await interestRateInput.clear();
120+
await interestRateInput.fill('101');
121+
await interestRateInput.blur();
122+
123+
await expect(page.locator('text=Interest rate must be a positive value between 0 and 100')).toBeVisible();
124+
});
125+
126+
test('Shows validation error for zero interest rate', async ({ page }) => {
127+
const interestRateInput = page.getByLabel('Interest Rate');
128+
129+
await interestRateInput.clear();
130+
await interestRateInput.fill('0');
131+
await interestRateInput.blur();
132+
133+
await expect(page.locator('text=Interest rate must be a positive value between 0 and 100')).toBeVisible();
134+
});
135+
136+
test('Hides results when inputs are invalid', async ({ page }) => {
137+
const loanAmountInput = page.getByLabel('Loan Amount');
138+
139+
await loanAmountInput.clear();
140+
await loanAmountInput.fill('invalid');
141+
142+
const resultsTable = page.locator('table').first();
143+
await expect(resultsTable).not.toBeVisible();
144+
});
145+
146+
test('Calculates correctly for short-term loan', async ({ page }) => {
147+
const loanAmountInput = page.getByLabel('Loan Amount');
148+
const loanTermInput = page.getByLabel('Loan Term');
149+
const interestRateInput = page.getByLabel('Interest Rate');
150+
151+
await loanAmountInput.clear();
152+
await loanAmountInput.fill('10000');
153+
await loanTermInput.clear();
154+
await loanTermInput.fill('12');
155+
await interestRateInput.clear();
156+
await interestRateInput.fill('12');
157+
158+
const monthlyPaymentCell = page.locator('tbody tr td').first();
159+
await expect(monthlyPaymentCell).toContainText('$888.49');
160+
161+
const scheduleTable = page.locator('table').nth(1);
162+
// Find the last month row (before the year summary row)
163+
const lastMonthRow = scheduleTable.locator('tbody tr').nth(11); // 12th month (0-indexed)
164+
await expect(lastMonthRow.locator('td').nth(0)).toContainText('12');
165+
await expect(lastMonthRow.locator('td').nth(3)).toContainText('$0.00');
166+
});
167+
168+
test('Schedule shows decreasing balance over time', async ({ page }) => {
169+
const loanAmountInput = page.getByLabel('Loan Amount');
170+
const loanTermInput = page.getByLabel('Loan Term');
171+
172+
await loanAmountInput.clear();
173+
await loanAmountInput.fill('50000');
174+
await loanTermInput.clear();
175+
await loanTermInput.fill('12');
176+
177+
const scheduleTable = page.locator('table').nth(1);
178+
const firstBalance = await scheduleTable.locator('tbody tr').nth(0).locator('td').nth(3).textContent();
179+
const sixthBalance = await scheduleTable.locator('tbody tr').nth(5).locator('td').nth(3).textContent();
180+
// Get the last month row (12th month, index 11) before the year summary row
181+
const lastMonthBalance = await scheduleTable.locator('tbody tr').nth(11).locator('td').nth(3).textContent();
182+
183+
// The values may contain country code in e2e tests
184+
const firstBalanceValue = Number.parseFloat(firstBalance!.replace(/[A-Za-z$,]/g, ''));
185+
const sixthBalanceValue = Number.parseFloat(sixthBalance!.replace(/[A-Za-z$,]/g, ''));
186+
const lastBalanceValue = Number.parseFloat(lastMonthBalance!.replace(/[A-Za-z$,]/g, ''));
187+
188+
expect(firstBalanceValue).toBeGreaterThan(sixthBalanceValue);
189+
expect(sixthBalanceValue).toBeGreaterThan(lastBalanceValue);
190+
expect(lastBalanceValue).toBeCloseTo(0, 0);
191+
});
192+
});

0 commit comments

Comments
 (0)