@novha/calc-engines 3.0.0 → 4.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 (64) hide show
  1. package/dist/corporate/germany/GermanyCorporateTaxService.js +3 -0
  2. package/dist/corporate/germany/GermanyCorporateTaxServiceImpl.js +56 -0
  3. package/dist/corporate/germany/domain/types.js +3 -0
  4. package/dist/corporate/usa/USACorporateTaxService.js +3 -0
  5. package/dist/corporate/usa/USACorporateTaxServiceImpl.js +29 -0
  6. package/dist/corporate/usa/domain/types.js +3 -0
  7. package/dist/income-tax/germany/GermanyIncomeTaxService.js +3 -0
  8. package/dist/income-tax/germany/GermanyIncomeTaxServiceImpl.js +86 -0
  9. package/dist/income-tax/germany/domain/types.js +3 -0
  10. package/dist/income-tax/usa/USAIncomeTaxService.js +3 -0
  11. package/dist/income-tax/usa/USAIncomeTaxServiceImpl.js +87 -0
  12. package/dist/income-tax/usa/domain/types.js +3 -0
  13. package/dist/index.js +16 -2
  14. package/dist/mortgage/germany/GermanyMortgageService.js +3 -0
  15. package/dist/mortgage/germany/GermanyMortgageServiceImpl.js +80 -0
  16. package/dist/mortgage/germany/domain/types.js +3 -0
  17. package/dist/mortgage/usa/USAMortgageService.js +3 -0
  18. package/dist/mortgage/usa/USAMortgageServiceImpl.js +92 -0
  19. package/dist/mortgage/usa/domain/types.js +3 -0
  20. package/dist/types/corporate/germany/GermanyCorporateTaxService.d.ts +4 -0
  21. package/dist/types/corporate/germany/GermanyCorporateTaxServiceImpl.d.ts +8 -0
  22. package/dist/types/corporate/germany/domain/types.d.ts +24 -0
  23. package/dist/types/corporate/usa/USACorporateTaxService.d.ts +4 -0
  24. package/dist/types/corporate/usa/USACorporateTaxServiceImpl.d.ts +8 -0
  25. package/dist/types/corporate/usa/domain/types.d.ts +15 -0
  26. package/dist/types/income-tax/germany/GermanyIncomeTaxService.d.ts +4 -0
  27. package/dist/types/income-tax/germany/GermanyIncomeTaxServiceImpl.d.ts +12 -0
  28. package/dist/types/income-tax/germany/domain/types.d.ts +23 -0
  29. package/dist/types/income-tax/usa/USAIncomeTaxService.d.ts +4 -0
  30. package/dist/types/income-tax/usa/USAIncomeTaxServiceImpl.d.ts +11 -0
  31. package/dist/types/income-tax/usa/domain/types.d.ts +30 -0
  32. package/dist/types/index.d.ts +12 -0
  33. package/dist/types/mortgage/germany/GermanyMortgageService.d.ts +4 -0
  34. package/dist/types/mortgage/germany/GermanyMortgageServiceImpl.d.ts +7 -0
  35. package/dist/types/mortgage/germany/domain/types.d.ts +42 -0
  36. package/dist/types/mortgage/usa/USAMortgageService.d.ts +4 -0
  37. package/dist/types/mortgage/usa/USAMortgageServiceImpl.d.ts +8 -0
  38. package/dist/types/mortgage/usa/domain/types.d.ts +44 -0
  39. package/package.json +1 -1
  40. package/src/corporate/germany/GermanyCorporateTaxService.ts +5 -0
  41. package/src/corporate/germany/GermanyCorporateTaxServiceImpl.ts +64 -0
  42. package/src/corporate/germany/domain/types.ts +20 -0
  43. package/src/corporate/usa/USACorporateTaxService.ts +5 -0
  44. package/src/corporate/usa/USACorporateTaxServiceImpl.ts +35 -0
  45. package/src/corporate/usa/domain/types.ts +15 -0
  46. package/src/income-tax/germany/GermanyIncomeTaxService.ts +5 -0
  47. package/src/income-tax/germany/GermanyIncomeTaxServiceImpl.ts +121 -0
  48. package/src/income-tax/germany/domain/types.ts +27 -0
  49. package/src/income-tax/usa/USAIncomeTaxService.ts +5 -0
  50. package/src/income-tax/usa/USAIncomeTaxServiceImpl.ts +113 -0
  51. package/src/income-tax/usa/domain/types.ts +24 -0
  52. package/src/index.ts +42 -0
  53. package/src/mortgage/germany/GermanyMortgageService.ts +5 -0
  54. package/src/mortgage/germany/GermanyMortgageServiceImpl.ts +111 -0
  55. package/src/mortgage/germany/domain/types.ts +49 -0
  56. package/src/mortgage/usa/USAMortgageService.ts +5 -0
  57. package/src/mortgage/usa/USAMortgageServiceImpl.ts +134 -0
  58. package/src/mortgage/usa/domain/types.ts +52 -0
  59. package/test/germany-corporate-tax.test.ts +77 -0
  60. package/test/germany-income-tax.test.ts +114 -0
  61. package/test/germany-mortgage.test.ts +105 -0
  62. package/test/usa-corporate-tax.test.ts +59 -0
  63. package/test/usa-income-tax.test.ts +138 -0
  64. package/test/usa-mortgage.test.ts +97 -0
