@novha/calc-engines 4.0.0 → 5.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 (80) hide show
  1. package/dist/capital-gains/australia/AustraliaCapitalGainsService.js +3 -0
  2. package/dist/capital-gains/australia/AustraliaCapitalGainsServiceImpl.js +56 -0
  3. package/dist/capital-gains/australia/domain/types.js +3 -0
  4. package/dist/capital-gains/canada/CanadaCapitalGainsService.js +3 -0
  5. package/dist/capital-gains/canada/CanadaCapitalGainsServiceImpl.js +55 -0
  6. package/dist/capital-gains/canada/domain/types.js +3 -0
  7. package/dist/capital-gains/domain/types.js +3 -0
  8. package/dist/capital-gains/france/FranceCapitalGainsService.js +3 -0
  9. package/dist/capital-gains/france/FranceCapitalGainsServiceImpl.js +41 -0
  10. package/dist/capital-gains/france/domain/types.js +3 -0
  11. package/dist/capital-gains/germany/GermanyCapitalGainsService.js +3 -0
  12. package/dist/capital-gains/germany/GermanyCapitalGainsServiceImpl.js +65 -0
  13. package/dist/capital-gains/germany/domain/types.js +3 -0
  14. package/dist/capital-gains/south-africa/SouthAfricaCapitalGainsService.js +3 -0
  15. package/dist/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.js +69 -0
  16. package/dist/capital-gains/south-africa/domain/types.js +3 -0
  17. package/dist/capital-gains/uk/UKCapitalGainsService.js +3 -0
  18. package/dist/capital-gains/uk/UKCapitalGainsServiceImpl.js +72 -0
  19. package/dist/capital-gains/uk/domain/types.js +3 -0
  20. package/dist/capital-gains/usa/USACapitalGainsService.js +3 -0
  21. package/dist/capital-gains/usa/USACapitalGainsServiceImpl.js +67 -0
  22. package/dist/capital-gains/usa/domain/types.js +3 -0
  23. package/dist/index.js +23 -2
  24. package/dist/shared/domain/types.js +2 -1
  25. package/dist/types/capital-gains/australia/AustraliaCapitalGainsService.d.ts +4 -0
  26. package/dist/types/capital-gains/australia/AustraliaCapitalGainsServiceImpl.d.ts +9 -0
  27. package/dist/types/capital-gains/australia/domain/types.d.ts +22 -0
  28. package/dist/types/capital-gains/canada/CanadaCapitalGainsService.d.ts +4 -0
  29. package/dist/types/capital-gains/canada/CanadaCapitalGainsServiceImpl.d.ts +9 -0
  30. package/dist/types/capital-gains/canada/domain/types.d.ts +20 -0
  31. package/dist/types/capital-gains/domain/types.d.ts +6 -0
  32. package/dist/types/capital-gains/france/FranceCapitalGainsService.d.ts +4 -0
  33. package/dist/types/capital-gains/france/FranceCapitalGainsServiceImpl.d.ts +8 -0
  34. package/dist/types/capital-gains/france/domain/types.d.ts +15 -0
  35. package/dist/types/capital-gains/germany/GermanyCapitalGainsService.d.ts +4 -0
  36. package/dist/types/capital-gains/germany/GermanyCapitalGainsServiceImpl.d.ts +8 -0
  37. package/dist/types/capital-gains/germany/domain/types.d.ts +17 -0
  38. package/dist/types/capital-gains/south-africa/SouthAfricaCapitalGainsService.d.ts +4 -0
  39. package/dist/types/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.d.ts +9 -0
  40. package/dist/types/capital-gains/south-africa/domain/types.d.ts +21 -0
  41. package/dist/types/capital-gains/uk/UKCapitalGainsService.d.ts +4 -0
  42. package/dist/types/capital-gains/uk/UKCapitalGainsServiceImpl.d.ts +8 -0
  43. package/dist/types/capital-gains/uk/domain/types.d.ts +17 -0
  44. package/dist/types/capital-gains/usa/USACapitalGainsService.d.ts +4 -0
  45. package/dist/types/capital-gains/usa/USACapitalGainsServiceImpl.d.ts +9 -0
  46. package/dist/types/capital-gains/usa/domain/types.d.ts +26 -0
  47. package/dist/types/index.d.ts +14 -0
  48. package/dist/types/shared/domain/types.d.ts +2 -1
  49. package/package.json +1 -1
  50. package/src/capital-gains/australia/AustraliaCapitalGainsService.ts +5 -0
  51. package/src/capital-gains/australia/AustraliaCapitalGainsServiceImpl.ts +73 -0
  52. package/src/capital-gains/australia/domain/types.ts +26 -0
  53. package/src/capital-gains/canada/CanadaCapitalGainsService.ts +5 -0
  54. package/src/capital-gains/canada/CanadaCapitalGainsServiceImpl.ts +71 -0
  55. package/src/capital-gains/canada/domain/types.ts +24 -0
  56. package/src/capital-gains/domain/types.ts +6 -0
  57. package/src/capital-gains/france/FranceCapitalGainsService.ts +5 -0
  58. package/src/capital-gains/france/FranceCapitalGainsServiceImpl.ts +48 -0
  59. package/src/capital-gains/france/domain/types.ts +18 -0
  60. package/src/capital-gains/germany/GermanyCapitalGainsService.ts +5 -0
  61. package/src/capital-gains/germany/GermanyCapitalGainsServiceImpl.ts +74 -0
  62. package/src/capital-gains/germany/domain/types.ts +20 -0
  63. package/src/capital-gains/south-africa/SouthAfricaCapitalGainsService.ts +5 -0
  64. package/src/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.ts +87 -0
  65. package/src/capital-gains/south-africa/domain/types.ts +25 -0
  66. package/src/capital-gains/uk/UKCapitalGainsService.ts +5 -0
  67. package/src/capital-gains/uk/UKCapitalGainsServiceImpl.ts +84 -0
  68. package/src/capital-gains/uk/domain/types.ts +20 -0
  69. package/src/capital-gains/usa/USACapitalGainsService.ts +5 -0
  70. package/src/capital-gains/usa/USACapitalGainsServiceImpl.ts +86 -0
  71. package/src/capital-gains/usa/domain/types.ts +27 -0
  72. package/src/index.ts +56 -0
  73. package/src/shared/domain/types.ts +1 -0
  74. package/test/australia-capital-gains.test.ts +80 -0
  75. package/test/canada-capital-gains.test.ts +71 -0
  76. package/test/france-capital-gains.test.ts +66 -0
  77. package/test/germany-capital-gains.test.ts +85 -0
  78. package/test/south-africa-capital-gains.test.ts +84 -0
  79. package/test/uk-capital-gains.test.ts +79 -0
  80. package/test/usa-capital-gains.test.ts +88 -0
