@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.
Files changed (35) hide show
  1. package/dist/corporate/uk/UKCorporateTaxService.js +3 -0
  2. package/dist/corporate/uk/UKCorporateTaxServiceImpl.js +70 -0
  3. package/dist/corporate/uk/domain/types.js +3 -0
  4. package/dist/income-tax/uk/UKIncomeTaxService.js +3 -0
  5. package/dist/income-tax/uk/UKIncomeTaxServiceImpl.js +96 -0
  6. package/dist/income-tax/uk/domain/types.js +3 -0
  7. package/dist/index.js +9 -2
  8. package/dist/mortgage/uk/UKMortgageService.js +3 -0
  9. package/dist/mortgage/uk/UKMortgageServiceImpl.js +99 -0
  10. package/dist/mortgage/uk/domain/types.js +3 -0
  11. package/dist/types/corporate/uk/UKCorporateTaxService.d.ts +4 -0
  12. package/dist/types/corporate/uk/UKCorporateTaxServiceImpl.d.ts +10 -0
  13. package/dist/types/corporate/uk/domain/types.d.ts +32 -0
  14. package/dist/types/income-tax/uk/UKIncomeTaxService.d.ts +4 -0
  15. package/dist/types/income-tax/uk/UKIncomeTaxServiceImpl.d.ts +12 -0
  16. package/dist/types/income-tax/uk/domain/types.d.ts +27 -0
  17. package/dist/types/index.d.ts +6 -0
  18. package/dist/types/mortgage/uk/UKMortgageService.d.ts +4 -0
  19. package/dist/types/mortgage/uk/UKMortgageServiceImpl.d.ts +9 -0
  20. package/dist/types/mortgage/uk/domain/types.d.ts +49 -0
  21. package/package.json +1 -1
  22. package/src/corporate/uk/UKCorporateTaxService.ts +5 -0
  23. package/src/corporate/uk/UKCorporateTaxServiceImpl.ts +82 -0
  24. package/src/corporate/uk/domain/types.ts +38 -0
  25. package/src/income-tax/uk/UKIncomeTaxService.ts +5 -0
  26. package/src/income-tax/uk/UKIncomeTaxServiceImpl.ts +133 -0
  27. package/src/income-tax/uk/domain/types.ts +31 -0
  28. package/src/index.ts +21 -0
  29. package/src/mortgage/uk/UKMortgageService.ts +5 -0
  30. package/src/mortgage/uk/UKMortgageServiceImpl.ts +140 -0
  31. package/src/mortgage/uk/domain/types.ts +58 -0
  32. package/test/australia-income-tax.test.ts +10 -10
  33. package/test/uk-corporate-tax.test.ts +119 -0
  34. package/test/uk-income-tax.test.ts +146 -0
  35. 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
+ });