@@ -0,0 +1,52 @@
1
+ import { OtherFees } from "../../domain/types";
2
+
3
+ export interface MortgageRules {
4
+ loanConstraints: LoanConstraints;
5
+ interest: InterestRules;
6
+ transferTax: TransferTaxRules;
7
+ }
8
+
9
+ export interface LoanConstraints {
10
+ maxLtvPercent: number;
11
+ maxAmortizationYears: number;
12
+ }
13
+
14
+ export interface InterestRules {
15
+ compounding: 'MONTHLY';
16
+ }
17
+
18
+ export interface TransferTaxBracket {
19
+ upTo?: number;
20
+ above?: number;
21
+ rate: number;
22
+ }
23
+
24
+ export interface TransferTaxRules {
25
+ brackets: TransferTaxBracket[];
26
+ }
27
+
28
+ export interface AmortizationScheduleItem {
29
+ year: number;
30
+ principal: number;
31
+ interest: number;
32
+ balance: number;
33
+ }
34
+
35
+ export interface MortgageInput {
36
+ propertyPrice: number;
37
+ downPayment: number;
38
+ annualInterestRate: number;
39
+ amortizationYears: number;
40
+ isFirstTimeBuyer: boolean;
41
+ }
42
+
43
+ export interface MortgageOutput {
44
+ loanAmount: number;
45
+ totalMortgage: number;
46
+ monthlyPayment: number;
47
+ totalInterestPaid: number;
48
+ totalPaid: number;
49
+ transferTax: number;
50
+ amortizationSchedule: AmortizationScheduleItem[];
51
+ otherFees: OtherFees;
52
+ }
@@ -0,0 +1,77 @@
1
+ import { GermanyCorporateTaxServiceImpl } from '../src/corporate/germany/GermanyCorporateTaxServiceImpl';
2
+ import { Input, Rules } from '../src/corporate/germany/domain/types';
3
+
4
+ // Germany corporate tax rules
5
+ const germanyCorporateRules: Rules = {
6
+ corporateIncomeTax: { rate: 0.15 },
7
+ solidaritySurcharge: { rate: 0.055 },
8
+ tradeTax: { multiplier: 0.035, assessmentRate: 4.0 },
9
+ };
10
+
11
+ describe('GermanyCorporateTaxServiceImpl', () => {
12
+ it('correctly calculates all components for €100,000 taxable income', () => {
13
+ const input: Input = { taxableIncome: 100000 };
14
+ const service = new GermanyCorporateTaxServiceImpl(input, germanyCorporateRules);
15
+ const result = service.calculate();
16
+
17
+ // Corp tax = 100000 * 15% = 15000
18
+ // Solidarity = 15000 * 5.5% = 825
19
+ // Trade tax = 100000 * 3.5% * 4.0 = 14000
20
+ // Total = 29825
21
+ expect(result.corporateTax).toBe(15000);
22
+ expect(result.solidaritySurcharge).toBe(825);
23
+ expect(result.tradeTax).toBeCloseTo(14000, 2);
24
+ expect(result.totalTax).toBeCloseTo(29825, 2);
25
+ });
26
+
27
+ it('calculates effective tax rate correctly', () => {
28
+ const input: Input = { taxableIncome: 100000 };
29
+ const service = new GermanyCorporateTaxServiceImpl(input, germanyCorporateRules);
30
+ const result = service.calculate();
31
+
32
+ // Effective rate = 29825 / 100000 * 100 = 29.825%
33
+ expect(result.effectiveTaxRate).toBeCloseTo(29.825, 3);
34
+ });
35
+
36
+ it('returns zero for all components when taxable income is zero', () => {
37
+ const input: Input = { taxableIncome: 0 };
38
+ const service = new GermanyCorporateTaxServiceImpl(input, germanyCorporateRules);
39
+ const result = service.calculate();
40
+
41
+ expect(result.corporateTax).toBe(0);
42
+ expect(result.solidaritySurcharge).toBe(0);
43
+ expect(result.tradeTax).toBe(0);
44
+ expect(result.totalTax).toBe(0);
45
+ expect(result.effectiveTaxRate).toBe(0);
46
+ expect(result.breakdowns).toHaveLength(0);
47
+ });
48
+
49
+ it('returns three breakdown entries for positive income', () => {
50
+ const input: Input = { taxableIncome: 100000 };
51
+ const service = new GermanyCorporateTaxServiceImpl(input, germanyCorporateRules);
52
+ const result = service.calculate();
53
+
54
+ expect(result.breakdowns).toHaveLength(3);
55
+ });
56
+
57
+ it('breakdown amounts match computed values', () => {
58
+ const input: Input = { taxableIncome: 100000 };
59
+ const service = new GermanyCorporateTaxServiceImpl(input, germanyCorporateRules);
60
+ const result = service.calculate();
61
+
62
+ expect(result.breakdowns[0].amount).toBe(15000);
63
+ expect(result.breakdowns[1].amount).toBe(825);
64
+ expect(result.breakdowns[2].amount).toBeCloseTo(14000, 2);
65
+ });
66
+
67
+ it('scales proportionally for different income levels', () => {
68
+ const input: Input = { taxableIncome: 200000 };
69
+ const service = new GermanyCorporateTaxServiceImpl(input, germanyCorporateRules);
70
+ const result = service.calculate();
71
+
72
+ expect(result.corporateTax).toBe(30000);
73
+ expect(result.solidaritySurcharge).toBe(1650);
74
+ expect(result.tradeTax).toBeCloseTo(28000, 2);
75
+ expect(result.totalTax).toBeCloseTo(59650, 2);
76
+ });
77
+ });
@@ -0,0 +1,114 @@
1
+ import { GermanyIncomeTaxServiceImpl } from '../src/income-tax/germany/GermanyIncomeTaxServiceImpl';
2
+ import { IncomeTaxRules } from '../src/income-tax/germany/domain/types';
3
+
4
+ // Germany 2024 income tax rules (simplified bracket approach)
5
+ const germanyRules: IncomeTaxRules = {
6
+ taxBrackets: [
7
+ { from: 0, to: 11604, rate: 0 },
8
+ { from: 11604, to: 17006, rate: 0.14 },
9
+ { from: 17006, to: 62810, rate: 0.24 },
10
+ { from: 62810, to: 277826, rate: 0.42 },
11
+ { from: 277826, to: null, rate: 0.45 },
12
+ ],
13
+ solidaritySurcharge: {
14
+ rate: 0.055,
15
+ exemptionThreshold: 18130,
16
+ },
17
+ socialContributions: {
18
+ rate: 0.197,
19
+ },
20
+ };
21
+
22
+ describe('GermanyIncomeTaxServiceImpl', () => {
23
+ it('returns zero income tax for income at or below the Grundfreibetrag', () => {
24
+ const service = new GermanyIncomeTaxServiceImpl(11604, germanyRules);
25
+ const result = service.calculateNetIncome();
26
+
27
+ expect(result.incomeTax).toBe(0);
28
+ expect(result.grossIncome).toBe(11604);
29
+ });
30
+
31
+ it('correctly calculates income tax for €30,000 income', () => {
32
+ const service = new GermanyIncomeTaxServiceImpl(30000, germanyRules);
33
+ const result = service.calculateNetIncome();
34
+
35
+ // 0%: 11604 * 0 = 0
36
+ // 14%: (17006 - 11604) * 0.14 = 5402 * 0.14 = 756.28
37
+ // 24%: (30000 - 17006) * 0.24 = 12994 * 0.24 = 3118.56
38
+ // Total = 3874.84
39
+ expect(result.incomeTax).toBeCloseTo(3874.84, 2);
40
+ });
41
+
42
+ it('correctly calculates income tax for €80,000 income', () => {
43
+ const service = new GermanyIncomeTaxServiceImpl(80000, germanyRules);
44
+ const result = service.calculateNetIncome();
45
+
46
+ // 0%: 11604 * 0 = 0
47
+ // 14%: (17006 - 11604) * 0.14 = 5402 * 0.14 = 756.28
48
+ // 24%: (62810 - 17006) * 0.24 = 45804 * 0.24 = 10992.96
49
+ // 42%: (80000 - 62810) * 0.42 = 17190 * 0.42 = 7219.80
50
+ // Total = 18969.04
51
+ expect(result.incomeTax).toBeCloseTo(18969.04, 2);
52
+ });
53
+
54
+ it('does not apply solidarity surcharge when income tax is below exemption threshold', () => {
55
+ // €30,000: income tax ≈ 3874.84 < 18130
56
+ const service = new GermanyIncomeTaxServiceImpl(30000, germanyRules);
57
+ const result = service.calculateNetIncome();
58
+
59
+ expect(result.solidaritySurcharge).toBe(0);
60
+ });
61
+
62
+ it('applies solidarity surcharge when income tax exceeds exemption threshold', () => {
63
+ // €80,000: income tax ≈ 18969.04 > 18130 → soli = 18969.04 * 5.5%
64
+ const service = new GermanyIncomeTaxServiceImpl(80000, germanyRules);
65
+ const result = service.calculateNetIncome();
66
+
67
+ expect(result.solidaritySurcharge).toBeCloseTo(18969.04 * 0.055, 2);
68
+ });
69
+
70
+ it('calculates social contributions as combined rate on gross income', () => {
71
+ const service = new GermanyIncomeTaxServiceImpl(30000, germanyRules);
72
+ const result = service.calculateNetIncome();
73
+
74
+ // 30000 * 19.7% = 5910
75
+ expect(result.socialContributions).toBeCloseTo(5910, 2);
76
+ });
77
+
78
+ it('net income equals gross income minus total deductions', () => {
79
+ const service = new GermanyIncomeTaxServiceImpl(50000, germanyRules);
80
+ const result = service.calculateNetIncome();
81
+
82
+ expect(result.netIncome).toBeCloseTo(
83
+ result.grossIncome - result.totalDeductions,
84
+ 2,
85
+ );
86
+ });
87
+
88
+ it('total deductions equals income tax plus solidarity surcharge plus social contributions', () => {
89
+ const service = new GermanyIncomeTaxServiceImpl(80000, germanyRules);
90
+ const result = service.calculateNetIncome();
91
+
92
+ expect(result.totalDeductions).toBeCloseTo(
93
+ result.incomeTax + result.solidaritySurcharge + result.socialContributions,
94
+ 2,
95
+ );
96
+ });
97
+
98
+ it('effective tax rate is income tax divided by gross income', () => {
99
+ const service = new GermanyIncomeTaxServiceImpl(80000, germanyRules);
100
+ const result = service.calculateNetIncome();
101
+
102
+ expect(result.effectiveTaxRate).toBeCloseTo(result.incomeTax / result.grossIncome, 4);
103
+ });
104
+
105
+ it('returns zero tax for zero income', () => {
106
+ const service = new GermanyIncomeTaxServiceImpl(0, germanyRules);
107
+ const result = service.calculateNetIncome();
108
+
109
+ expect(result.incomeTax).toBe(0);
110
+ expect(result.solidaritySurcharge).toBe(0);
111
+ expect(result.socialContributions).toBe(0);
112
+ expect(result.effectiveTaxRate).toBe(0);
113
+ });
114
+ });
@@ -0,0 +1,105 @@
1
+ import { GermanyMortgageServiceImpl } from '../src/mortgage/germany/GermanyMortgageServiceImpl';
2
+ import { MortgageInput, MortgageRules } from '../src/mortgage/germany/domain/types';
3
+
4
+ // Germany mortgage rules
5
+ const germanyMortgageRules: MortgageRules = {
6
+ loanConstraints: {
7
+ maxLtvPercent: 80,
8
+ maxAmortizationYears: 30,
9
+ },
10
+ interest: {
11
+ compounding: 'MONTHLY',
12
+ },
13
+ landTransferTax: {
14
+ rate: 0.035,
15
+ },
16
+ notaryFeeRate: 0.015,
17
+ registrationFeeRate: 0.005,
18
+ };
19
+
20
+ const defaultInput: MortgageInput = {
21
+ propertyPrice: 300000,
22
+ downPayment: 60000, // 20% down
23
+ annualInterestRate: 3.5,
24
+ amortizationYears: 25,
25
+ };
26
+
27
+ describe('GermanyMortgageServiceImpl', () => {
28
+ const service = new GermanyMortgageServiceImpl();
29
+
30
+ it('calculates loan amount correctly', () => {
31
+ const result = service.calculate(defaultInput, germanyMortgageRules);
32
+
33
+ expect(result.loanAmount).toBe(240000);
34
+ });
35
+
36
+ it('calculates monthly payment using standard amortization formula', () => {
37
+ const result = service.calculate(defaultInput, germanyMortgageRules);
38
+
39
+ const r = 0.035 / 12;
40
+ const n = 25 * 12;
41
+ const expected = 240000 * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
42
+ expect(result.monthlyPayment).toBeCloseTo(expected, 2);
43
+ });
44
+
45
+ it('calculates Grunderwerbsteuer (land transfer tax) as flat rate on property price', () => {
46
+ const result = service.calculate(defaultInput, germanyMortgageRules);
47
+
48
+ // 300000 * 3.5% = 10500
49
+ expect(result.landTransferTax).toBeCloseTo(10500, 2);
50
+ });
51
+
52
+ it('calculates notary fees as flat rate on property price', () => {
53
+ const result = service.calculate(defaultInput, germanyMortgageRules);
54
+
55
+ // 300000 * 1.5% = 4500
56
+ expect(result.notaryFees).toBe(4500);
57
+ });
58
+
59
+ it('calculates registration fees as flat rate on property price', () => {
60
+ const result = service.calculate(defaultInput, germanyMortgageRules);
61
+
62
+ // 300000 * 0.5% = 1500
63
+ expect(result.registrationFees).toBe(1500);
64
+ });
65
+
66
+ it('generates amortization schedule with correct length', () => {
67
+ const result = service.calculate(defaultInput, germanyMortgageRules);
68
+
69
+ expect(result.amortizationSchedule.length).toBe(25);
70
+ expect(result.amortizationSchedule[0].year).toBe(1);
71
+ expect(result.amortizationSchedule[24].balance).toBeCloseTo(0, 0);
72
+ });
73
+
74
+ it('amortization schedule balance decreases over time', () => {
75
+ const result = service.calculate(defaultInput, germanyMortgageRules);
76
+ const schedule = result.amortizationSchedule;
77
+
78
+ for (let i = 1; i < schedule.length; i++) {
79
+ expect(schedule[i].balance).toBeLessThan(schedule[i - 1].balance);
80
+ }
81
+ });
82
+
83
+ it('totalPaid equals monthlyPayment * totalPayments', () => {
84
+ const result = service.calculate(defaultInput, germanyMortgageRules);
85
+ const totalPayments = 25 * 12;
86
+
87
+ expect(result.totalPaid).toBeCloseTo(result.monthlyPayment * totalPayments, 0);
88
+ });
89
+
90
+ it('throws error when loan amount is zero or negative', () => {
91
+ const input: MortgageInput = {
92
+ ...defaultInput,
93
+ downPayment: 300000,
94
+ };
95
+
96
+ expect(() => service.calculate(input, germanyMortgageRules)).toThrow('Invalid loan amount');
97
+ });
98
+
99
+ it('otherFees includes notary fees label', () => {
100
+ const result = service.calculate(defaultInput, germanyMortgageRules);
101
+
102
+ expect(result.otherFees.notaryFees.label).toBe('NOTARY_FEES');
103
+ expect(result.otherFees.notaryFees.value).toBe(result.notaryFees);
104
+ });
105
+ });
@@ -0,0 +1,59 @@
1
+ import { USACorporateTaxServiceImpl } from '../src/corporate/usa/USACorporateTaxServiceImpl';
2
+ import { Input, Rules } from '../src/corporate/usa/domain/types';
3
+
4
+ // USA federal corporate tax rules (TCJA 2017, flat 21%)
5
+ const usaCorporateRules: Rules = {
6
+ regime: {
7
+ type: 'flat',
8
+ rate: 0.21,
9
+ },
10
+ };
11
+
12
+ describe('USACorporateTaxServiceImpl', () => {
13
+ it('applies flat 21% rate for $100,000 taxable income', () => {
14
+ const input: Input = { taxableIncome: 100000 };
15
+ const service = new USACorporateTaxServiceImpl(input, usaCorporateRules);
16
+ const result = service.calculate();
17
+
18
+ expect(result.corporateTax).toBe(21000);
19
+ expect(result.effectiveTaxRate).toBe(21);
20
+ });
21
+
22
+ it('returns zero tax for zero taxable income', () => {
23
+ const input: Input = { taxableIncome: 0 };
24
+ const service = new USACorporateTaxServiceImpl(input, usaCorporateRules);
25
+ const result = service.calculate();
26
+
27
+ expect(result.corporateTax).toBe(0);
28
+ expect(result.effectiveTaxRate).toBe(0);
29
+ expect(result.breakdowns).toHaveLength(0);
30
+ });
31
+
32
+ it('effective tax rate is always 21% for positive income', () => {
33
+ const input: Input = { taxableIncome: 500000 };
34
+ const service = new USACorporateTaxServiceImpl(input, usaCorporateRules);
35
+ const result = service.calculate();
36
+
37
+ expect(result.effectiveTaxRate).toBe(21);
38
+ expect(result.corporateTax).toBe(105000);
39
+ });
40
+
41
+ it('returns one breakdown entry for flat rate', () => {
42
+ const input: Input = { taxableIncome: 100000 };
43
+ const service = new USACorporateTaxServiceImpl(input, usaCorporateRules);
44
+ const result = service.calculate();
45
+
46
+ expect(result.breakdowns).toHaveLength(1);
47
+ expect(result.breakdowns[0].rate).toBe(0.21);
48
+ expect(result.breakdowns[0].amount).toBe(21000);
49
+ });
50
+
51
+ it('scales linearly for large income', () => {
52
+ const input: Input = { taxableIncome: 1000000 };
53
+ const service = new USACorporateTaxServiceImpl(input, usaCorporateRules);
54
+ const result = service.calculate();
55
+
56
+ expect(result.corporateTax).toBe(210000);
57
+ expect(result.effectiveTaxRate).toBe(21);
58
+ });
59
+ });
@@ -0,0 +1,138 @@
1
+ import { USAIncomeTaxServiceImpl } from '../src/income-tax/usa/USAIncomeTaxServiceImpl';
2
+ import { IncomeTaxRules } from '../src/income-tax/usa/domain/types';
3
+
4
+ // USA 2024 federal income tax rules (single filer)
5
+ const usaRules: IncomeTaxRules = {
6
+ taxBrackets: [
7
+ { from: 0, to: 11600, rate: 0.10 },
8
+ { from: 11600, to: 47150, rate: 0.12 },
9
+ { from: 47150, to: 100525, rate: 0.22 },
10
+ { from: 100525, to: 191950, rate: 0.24 },
11
+ { from: 191950, to: 243725, rate: 0.32 },
12
+ { from: 243725, to: 609350, rate: 0.35 },
13
+ { from: 609350, to: null, rate: 0.37 },
14
+ ],
15
+ standardDeduction: { amount: 14600 },
16
+ fica: {
17
+ socialSecurity: { rate: 0.062, wageBase: 168600 },
18
+ medicare: { rate: 0.0145, additionalRate: 0.009, additionalThreshold: 200000 },
19
+ },
20
+ };
21
+
22
+ describe('USAIncomeTaxServiceImpl', () => {
23
+ it('returns zero income tax for income at or below the standard deduction', () => {
24
+ const service = new USAIncomeTaxServiceImpl(14600, usaRules);
25
+ const result = service.calculateNetIncome();
26
+
27
+ expect(result.incomeTax).toBe(0);
28
+ expect(result.grossIncome).toBe(14600);
29
+ expect(result.standardDeduction).toBe(14600);
30
+ });
31
+
32
+ it('correctly calculates income tax for $50,000 income', () => {
33
+ const service = new USAIncomeTaxServiceImpl(50000, usaRules);
34
+ const result = service.calculateNetIncome();
35
+
36
+ // Standard deduction = $14,600
37
+ // Taxable income = 50000 - 14600 = 35400
38
+ // 10%: 11600 * 0.10 = 1160
39
+ // 12%: (35400 - 11600) * 0.12 = 23800 * 0.12 = 2856
40
+ // Total = 4016
41
+ expect(result.incomeTax).toBe(4016);
42
+ expect(result.standardDeduction).toBe(14600);
43
+ });
44
+
45
+ it('correctly calculates income tax for $100,000 income', () => {
46
+ const service = new USAIncomeTaxServiceImpl(100000, usaRules);
47
+ const result = service.calculateNetIncome();
48
+
49
+ // Taxable income = 100000 - 14600 = 85400
50
+ // 10%: 11600 * 0.10 = 1160
51
+ // 12%: (47150 - 11600) * 0.12 = 35550 * 0.12 = 4266
52
+ // 22%: (85400 - 47150) * 0.22 = 38250 * 0.22 = 8415
53
+ // Total = 13841
54
+ expect(result.incomeTax).toBe(13841);
55
+ });
56
+
57
+ it('correctly calculates income tax for $200,000 income', () => {
58
+ const service = new USAIncomeTaxServiceImpl(200000, usaRules);
59
+ const result = service.calculateNetIncome();
60
+
61
+ // Taxable income = 200000 - 14600 = 185400
62
+ // 10%: 11600 * 0.10 = 1160
63
+ // 12%: (47150 - 11600) * 0.12 = 4266
64
+ // 22%: (100525 - 47150) * 0.22 = 11742.50
65
+ // 24%: (185400 - 100525) * 0.24 = 84875 * 0.24 = 20370
66
+ // Total = 37538.50
67
+ expect(result.incomeTax).toBe(37538.50);
68
+ });
69
+
70
+ it('calculates Social Security correctly up to wage base', () => {
71
+ const service = new USAIncomeTaxServiceImpl(50000, usaRules);
72
+ const result = service.calculateNetIncome();
73
+
74
+ // Social Security: 50000 * 6.2% = 3100
75
+ expect(result.socialSecurity).toBeCloseTo(3100, 2);
76
+ });
77
+
78
+ it('caps Social Security at the wage base', () => {
79
+ const service = new USAIncomeTaxServiceImpl(200000, usaRules);
80
+ const result = service.calculateNetIncome();
81
+
82
+ // Social Security: 168600 * 6.2% = 10453.20
83
+ expect(result.socialSecurity).toBeCloseTo(10453.20, 2);
84
+ });
85
+
86
+ it('calculates Medicare without additional tax below $200,000 threshold', () => {
87
+ const service = new USAIncomeTaxServiceImpl(100000, usaRules);
88
+ const result = service.calculateNetIncome();
89
+
90
+ // Medicare: 100000 * 1.45% = 1450
91
+ expect(result.medicare).toBeCloseTo(1450, 2);
92
+ });
93
+
94
+ it('applies additional Medicare tax above $200,000 threshold', () => {
95
+ const service = new USAIncomeTaxServiceImpl(250000, usaRules);
96
+ const result = service.calculateNetIncome();
97
+
98
+ // Medicare: 250000 * 1.45% + (250000 - 200000) * 0.9% = 3625 + 450 = 4075
99
+ expect(result.medicare).toBeCloseTo(4075, 2);
100
+ });
101
+
102
+ it('net income equals gross income minus total deductions', () => {
103
+ const service = new USAIncomeTaxServiceImpl(100000, usaRules);
104
+ const result = service.calculateNetIncome();
105
+
106
+ expect(result.netIncome).toBeCloseTo(
107
+ result.grossIncome - result.totalDeductions,
108
+ 2,
109
+ );
110
+ });
111
+
112
+ it('total deductions equals income tax plus FICA', () => {
113
+ const service = new USAIncomeTaxServiceImpl(100000, usaRules);
114
+ const result = service.calculateNetIncome();
115
+
116
+ expect(result.totalDeductions).toBeCloseTo(
117
+ result.incomeTax + result.socialSecurity + result.medicare,
118
+ 2,
119
+ );
120
+ });
121
+
122
+ it('effective tax rate is income tax divided by gross income', () => {
123
+ const service = new USAIncomeTaxServiceImpl(100000, usaRules);
124
+ const result = service.calculateNetIncome();
125
+
126
+ expect(result.effectiveTaxRate).toBeCloseTo(result.incomeTax / result.grossIncome, 4);
127
+ });
128
+
129
+ it('returns zero tax for zero income', () => {
130
+ const service = new USAIncomeTaxServiceImpl(0, usaRules);
131
+ const result = service.calculateNetIncome();
132
+
133
+ expect(result.incomeTax).toBe(0);
134
+ expect(result.socialSecurity).toBe(0);
135
+ expect(result.medicare).toBe(0);
136
+ expect(result.effectiveTaxRate).toBe(0);
137
+ });
138
+ });
@@ -0,0 +1,97 @@
1
+ import { USAMortgageServiceImpl } from '../src/mortgage/usa/USAMortgageServiceImpl';
2
+ import { MortgageInput, MortgageRules } from '../src/mortgage/usa/domain/types';
3
+
4
+ // USA federal mortgage rules (no federal transfer tax)
5
+ const usaMortgageRules: MortgageRules = {
6
+ loanConstraints: {
7
+ maxLtvPercent: 97,
8
+ maxAmortizationYears: 30,
9
+ },
10
+ interest: {
11
+ compounding: 'MONTHLY',
12
+ },
13
+ transferTax: {
14
+ brackets: [
15
+ { above: 0, rate: 0 },
16
+ ],
17
+ },
18
+ };
19
+
20
+ const defaultInput: MortgageInput = {
21
+ propertyPrice: 400000,
22
+ downPayment: 80000, // 20% down
23
+ annualInterestRate: 5.0,
24
+ amortizationYears: 30,
25
+ isFirstTimeBuyer: false,
26
+ };
27
+
28
+ describe('USAMortgageServiceImpl', () => {
29
+ const service = new USAMortgageServiceImpl();
30
+
31
+ it('calculates loan amount correctly', () => {
32
+ const result = service.calculate(defaultInput, usaMortgageRules);
33
+
34
+ expect(result.loanAmount).toBe(320000);
35
+ });
36
+
37
+ it('calculates monthly payment using standard amortization formula', () => {
38
+ const result = service.calculate(defaultInput, usaMortgageRules);
39
+
40
+ const r = 0.05 / 12;
41
+ const n = 30 * 12;
42
+ const expected = 320000 * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
43
+ expect(result.monthlyPayment).toBeCloseTo(expected, 2);
44
+ });
45
+
46
+ it('totalPaid equals monthlyPayment * totalPayments', () => {
47
+ const result = service.calculate(defaultInput, usaMortgageRules);
48
+ const totalPayments = 30 * 12;
49
+
50
+ expect(result.totalPaid).toBeCloseTo(result.monthlyPayment * totalPayments, 0);
51
+ });
52
+
53
+ it('calculates zero federal transfer tax', () => {
54
+ const result = service.calculate(defaultInput, usaMortgageRules);
55
+
56
+ expect(result.transferTax).toBe(0);
57
+ });
58
+
59
+ it('generates amortization schedule with correct length', () => {
60
+ const result = service.calculate(defaultInput, usaMortgageRules);
61
+
62
+ expect(result.amortizationSchedule.length).toBe(30);
63
+ expect(result.amortizationSchedule[0].year).toBe(1);
64
+ expect(result.amortizationSchedule[29].balance).toBeCloseTo(0, 0);
65
+ });
66
+
67
+ it('amortization schedule balance decreases over time', () => {
68
+ const result = service.calculate(defaultInput, usaMortgageRules);
69
+ const schedule = result.amortizationSchedule;
70
+
71
+ for (let i = 1; i < schedule.length; i++) {
72
+ expect(schedule[i].balance).toBeLessThan(schedule[i - 1].balance);
73
+ }
74
+ });
75
+
76
+ it('throws error when loan amount is zero or negative', () => {
77
+ const input: MortgageInput = {
78
+ ...defaultInput,
79
+ downPayment: 400000,
80
+ };
81
+
82
+ expect(() => service.calculate(input, usaMortgageRules)).toThrow('Invalid loan amount');
83
+ });
84
+
85
+ it('otherFees includes transfer tax label', () => {
86
+ const result = service.calculate(defaultInput, usaMortgageRules);
87
+
88
+ expect(result.otherFees.notaryFees.label).toBe('TRANSFER_TAX');
89
+ expect(result.otherFees.notaryFees.value).toBe(result.transferTax);
90
+ });
91
+
92
+ it('totalInterestPaid equals totalPaid minus loanAmount', () => {
93
+ const result = service.calculate(defaultInput, usaMortgageRules);
94
+
95
+ expect(result.totalInterestPaid).toBeCloseTo(result.totalPaid - result.loanAmount, 2);
96
+ });
97
+ });