@@ -0,0 +1,71 @@
1
+ import { CanadaCapitalGainsServiceImpl } from '../src/capital-gains/canada/CanadaCapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/canada/domain/types';
3
+
4
+ // Canada 2024 federal tax brackets with 50% inclusion rate
5
+ const canadaCapitalGainsRules: Rules = {
6
+ inclusionRate: 0.50,
7
+ taxBrackets: [
8
+ { from: 0, to: 55867, rate: 0.15 },
9
+ { from: 55867, to: 111733, rate: 0.205 },
10
+ { from: 111733, to: 154906, rate: 0.26 },
11
+ { from: 154906, to: 220000, rate: 0.29 },
12
+ { from: 220000, to: null, rate: 0.33 },
13
+ ],
14
+ };
15
+
16
+ describe('CanadaCapitalGainsServiceImpl', () => {
17
+ it('applies 50% inclusion rate', () => {
18
+ const input: Input = { capitalGain: 100000, totalTaxableIncome: 100000 };
19
+ const service = new CanadaCapitalGainsServiceImpl(input, canadaCapitalGainsRules);
20
+ const result = service.calculate();
21
+
22
+ expect(result.taxableGain).toBe(50000);
23
+ });
24
+
25
+ it('taxes included gain at marginal rate for low income', () => {
26
+ const input: Input = { capitalGain: 40000, totalTaxableIncome: 40000 };
27
+ const service = new CanadaCapitalGainsServiceImpl(input, canadaCapitalGainsRules);
28
+ const result = service.calculate();
29
+
30
+ // Taxable gain = 20000, all in first bracket (15%)
31
+ expect(result.taxableGain).toBe(20000);
32
+ expect(result.capitalGainsTax).toBe(20000 * 0.15);
33
+ });
34
+
35
+ it('returns zero for zero gain', () => {
36
+ const input: Input = { capitalGain: 0, totalTaxableIncome: 50000 };
37
+ const service = new CanadaCapitalGainsServiceImpl(input, canadaCapitalGainsRules);
38
+ const result = service.calculate();
39
+
40
+ expect(result.capitalGainsTax).toBe(0);
41
+ expect(result.taxableGain).toBe(0);
42
+ expect(result.breakdowns).toHaveLength(0);
43
+ });
44
+
45
+ it('returns zero for negative gain', () => {
46
+ const input: Input = { capitalGain: -10000, totalTaxableIncome: 50000 };
47
+ const service = new CanadaCapitalGainsServiceImpl(input, canadaCapitalGainsRules);
48
+ const result = service.calculate();
49
+
50
+ expect(result.capitalGainsTax).toBe(0);
51
+ });
52
+
53
+ it('calculates effective rate based on full gain', () => {
54
+ const input: Input = { capitalGain: 100000, totalTaxableIncome: 100000 };
55
+ const service = new CanadaCapitalGainsServiceImpl(input, canadaCapitalGainsRules);
56
+ const result = service.calculate();
57
+
58
+ expect(result.effectiveRate).toBe((result.capitalGainsTax / 100000) * 100);
59
+ });
60
+
61
+ it('stacks gain on top of other income', () => {
62
+ const input: Input = { capitalGain: 20000, totalTaxableIncome: 70000 };
63
+ const service = new CanadaCapitalGainsServiceImpl(input, canadaCapitalGainsRules);
64
+ const result = service.calculate();
65
+
66
+ // Other income = 50000, taxable gain = 10000
67
+ // 10000 falls in first bracket (50000-55867 = 5867 at 15%, rest at 20.5%)
68
+ expect(result.taxableGain).toBe(10000);
69
+ expect(result.capitalGainsTax).toBeGreaterThan(0);
70
+ });
71
+ });
@@ -0,0 +1,66 @@
1
+ import { FranceCapitalGainsServiceImpl } from '../src/capital-gains/france/FranceCapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/france/domain/types';
3
+
4
+ // France PFU (Prélèvement Forfaitaire Unique) 2024
5
+ const franceCapitalGainsRules: Rules = {
6
+ flatTaxRate: 0.128,
7
+ socialContributionsRate: 0.172,
8
+ };
9
+
10
+ describe('FranceCapitalGainsServiceImpl', () => {
11
+ it('applies flat tax rate and social contributions', () => {
12
+ const input: Input = { capitalGain: 100000 };
13
+ const service = new FranceCapitalGainsServiceImpl(input, franceCapitalGainsRules);
14
+ const result = service.calculate();
15
+
16
+ expect(result.incomeTax).toBe(12800);
17
+ expect(result.socialContributions).toBe(17200);
18
+ expect(result.totalTax).toBe(30000);
19
+ });
20
+
21
+ it('effective rate is 30% (12.8% + 17.2%)', () => {
22
+ const input: Input = { capitalGain: 50000 };
23
+ const service = new FranceCapitalGainsServiceImpl(input, franceCapitalGainsRules);
24
+ const result = service.calculate();
25
+
26
+ expect(result.effectiveRate).toBeCloseTo(30, 1);
27
+ });
28
+
29
+ it('returns two breakdowns (income tax + social)', () => {
30
+ const input: Input = { capitalGain: 100000 };
31
+ const service = new FranceCapitalGainsServiceImpl(input, franceCapitalGainsRules);
32
+ const result = service.calculate();
33
+
34
+ expect(result.breakdowns).toHaveLength(2);
35
+ expect(result.breakdowns[0].rate).toBe(0.128);
36
+ expect(result.breakdowns[1].rate).toBe(0.172);
37
+ });
38
+
39
+ it('returns zero for zero gain', () => {
40
+ const input: Input = { capitalGain: 0 };
41
+ const service = new FranceCapitalGainsServiceImpl(input, franceCapitalGainsRules);
42
+ const result = service.calculate();
43
+
44
+ expect(result.totalTax).toBe(0);
45
+ expect(result.effectiveRate).toBe(0);
46
+ expect(result.breakdowns).toHaveLength(0);
47
+ });
48
+
49
+ it('returns zero for negative gain', () => {
50
+ const input: Input = { capitalGain: -10000 };
51
+ const service = new FranceCapitalGainsServiceImpl(input, franceCapitalGainsRules);
52
+ const result = service.calculate();
53
+
54
+ expect(result.totalTax).toBe(0);
55
+ });
56
+
57
+ it('scales linearly with gain amount', () => {
58
+ const input1: Input = { capitalGain: 100000 };
59
+ const input2: Input = { capitalGain: 200000 };
60
+
61
+ const result1 = new FranceCapitalGainsServiceImpl(input1, franceCapitalGainsRules).calculate();
62
+ const result2 = new FranceCapitalGainsServiceImpl(input2, franceCapitalGainsRules).calculate();
63
+
64
+ expect(result2.totalTax).toBe(result1.totalTax * 2);
65
+ });
66
+ });
@@ -0,0 +1,85 @@
1
+ import { GermanyCapitalGainsServiceImpl } from '../src/capital-gains/germany/GermanyCapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/germany/domain/types';
3
+
4
+ // Germany 2024 Abgeltungsteuer rules
5
+ const germanyCapitalGainsRules: Rules = {
6
+ flatTaxRate: 0.25,
7
+ solidaritySurchargeRate: 0.055,
8
+ annualExemption: 1000,
9
+ };
10
+
11
+ describe('GermanyCapitalGainsServiceImpl', () => {
12
+ it('applies annual exemption of €1,000', () => {
13
+ const input: Input = { capitalGain: 800 };
14
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
15
+ const result = service.calculate();
16
+
17
+ expect(result.taxableGain).toBe(0);
18
+ expect(result.totalTax).toBe(0);
19
+ });
20
+
21
+ it('taxes gains above exemption at 25% + solidarity', () => {
22
+ const input: Input = { capitalGain: 11000 };
23
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
24
+ const result = service.calculate();
25
+
26
+ // Taxable = 11000 - 1000 = 10000
27
+ // Tax = 10000 * 25% = 2500
28
+ // Solidarity = 2500 * 5.5% = 137.5
29
+ // Total = 2637.5
30
+ expect(result.taxableGain).toBe(10000);
31
+ expect(result.capitalGainsTax).toBe(2500);
32
+ expect(result.solidaritySurcharge).toBe(137.5);
33
+ expect(result.totalTax).toBe(2637.5);
34
+ });
35
+
36
+ it('effective rate accounts for exemption', () => {
37
+ const input: Input = { capitalGain: 11000 };
38
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
39
+ const result = service.calculate();
40
+
41
+ // Effective rate = 2637.5 / 11000 * 100
42
+ expect(result.effectiveRate).toBeCloseTo((2637.5 / 11000) * 100, 2);
43
+ });
44
+
45
+ it('returns two breakdowns for taxable gains', () => {
46
+ const input: Input = { capitalGain: 5000 };
47
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
48
+ const result = service.calculate();
49
+
50
+ expect(result.breakdowns).toHaveLength(2);
51
+ expect(result.breakdowns[0].rate).toBe(0.25);
52
+ expect(result.breakdowns[1].rate).toBe(0.055);
53
+ });
54
+
55
+ it('returns zero for zero gain', () => {
56
+ const input: Input = { capitalGain: 0 };
57
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
58
+ const result = service.calculate();
59
+
60
+ expect(result.totalTax).toBe(0);
61
+ expect(result.breakdowns).toHaveLength(0);
62
+ });
63
+
64
+ it('returns zero for negative gain', () => {
65
+ const input: Input = { capitalGain: -5000 };
66
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
67
+ const result = service.calculate();
68
+
69
+ expect(result.totalTax).toBe(0);
70
+ });
71
+
72
+ it('calculates correctly for large gains', () => {
73
+ const input: Input = { capitalGain: 100000 };
74
+ const service = new GermanyCapitalGainsServiceImpl(input, germanyCapitalGainsRules);
75
+ const result = service.calculate();
76
+
77
+ const taxable = 99000;
78
+ const cgt = taxable * 0.25;
79
+ const soli = cgt * 0.055;
80
+ expect(result.taxableGain).toBe(taxable);
81
+ expect(result.capitalGainsTax).toBe(cgt);
82
+ expect(result.solidaritySurcharge).toBeCloseTo(soli, 2);
83
+ expect(result.totalTax).toBeCloseTo(cgt + soli, 2);
84
+ });
85
+ });
@@ -0,0 +1,84 @@
1
+ import { SouthAfricaCapitalGainsServiceImpl } from '../src/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/south-africa/domain/types';
3
+
4
+ // South Africa 2024-25 CGT rules (individuals)
5
+ const southAfricaCapitalGainsRules: Rules = {
6
+ inclusionRate: 0.40,
7
+ annualExclusion: 40000,
8
+ taxBrackets: [
9
+ { from: 0, to: 237100, rate: 0.18 },
10
+ { from: 237100, to: 370500, rate: 0.26 },
11
+ { from: 370500, to: 512800, rate: 0.31 },
12
+ { from: 512800, to: 673000, rate: 0.36 },
13
+ { from: 673000, to: 857900, rate: 0.39 },
14
+ { from: 857900, to: 1817000, rate: 0.41 },
15
+ { from: 1817000, to: null, rate: 0.45 },
16
+ ],
17
+ };
18
+
19
+ describe('SouthAfricaCapitalGainsServiceImpl', () => {
20
+ it('applies annual exclusion of R40,000', () => {
21
+ const input: Input = { capitalGain: 30000, totalTaxableIncome: 30000 };
22
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
23
+ const result = service.calculate();
24
+
25
+ expect(result.taxableGain).toBe(0);
26
+ expect(result.capitalGainsTax).toBe(0);
27
+ });
28
+
29
+ it('applies 40% inclusion rate after exclusion', () => {
30
+ const input: Input = { capitalGain: 140000, totalTaxableIncome: 140000 };
31
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
32
+ const result = service.calculate();
33
+
34
+ // Net gain = 140000 - 40000 = 100000
35
+ // Taxable gain = 100000 * 0.40 = 40000
36
+ expect(result.taxableGain).toBe(40000);
37
+ });
38
+
39
+ it('taxes included gain at marginal rates', () => {
40
+ const input: Input = { capitalGain: 140000, totalTaxableIncome: 140000 };
41
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
42
+ const result = service.calculate();
43
+
44
+ // Taxable gain = 40000, first bracket rate 18%
45
+ expect(result.capitalGainsTax).toBe(40000 * 0.18);
46
+ });
47
+
48
+ it('returns zero for zero gain', () => {
49
+ const input: Input = { capitalGain: 0, totalTaxableIncome: 300000 };
50
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
51
+ const result = service.calculate();
52
+
53
+ expect(result.capitalGainsTax).toBe(0);
54
+ expect(result.breakdowns).toHaveLength(0);
55
+ });
56
+
57
+ it('returns zero for negative gain', () => {
58
+ const input: Input = { capitalGain: -50000, totalTaxableIncome: 300000 };
59
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
60
+ const result = service.calculate();
61
+
62
+ expect(result.capitalGainsTax).toBe(0);
63
+ });
64
+
65
+ it('calculates effective rate based on full gain', () => {
66
+ const input: Input = { capitalGain: 140000, totalTaxableIncome: 140000 };
67
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
68
+ const result = service.calculate();
69
+
70
+ expect(result.effectiveRate).toBe((result.capitalGainsTax / 140000) * 100);
71
+ });
72
+
73
+ it('stacks included gain on other income', () => {
74
+ const input: Input = { capitalGain: 540000, totalTaxableIncome: 840000 };
75
+ const service = new SouthAfricaCapitalGainsServiceImpl(input, southAfricaCapitalGainsRules);
76
+ const result = service.calculate();
77
+
78
+ // Net gain = 540000 - 40000 = 500000
79
+ // Taxable gain = 500000 * 0.40 = 200000
80
+ // Other income = 300000
81
+ expect(result.taxableGain).toBe(200000);
82
+ expect(result.capitalGainsTax).toBeGreaterThan(0);
83
+ });
84
+ });
@@ -0,0 +1,79 @@
1
+ import { UKCapitalGainsServiceImpl } from '../src/capital-gains/uk/UKCapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/uk/domain/types';
3
+
4
+ // UK 2024-25 CGT rules (non-residential assets)
5
+ const ukCapitalGainsRules: Rules = {
6
+ annualExemption: 3000,
7
+ basicRate: 0.10,
8
+ higherRate: 0.20,
9
+ basicRateLimit: 37700,
10
+ };
11
+
12
+ describe('UKCapitalGainsServiceImpl', () => {
13
+ it('applies annual exemption', () => {
14
+ const input: Input = { capitalGain: 2000, totalTaxableIncome: 20000 };
15
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
16
+ const result = service.calculate();
17
+
18
+ expect(result.taxableGain).toBe(0);
19
+ expect(result.capitalGainsTax).toBe(0);
20
+ });
21
+
22
+ it('applies basic rate for basic-rate taxpayer', () => {
23
+ const input: Input = { capitalGain: 13000, totalTaxableIncome: 20000 };
24
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
25
+ const result = service.calculate();
26
+
27
+ // Taxable gain = 13000 - 3000 = 10000
28
+ // Basic rate remaining = 37700 - 20000 = 17700 (enough for all)
29
+ expect(result.taxableGain).toBe(10000);
30
+ expect(result.capitalGainsTax).toBe(10000 * 0.10);
31
+ });
32
+
33
+ it('applies higher rate for higher-rate taxpayer', () => {
34
+ const input: Input = { capitalGain: 53000, totalTaxableIncome: 50000 };
35
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
36
+ const result = service.calculate();
37
+
38
+ // Taxable gain = 53000 - 3000 = 50000, all at higher rate (income > basic rate limit)
39
+ expect(result.taxableGain).toBe(50000);
40
+ expect(result.capitalGainsTax).toBe(50000 * 0.20);
41
+ });
42
+
43
+ it('splits between basic and higher rate', () => {
44
+ const input: Input = { capitalGain: 23000, totalTaxableIncome: 30000 };
45
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
46
+ const result = service.calculate();
47
+
48
+ // Taxable gain = 23000 - 3000 = 20000
49
+ // Basic rate remaining = 37700 - 30000 = 7700
50
+ // 7700 at 10% = 770, 12300 at 20% = 2460
51
+ expect(result.taxableGain).toBe(20000);
52
+ expect(result.capitalGainsTax).toBe(7700 * 0.10 + 12300 * 0.20);
53
+ });
54
+
55
+ it('returns zero for zero gain', () => {
56
+ const input: Input = { capitalGain: 0, totalTaxableIncome: 50000 };
57
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
58
+ const result = service.calculate();
59
+
60
+ expect(result.capitalGainsTax).toBe(0);
61
+ expect(result.breakdowns).toHaveLength(0);
62
+ });
63
+
64
+ it('returns zero for negative gain', () => {
65
+ const input: Input = { capitalGain: -5000, totalTaxableIncome: 50000 };
66
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
67
+ const result = service.calculate();
68
+
69
+ expect(result.capitalGainsTax).toBe(0);
70
+ });
71
+
72
+ it('calculates effective rate based on full gain', () => {
73
+ const input: Input = { capitalGain: 53000, totalTaxableIncome: 50000 };
74
+ const service = new UKCapitalGainsServiceImpl(input, ukCapitalGainsRules);
75
+ const result = service.calculate();
76
+
77
+ expect(result.effectiveRate).toBe((result.capitalGainsTax / 53000) * 100);
78
+ });
79
+ });
@@ -0,0 +1,88 @@
1
+ import { USACapitalGainsServiceImpl } from '../src/capital-gains/usa/USACapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/usa/domain/types';
3
+
4
+ // USA 2024 long-term capital gains brackets (single filer)
5
+ const usaCapitalGainsRules: Rules = {
6
+ longTermBrackets: [
7
+ { from: 0, to: 47025, rate: 0.00 },
8
+ { from: 47025, to: 518900, rate: 0.15 },
9
+ { from: 518900, to: null, rate: 0.20 },
10
+ ],
11
+ shortTermBrackets: [
12
+ { from: 0, to: 11600, rate: 0.10 },
13
+ { from: 11600, to: 47150, rate: 0.12 },
14
+ { from: 47150, to: 100525, rate: 0.22 },
15
+ { from: 100525, to: 191950, rate: 0.24 },
16
+ { from: 191950, to: 243725, rate: 0.32 },
17
+ { from: 243725, to: 609350, rate: 0.35 },
18
+ { from: 609350, to: null, rate: 0.37 },
19
+ ],
20
+ netInvestmentIncomeTax: { rate: 0.038, threshold: 200000 },
21
+ };
22
+
23
+ describe('USACapitalGainsServiceImpl', () => {
24
+ it('applies 0% long-term rate for low income', () => {
25
+ const input: Input = { capitalGain: 30000, totalTaxableIncome: 30000, holdingPeriodMonths: 24 };
26
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
27
+ const result = service.calculate();
28
+
29
+ expect(result.capitalGainsTax).toBe(0);
30
+ expect(result.netInvestmentIncomeTax).toBe(0);
31
+ expect(result.totalTax).toBe(0);
32
+ });
33
+
34
+ it('applies 15% long-term rate for mid income', () => {
35
+ const input: Input = { capitalGain: 50000, totalTaxableIncome: 100000, holdingPeriodMonths: 13 };
36
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
37
+ const result = service.calculate();
38
+
39
+ // Other income = 50000, gain falls in 50000-100000 range, all at 15%
40
+ expect(result.capitalGainsTax).toBe(50000 * 0.15);
41
+ expect(result.netInvestmentIncomeTax).toBe(0);
42
+ });
43
+
44
+ it('applies NIIT for high-income earners', () => {
45
+ const input: Input = { capitalGain: 100000, totalTaxableIncome: 300000, holdingPeriodMonths: 24 };
46
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
47
+ const result = service.calculate();
48
+
49
+ expect(result.netInvestmentIncomeTax).toBe(100000 * 0.038);
50
+ expect(result.totalTax).toBe(result.capitalGainsTax + result.netInvestmentIncomeTax);
51
+ });
52
+
53
+ it('applies short-term rates for gains held <= 12 months', () => {
54
+ const input: Input = { capitalGain: 50000, totalTaxableIncome: 50000, holdingPeriodMonths: 6 };
55
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
56
+ const result = service.calculate();
57
+
58
+ // Short-term gain taxed as ordinary income from 0-50000
59
+ expect(result.capitalGainsTax).toBeGreaterThan(0);
60
+ expect(result.breakdowns.length).toBeGreaterThan(0);
61
+ });
62
+
63
+ it('returns zero for zero gain', () => {
64
+ const input: Input = { capitalGain: 0, totalTaxableIncome: 50000, holdingPeriodMonths: 24 };
65
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
66
+ const result = service.calculate();
67
+
68
+ expect(result.totalTax).toBe(0);
69
+ expect(result.effectiveRate).toBe(0);
70
+ expect(result.breakdowns).toHaveLength(0);
71
+ });
72
+
73
+ it('returns zero for negative gain', () => {
74
+ const input: Input = { capitalGain: -5000, totalTaxableIncome: 50000, holdingPeriodMonths: 24 };
75
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
76
+ const result = service.calculate();
77
+
78
+ expect(result.totalTax).toBe(0);
79
+ });
80
+
81
+ it('calculates effective rate correctly', () => {
82
+ const input: Input = { capitalGain: 100000, totalTaxableIncome: 100000, holdingPeriodMonths: 24 };
83
+ const service = new USACapitalGainsServiceImpl(input, usaCapitalGainsRules);
84
+ const result = service.calculate();
85
+
86
+ expect(result.effectiveRate).toBe((result.totalTax / 100000) * 100);
87
+ });
88
+ });