@novha/calc-engines 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/corporate/uk/UKCorporateTaxService.js +3 -0
- package/dist/corporate/uk/UKCorporateTaxServiceImpl.js +70 -0
- package/dist/corporate/uk/domain/types.js +3 -0
- package/dist/income-tax/uk/UKIncomeTaxService.js +3 -0
- package/dist/income-tax/uk/UKIncomeTaxServiceImpl.js +96 -0
- package/dist/income-tax/uk/domain/types.js +3 -0
- package/dist/index.js +9 -2
- package/dist/mortgage/uk/UKMortgageService.js +3 -0
- package/dist/mortgage/uk/UKMortgageServiceImpl.js +99 -0
- package/dist/mortgage/uk/domain/types.js +3 -0
- package/dist/types/corporate/uk/UKCorporateTaxService.d.ts +4 -0
- package/dist/types/corporate/uk/UKCorporateTaxServiceImpl.d.ts +10 -0
- package/dist/types/corporate/uk/domain/types.d.ts +32 -0
- package/dist/types/income-tax/uk/UKIncomeTaxService.d.ts +4 -0
- package/dist/types/income-tax/uk/UKIncomeTaxServiceImpl.d.ts +12 -0
- package/dist/types/income-tax/uk/domain/types.d.ts +27 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mortgage/uk/UKMortgageService.d.ts +4 -0
- package/dist/types/mortgage/uk/UKMortgageServiceImpl.d.ts +9 -0
- package/dist/types/mortgage/uk/domain/types.d.ts +49 -0
- package/package.json +1 -1
- package/src/corporate/uk/UKCorporateTaxService.ts +5 -0
- package/src/corporate/uk/UKCorporateTaxServiceImpl.ts +82 -0
- package/src/corporate/uk/domain/types.ts +38 -0
- package/src/income-tax/uk/UKIncomeTaxService.ts +5 -0
- package/src/income-tax/uk/UKIncomeTaxServiceImpl.ts +133 -0
- package/src/income-tax/uk/domain/types.ts +31 -0
- package/src/index.ts +21 -0
- package/src/mortgage/uk/UKMortgageService.ts +5 -0
- package/src/mortgage/uk/UKMortgageServiceImpl.ts +140 -0
- package/src/mortgage/uk/domain/types.ts +58 -0
- package/test/australia-income-tax.test.ts +10 -10
- package/test/uk-corporate-tax.test.ts +119 -0
- package/test/uk-income-tax.test.ts +146 -0
- package/test/uk-mortgage.test.ts +154 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { UKCorporateTaxServiceImpl } from '../src/corporate/uk/UKCorporateTaxServiceImpl';
|
|
2
|
+
import { Input, Rules } from '../src/corporate/uk/domain/types';
|
|
3
|
+
|
|
4
|
+
// UK 2024-25 corporate tax rules
|
|
5
|
+
const ukCorporateRules: Rules = {
|
|
6
|
+
regimes: {
|
|
7
|
+
smallProfits: {
|
|
8
|
+
type: 'flat',
|
|
9
|
+
rate: 0.19,
|
|
10
|
+
},
|
|
11
|
+
main: {
|
|
12
|
+
type: 'flat',
|
|
13
|
+
rate: 0.25,
|
|
14
|
+
},
|
|
15
|
+
marginalRelief: {
|
|
16
|
+
type: 'marginal_relief',
|
|
17
|
+
mainRate: 0.25,
|
|
18
|
+
smallProfitsRate: 0.19,
|
|
19
|
+
upperLimit: 250000,
|
|
20
|
+
lowerLimit: 50000,
|
|
21
|
+
standardFraction: 3 / 200,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('UKCorporateTaxServiceImpl', () => {
|
|
27
|
+
it('applies small profits rate (19%) for profits up to £50,000', () => {
|
|
28
|
+
const input: Input = { taxableIncome: 50000 };
|
|
29
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
30
|
+
const result = service.calculate();
|
|
31
|
+
|
|
32
|
+
expect(result.corporateTax).toBe(9500);
|
|
33
|
+
expect(result.effectiveTaxRate).toBe(19);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('applies main rate (25%) for profits above £250,000', () => {
|
|
37
|
+
const input: Input = { taxableIncome: 300000 };
|
|
38
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
39
|
+
const result = service.calculate();
|
|
40
|
+
|
|
41
|
+
expect(result.corporateTax).toBe(75000);
|
|
42
|
+
expect(result.effectiveTaxRate).toBe(25);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('applies marginal relief for profits between £50,000 and £250,000', () => {
|
|
46
|
+
// At £150,000:
|
|
47
|
+
// Gross tax = 150000 * 25% = 37500
|
|
48
|
+
// Relief = (3/200) * (250000 - 150000) = 0.015 * 100000 = 1500
|
|
49
|
+
// Net tax = 37500 - 1500 = 36000
|
|
50
|
+
const input: Input = { taxableIncome: 150000 };
|
|
51
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
52
|
+
const result = service.calculate();
|
|
53
|
+
|
|
54
|
+
expect(result.corporateTax).toBe(36000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('marginal relief at lower limit boundary produces 19% effective rate', () => {
|
|
58
|
+
// At £50,001 (just above lower limit):
|
|
59
|
+
// Gross tax = 50001 * 25% = 12500.25
|
|
60
|
+
// Relief = (3/200) * (250000 - 50001) = 0.015 * 199999 = 2999.985
|
|
61
|
+
// Net tax ≈ 9500.265
|
|
62
|
+
// Effective rate ≈ 19.00%
|
|
63
|
+
const input: Input = { taxableIncome: 50001 };
|
|
64
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
65
|
+
const result = service.calculate();
|
|
66
|
+
|
|
67
|
+
expect(result.effectiveTaxRate).toBeCloseTo(19, 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('marginal relief at upper limit boundary produces 25% effective rate', () => {
|
|
71
|
+
// At £249,999:
|
|
72
|
+
// Gross tax = 249999 * 25% = 62499.75
|
|
73
|
+
// Relief = (3/200) * (250000 - 249999) = 0.015 * 1 = 0.015
|
|
74
|
+
// Net tax ≈ 62499.735
|
|
75
|
+
// Effective rate ≈ 25%
|
|
76
|
+
const input: Input = { taxableIncome: 249999 };
|
|
77
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
78
|
+
const result = service.calculate();
|
|
79
|
+
|
|
80
|
+
expect(result.effectiveTaxRate).toBeCloseTo(25, 0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns zero tax for zero taxable income', () => {
|
|
84
|
+
const input: Input = { taxableIncome: 0 };
|
|
85
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
86
|
+
const result = service.calculate();
|
|
87
|
+
|
|
88
|
+
expect(result.corporateTax).toBe(0);
|
|
89
|
+
expect(result.effectiveTaxRate).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns breakdowns for small profits rate', () => {
|
|
93
|
+
const input: Input = { taxableIncome: 30000 };
|
|
94
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
95
|
+
const result = service.calculate();
|
|
96
|
+
|
|
97
|
+
expect(result.breakdowns).toHaveLength(1);
|
|
98
|
+
expect(result.breakdowns[0].rate).toBe(0.19);
|
|
99
|
+
expect(result.breakdowns[0].amount).toBe(5700);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns breakdowns for main rate', () => {
|
|
103
|
+
const input: Input = { taxableIncome: 500000 };
|
|
104
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
105
|
+
const result = service.calculate();
|
|
106
|
+
|
|
107
|
+
expect(result.breakdowns).toHaveLength(1);
|
|
108
|
+
expect(result.breakdowns[0].rate).toBe(0.25);
|
|
109
|
+
expect(result.breakdowns[0].amount).toBe(125000);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns two breakdowns for marginal relief (main tax + relief)', () => {
|
|
113
|
+
const input: Input = { taxableIncome: 100000 };
|
|
114
|
+
const service = new UKCorporateTaxServiceImpl(input, ukCorporateRules);
|
|
115
|
+
const result = service.calculate();
|
|
116
|
+
|
|
117
|
+
expect(result.breakdowns).toHaveLength(2);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { UKIncomeTaxServiceImpl } from '../src/income-tax/uk/UKIncomeTaxServiceImpl';
|
|
2
|
+
import { IncomeTaxRules } from '../src/income-tax/uk/domain/types';
|
|
3
|
+
|
|
4
|
+
// UK 2024-25 income tax rules
|
|
5
|
+
// Tax brackets are in terms of taxable income (after personal allowance deduction)
|
|
6
|
+
const ukRules: IncomeTaxRules = {
|
|
7
|
+
taxBrackets: [
|
|
8
|
+
{ from: 0, to: 37700, rate: 0.20 }, // Basic rate
|
|
9
|
+
{ from: 37700, to: 125140, rate: 0.40 }, // Higher rate
|
|
10
|
+
{ from: 125140, to: null, rate: 0.45 }, // Additional rate
|
|
11
|
+
],
|
|
12
|
+
personalAllowance: {
|
|
13
|
+
amount: 12570,
|
|
14
|
+
taperThreshold: 100000,
|
|
15
|
+
taperRate: 0.5,
|
|
16
|
+
},
|
|
17
|
+
nationalInsurance: {
|
|
18
|
+
primaryThreshold: 12570,
|
|
19
|
+
upperEarningsLimit: 50270,
|
|
20
|
+
mainRate: 0.08,
|
|
21
|
+
upperRate: 0.02,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('UKIncomeTaxServiceImpl', () => {
|
|
26
|
+
it('returns zero tax for income at or below the personal allowance', () => {
|
|
27
|
+
const service = new UKIncomeTaxServiceImpl(12570, ukRules);
|
|
28
|
+
const result = service.calculateNetIncome();
|
|
29
|
+
|
|
30
|
+
expect(result.incomeTax).toBe(0);
|
|
31
|
+
expect(result.grossIncome).toBe(12570);
|
|
32
|
+
expect(result.netIncome).toBe(result.grossIncome - result.totalDeductions);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('correctly calculates income tax for £30,000 income', () => {
|
|
36
|
+
const service = new UKIncomeTaxServiceImpl(30000, ukRules);
|
|
37
|
+
const result = service.calculateNetIncome();
|
|
38
|
+
|
|
39
|
+
// Personal allowance = £12,570
|
|
40
|
+
// Taxable income = 30000 - 12570 = 17430
|
|
41
|
+
// Tax = 17430 * 20% = 3486
|
|
42
|
+
expect(result.incomeTax).toBe(3486);
|
|
43
|
+
expect(result.personalAllowance).toBe(12570);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('correctly calculates income tax for £50,000 income (basic rate only)', () => {
|
|
47
|
+
const service = new UKIncomeTaxServiceImpl(50000, ukRules);
|
|
48
|
+
const result = service.calculateNetIncome();
|
|
49
|
+
|
|
50
|
+
// Taxable income = 50000 - 12570 = 37430
|
|
51
|
+
// Basic rate band: 37430 * 20% = 7486
|
|
52
|
+
expect(result.incomeTax).toBe(7486);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('correctly calculates income tax for £60,000 income (higher rate applies)', () => {
|
|
56
|
+
const service = new UKIncomeTaxServiceImpl(60000, ukRules);
|
|
57
|
+
const result = service.calculateNetIncome();
|
|
58
|
+
|
|
59
|
+
// Taxable income = 60000 - 12570 = 47430
|
|
60
|
+
// Basic rate: 37700 * 20% = 7540
|
|
61
|
+
// Higher rate: (47430 - 37700) * 40% = 9730 * 0.40 = 3892
|
|
62
|
+
// Total = 11432
|
|
63
|
+
expect(result.incomeTax).toBe(11432);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('tapers personal allowance for income above £100,000', () => {
|
|
67
|
+
const service = new UKIncomeTaxServiceImpl(110000, ukRules);
|
|
68
|
+
const result = service.calculateNetIncome();
|
|
69
|
+
|
|
70
|
+
// Personal allowance = 12570 - (110000 - 100000) * 0.5 = 12570 - 5000 = 7570
|
|
71
|
+
expect(result.personalAllowance).toBe(7570);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('removes personal allowance entirely for income above £125,140', () => {
|
|
75
|
+
const service = new UKIncomeTaxServiceImpl(130000, ukRules);
|
|
76
|
+
const result = service.calculateNetIncome();
|
|
77
|
+
|
|
78
|
+
// Personal allowance = max(0, 12570 - (130000 - 100000) * 0.5) = max(0, 12570 - 15000) = 0
|
|
79
|
+
expect(result.personalAllowance).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('applies additional rate (45%) for income above £125,140 (after PA deducted)', () => {
|
|
83
|
+
const service = new UKIncomeTaxServiceImpl(150000, ukRules);
|
|
84
|
+
const result = service.calculateNetIncome();
|
|
85
|
+
|
|
86
|
+
// No personal allowance (income > £125,140)
|
|
87
|
+
// Taxable income = 150000
|
|
88
|
+
// Basic rate: 37700 * 20% = 7540
|
|
89
|
+
// Higher rate: (125140 - 37700) * 40% = 87440 * 40% = 34976
|
|
90
|
+
// Additional rate: (150000 - 125140) * 45% = 24860 * 45% = 11187
|
|
91
|
+
// Total = 7540 + 34976 + 11187 = 53703
|
|
92
|
+
expect(result.incomeTax).toBe(53703);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('calculates National Insurance correctly for basic rate taxpayer', () => {
|
|
96
|
+
const service = new UKIncomeTaxServiceImpl(30000, ukRules);
|
|
97
|
+
const result = service.calculateNetIncome();
|
|
98
|
+
|
|
99
|
+
// NI: (30000 - 12570) * 8% = 17430 * 0.08 = 1394.40
|
|
100
|
+
expect(result.nationalInsurance).toBeCloseTo(1394.40, 2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('calculates National Insurance correctly above upper earnings limit', () => {
|
|
104
|
+
const service = new UKIncomeTaxServiceImpl(60000, ukRules);
|
|
105
|
+
const result = service.calculateNetIncome();
|
|
106
|
+
|
|
107
|
+
// NI main band: (50270 - 12570) * 8% = 37700 * 0.08 = 3016
|
|
108
|
+
// NI upper band: (60000 - 50270) * 2% = 9730 * 0.02 = 194.60
|
|
109
|
+
// Total NI = 3210.60
|
|
110
|
+
expect(result.nationalInsurance).toBeCloseTo(3210.60, 2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns zero National Insurance for income at or below primary threshold', () => {
|
|
114
|
+
const service = new UKIncomeTaxServiceImpl(12570, ukRules);
|
|
115
|
+
const result = service.calculateNetIncome();
|
|
116
|
+
|
|
117
|
+
expect(result.nationalInsurance).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('net income equals gross income minus total deductions', () => {
|
|
121
|
+
const service = new UKIncomeTaxServiceImpl(50000, ukRules);
|
|
122
|
+
const result = service.calculateNetIncome();
|
|
123
|
+
|
|
124
|
+
expect(result.netIncome).toBeCloseTo(
|
|
125
|
+
result.grossIncome - result.totalDeductions,
|
|
126
|
+
2,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('effective tax rate is income tax divided by gross income', () => {
|
|
131
|
+
const service = new UKIncomeTaxServiceImpl(80000, ukRules);
|
|
132
|
+
const result = service.calculateNetIncome();
|
|
133
|
+
|
|
134
|
+
expect(result.effectiveTaxRate).toBeCloseTo(result.incomeTax / result.grossIncome, 4);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('total deductions equals income tax plus National Insurance', () => {
|
|
138
|
+
const service = new UKIncomeTaxServiceImpl(50000, ukRules);
|
|
139
|
+
const result = service.calculateNetIncome();
|
|
140
|
+
|
|
141
|
+
expect(result.totalDeductions).toBeCloseTo(
|
|
142
|
+
result.incomeTax + result.nationalInsurance,
|
|
143
|
+
2,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { UKMortgageServiceImpl } from '../src/mortgage/uk/UKMortgageServiceImpl';
|
|
2
|
+
import { MortgageInput, MortgageRules } from '../src/mortgage/uk/domain/types';
|
|
3
|
+
|
|
4
|
+
// UK 2024-25 mortgage rules
|
|
5
|
+
const ukMortgageRules: MortgageRules = {
|
|
6
|
+
loanConstraints: {
|
|
7
|
+
maxLtvPercent: 95,
|
|
8
|
+
maxAmortizationYears: 35,
|
|
9
|
+
},
|
|
10
|
+
interest: {
|
|
11
|
+
compounding: 'MONTHLY',
|
|
12
|
+
},
|
|
13
|
+
stampDuty: {
|
|
14
|
+
standardBrackets: [
|
|
15
|
+
{ upTo: 250000, rate: 0 },
|
|
16
|
+
{ upTo: 925000, rate: 0.05 },
|
|
17
|
+
{ upTo: 1500000, rate: 0.10 },
|
|
18
|
+
{ above: 1500000, rate: 0.12 },
|
|
19
|
+
],
|
|
20
|
+
firstTimeBuyer: {
|
|
21
|
+
brackets: [
|
|
22
|
+
{ upTo: 425000, rate: 0 },
|
|
23
|
+
{ upTo: 625000, rate: 0.05 },
|
|
24
|
+
],
|
|
25
|
+
maxEligiblePropertyPrice: 625000,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defaultInput: MortgageInput = {
|
|
31
|
+
propertyPrice: 400000,
|
|
32
|
+
downPayment: 80000, // 20% down
|
|
33
|
+
annualInterestRate: 5.0,
|
|
34
|
+
amortizationYears: 25,
|
|
35
|
+
isFirstTimeBuyer: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('UKMortgageServiceImpl', () => {
|
|
39
|
+
const service = new UKMortgageServiceImpl();
|
|
40
|
+
|
|
41
|
+
it('calculates loan amount correctly', () => {
|
|
42
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
43
|
+
|
|
44
|
+
expect(result.loanAmount).toBe(320000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('calculates monthly payment using standard amortization formula', () => {
|
|
48
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
49
|
+
|
|
50
|
+
const r = 0.05 / 12;
|
|
51
|
+
const n = 25 * 12;
|
|
52
|
+
const expected = 320000 * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
|
|
53
|
+
expect(result.monthlyPayment).toBeCloseTo(expected, 2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('totalPaid equals monthlyPayment * totalPayments', () => {
|
|
57
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
58
|
+
const totalPayments = 25 * 12;
|
|
59
|
+
|
|
60
|
+
expect(result.totalPaid).toBeCloseTo(result.monthlyPayment * totalPayments, 0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('calculates standard SDLT at 0% for property at or below £250,000', () => {
|
|
64
|
+
const input: MortgageInput = {
|
|
65
|
+
...defaultInput,
|
|
66
|
+
propertyPrice: 200000,
|
|
67
|
+
downPayment: 40000,
|
|
68
|
+
};
|
|
69
|
+
const result = service.calculate(input, ukMortgageRules);
|
|
70
|
+
|
|
71
|
+
expect(result.stampDuty).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('calculates standard SDLT for property above £250,000', () => {
|
|
75
|
+
// 400000: 0% on 250000 = 0, 5% on (400000 - 250000) = 7500
|
|
76
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
77
|
+
|
|
78
|
+
expect(result.stampDuty).toBe(7500);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('calculates standard SDLT for property above £925,000', () => {
|
|
82
|
+
// 1000000: 0% on 250000 + 5% on 675000 + 10% on 75000
|
|
83
|
+
// = 0 + 33750 + 7500 = 41250
|
|
84
|
+
const input: MortgageInput = {
|
|
85
|
+
...defaultInput,
|
|
86
|
+
propertyPrice: 1000000,
|
|
87
|
+
downPayment: 200000,
|
|
88
|
+
};
|
|
89
|
+
const result = service.calculate(input, ukMortgageRules);
|
|
90
|
+
|
|
91
|
+
expect(result.stampDuty).toBe(41250);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('applies first-time buyer SDLT relief for eligible property', () => {
|
|
95
|
+
// First-time buyer, property = 500000
|
|
96
|
+
// 0% on first 425000 = 0, 5% on (500000 - 425000) = 3750
|
|
97
|
+
const input: MortgageInput = {
|
|
98
|
+
...defaultInput,
|
|
99
|
+
propertyPrice: 500000,
|
|
100
|
+
downPayment: 100000,
|
|
101
|
+
isFirstTimeBuyer: true,
|
|
102
|
+
};
|
|
103
|
+
const result = service.calculate(input, ukMortgageRules);
|
|
104
|
+
|
|
105
|
+
expect(result.stampDuty).toBe(3750);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('applies standard SDLT for first-time buyer on property above £625,000', () => {
|
|
109
|
+
// First-time buyer but property > 625000 → standard rates apply
|
|
110
|
+
const input: MortgageInput = {
|
|
111
|
+
...defaultInput,
|
|
112
|
+
propertyPrice: 700000,
|
|
113
|
+
downPayment: 140000,
|
|
114
|
+
isFirstTimeBuyer: true,
|
|
115
|
+
};
|
|
116
|
+
const result = service.calculate(input, ukMortgageRules);
|
|
117
|
+
|
|
118
|
+
// Standard: 0% on 250000 = 0, 5% on 450000 = 22500
|
|
119
|
+
expect(result.stampDuty).toBe(22500);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('throws error when loan amount is zero or negative', () => {
|
|
123
|
+
const input: MortgageInput = {
|
|
124
|
+
...defaultInput,
|
|
125
|
+
downPayment: 400000,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
expect(() => service.calculate(input, ukMortgageRules)).toThrow('Invalid loan amount');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('generates amortization schedule with correct length', () => {
|
|
132
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
133
|
+
|
|
134
|
+
expect(result.amortizationSchedule.length).toBe(25);
|
|
135
|
+
expect(result.amortizationSchedule[0].year).toBe(1);
|
|
136
|
+
expect(result.amortizationSchedule[24].balance).toBeCloseTo(0, 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('amortization schedule balance decreases over time', () => {
|
|
140
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
141
|
+
const schedule = result.amortizationSchedule;
|
|
142
|
+
|
|
143
|
+
for (let i = 1; i < schedule.length; i++) {
|
|
144
|
+
expect(schedule[i].balance).toBeLessThan(schedule[i - 1].balance);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('otherFees includes stamp duty label', () => {
|
|
149
|
+
const result = service.calculate(defaultInput, ukMortgageRules);
|
|
150
|
+
|
|
151
|
+
expect(result.otherFees.notaryFees.label).toBe('STAMP_DUTY');
|
|
152
|
+
expect(result.otherFees.notaryFees.value).toBe(result.stampDuty);
|
|
153
|
+
});
|
|
154
|
+
});
|