@novha/calc-engines 3.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 (140) 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/corporate/germany/GermanyCorporateTaxService.js +3 -0
  24. package/dist/corporate/germany/GermanyCorporateTaxServiceImpl.js +56 -0
  25. package/dist/corporate/germany/domain/types.js +3 -0
  26. package/dist/corporate/usa/USACorporateTaxService.js +3 -0
  27. package/dist/corporate/usa/USACorporateTaxServiceImpl.js +29 -0
  28. package/dist/corporate/usa/domain/types.js +3 -0
  29. package/dist/income-tax/germany/GermanyIncomeTaxService.js +3 -0
  30. package/dist/income-tax/germany/GermanyIncomeTaxServiceImpl.js +86 -0
  31. package/dist/income-tax/germany/domain/types.js +3 -0
  32. package/dist/income-tax/usa/USAIncomeTaxService.js +3 -0
  33. package/dist/income-tax/usa/USAIncomeTaxServiceImpl.js +87 -0
  34. package/dist/income-tax/usa/domain/types.js +3 -0
  35. package/dist/index.js +37 -2
  36. package/dist/mortgage/germany/GermanyMortgageService.js +3 -0
  37. package/dist/mortgage/germany/GermanyMortgageServiceImpl.js +80 -0
  38. package/dist/mortgage/germany/domain/types.js +3 -0
  39. package/dist/mortgage/usa/USAMortgageService.js +3 -0
  40. package/dist/mortgage/usa/USAMortgageServiceImpl.js +92 -0
  41. package/dist/mortgage/usa/domain/types.js +3 -0
  42. package/dist/shared/domain/types.js +2 -1
  43. package/dist/types/capital-gains/australia/AustraliaCapitalGainsService.d.ts +4 -0
  44. package/dist/types/capital-gains/australia/AustraliaCapitalGainsServiceImpl.d.ts +9 -0
  45. package/dist/types/capital-gains/australia/domain/types.d.ts +22 -0
  46. package/dist/types/capital-gains/canada/CanadaCapitalGainsService.d.ts +4 -0
  47. package/dist/types/capital-gains/canada/CanadaCapitalGainsServiceImpl.d.ts +9 -0
  48. package/dist/types/capital-gains/canada/domain/types.d.ts +20 -0
  49. package/dist/types/capital-gains/domain/types.d.ts +6 -0
  50. package/dist/types/capital-gains/france/FranceCapitalGainsService.d.ts +4 -0
  51. package/dist/types/capital-gains/france/FranceCapitalGainsServiceImpl.d.ts +8 -0
  52. package/dist/types/capital-gains/france/domain/types.d.ts +15 -0
  53. package/dist/types/capital-gains/germany/GermanyCapitalGainsService.d.ts +4 -0
  54. package/dist/types/capital-gains/germany/GermanyCapitalGainsServiceImpl.d.ts +8 -0
  55. package/dist/types/capital-gains/germany/domain/types.d.ts +17 -0
  56. package/dist/types/capital-gains/south-africa/SouthAfricaCapitalGainsService.d.ts +4 -0
  57. package/dist/types/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.d.ts +9 -0
  58. package/dist/types/capital-gains/south-africa/domain/types.d.ts +21 -0
  59. package/dist/types/capital-gains/uk/UKCapitalGainsService.d.ts +4 -0
  60. package/dist/types/capital-gains/uk/UKCapitalGainsServiceImpl.d.ts +8 -0
  61. package/dist/types/capital-gains/uk/domain/types.d.ts +17 -0
  62. package/dist/types/capital-gains/usa/USACapitalGainsService.d.ts +4 -0
  63. package/dist/types/capital-gains/usa/USACapitalGainsServiceImpl.d.ts +9 -0
  64. package/dist/types/capital-gains/usa/domain/types.d.ts +26 -0
  65. package/dist/types/corporate/germany/GermanyCorporateTaxService.d.ts +4 -0
  66. package/dist/types/corporate/germany/GermanyCorporateTaxServiceImpl.d.ts +8 -0
  67. package/dist/types/corporate/germany/domain/types.d.ts +24 -0
  68. package/dist/types/corporate/usa/USACorporateTaxService.d.ts +4 -0
  69. package/dist/types/corporate/usa/USACorporateTaxServiceImpl.d.ts +8 -0
  70. package/dist/types/corporate/usa/domain/types.d.ts +15 -0
  71. package/dist/types/income-tax/germany/GermanyIncomeTaxService.d.ts +4 -0
  72. package/dist/types/income-tax/germany/GermanyIncomeTaxServiceImpl.d.ts +12 -0
  73. package/dist/types/income-tax/germany/domain/types.d.ts +23 -0
  74. package/dist/types/income-tax/usa/USAIncomeTaxService.d.ts +4 -0
  75. package/dist/types/income-tax/usa/USAIncomeTaxServiceImpl.d.ts +11 -0
  76. package/dist/types/income-tax/usa/domain/types.d.ts +30 -0
  77. package/dist/types/index.d.ts +26 -0
  78. package/dist/types/mortgage/germany/GermanyMortgageService.d.ts +4 -0
  79. package/dist/types/mortgage/germany/GermanyMortgageServiceImpl.d.ts +7 -0
  80. package/dist/types/mortgage/germany/domain/types.d.ts +42 -0
  81. package/dist/types/mortgage/usa/USAMortgageService.d.ts +4 -0
  82. package/dist/types/mortgage/usa/USAMortgageServiceImpl.d.ts +8 -0
  83. package/dist/types/mortgage/usa/domain/types.d.ts +44 -0
  84. package/dist/types/shared/domain/types.d.ts +2 -1
  85. package/package.json +1 -1
  86. package/src/capital-gains/australia/AustraliaCapitalGainsService.ts +5 -0
  87. package/src/capital-gains/australia/AustraliaCapitalGainsServiceImpl.ts +73 -0
  88. package/src/capital-gains/australia/domain/types.ts +26 -0
  89. package/src/capital-gains/canada/CanadaCapitalGainsService.ts +5 -0
  90. package/src/capital-gains/canada/CanadaCapitalGainsServiceImpl.ts +71 -0
  91. package/src/capital-gains/canada/domain/types.ts +24 -0
  92. package/src/capital-gains/domain/types.ts +6 -0
  93. package/src/capital-gains/france/FranceCapitalGainsService.ts +5 -0
  94. package/src/capital-gains/france/FranceCapitalGainsServiceImpl.ts +48 -0
  95. package/src/capital-gains/france/domain/types.ts +18 -0
  96. package/src/capital-gains/germany/GermanyCapitalGainsService.ts +5 -0
  97. package/src/capital-gains/germany/GermanyCapitalGainsServiceImpl.ts +74 -0
  98. package/src/capital-gains/germany/domain/types.ts +20 -0
  99. package/src/capital-gains/south-africa/SouthAfricaCapitalGainsService.ts +5 -0
  100. package/src/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.ts +87 -0
  101. package/src/capital-gains/south-africa/domain/types.ts +25 -0
  102. package/src/capital-gains/uk/UKCapitalGainsService.ts +5 -0
  103. package/src/capital-gains/uk/UKCapitalGainsServiceImpl.ts +84 -0
  104. package/src/capital-gains/uk/domain/types.ts +20 -0
  105. package/src/capital-gains/usa/USACapitalGainsService.ts +5 -0
  106. package/src/capital-gains/usa/USACapitalGainsServiceImpl.ts +86 -0
  107. package/src/capital-gains/usa/domain/types.ts +27 -0
  108. package/src/corporate/germany/GermanyCorporateTaxService.ts +5 -0
  109. package/src/corporate/germany/GermanyCorporateTaxServiceImpl.ts +64 -0
  110. package/src/corporate/germany/domain/types.ts +20 -0
  111. package/src/corporate/usa/USACorporateTaxService.ts +5 -0
  112. package/src/corporate/usa/USACorporateTaxServiceImpl.ts +35 -0
  113. package/src/corporate/usa/domain/types.ts +15 -0
  114. package/src/income-tax/germany/GermanyIncomeTaxService.ts +5 -0
  115. package/src/income-tax/germany/GermanyIncomeTaxServiceImpl.ts +121 -0
  116. package/src/income-tax/germany/domain/types.ts +27 -0
  117. package/src/income-tax/usa/USAIncomeTaxService.ts +5 -0
  118. package/src/income-tax/usa/USAIncomeTaxServiceImpl.ts +113 -0
  119. package/src/income-tax/usa/domain/types.ts +24 -0
  120. package/src/index.ts +98 -0
  121. package/src/mortgage/germany/GermanyMortgageService.ts +5 -0
  122. package/src/mortgage/germany/GermanyMortgageServiceImpl.ts +111 -0
  123. package/src/mortgage/germany/domain/types.ts +49 -0
  124. package/src/mortgage/usa/USAMortgageService.ts +5 -0
  125. package/src/mortgage/usa/USAMortgageServiceImpl.ts +134 -0
  126. package/src/mortgage/usa/domain/types.ts +52 -0
  127. package/src/shared/domain/types.ts +1 -0
  128. package/test/australia-capital-gains.test.ts +80 -0
  129. package/test/canada-capital-gains.test.ts +71 -0
  130. package/test/france-capital-gains.test.ts +66 -0
  131. package/test/germany-capital-gains.test.ts +85 -0
  132. package/test/germany-corporate-tax.test.ts +77 -0
  133. package/test/germany-income-tax.test.ts +114 -0
  134. package/test/germany-mortgage.test.ts +105 -0
  135. package/test/south-africa-capital-gains.test.ts +84 -0
  136. package/test/uk-capital-gains.test.ts +79 -0
  137. package/test/usa-capital-gains.test.ts +88 -0
  138. package/test/usa-corporate-tax.test.ts +59 -0
  139. package/test/usa-income-tax.test.ts +138 -0
  140. package/test/usa-mortgage.test.ts +97 -0
package/src/index.ts CHANGED
@@ -102,5 +102,103 @@ export {
102
102
  MortgageOutput as UKMortgageOutput,
103
103
  } from './mortgage/uk/domain/types';
104
104
 
105
+ // USA
106
+ export { USAIncomeTaxServiceImpl as USAIncomeTaxService } from './income-tax/usa/USAIncomeTaxServiceImpl';
107
+ export {
108
+ ComputedIncomeTaxValues as USAComputedIncomeTaxValues,
109
+ IncomeTaxRules as USAIncomeTaxRules,
110
+ } from './income-tax/usa/domain/types';
111
+
112
+ export { USAMortgageServiceImpl as USAMortgageService } from './mortgage/usa/USAMortgageServiceImpl';
113
+ export {
114
+ MortgageRules as USAMortgageRules,
115
+ MortgageInput as USAMortgageInput,
116
+ MortgageOutput as USAMortgageOutput,
117
+ } from './mortgage/usa/domain/types';
118
+
119
+ export { USACorporateTaxServiceImpl as USACorporateTaxService } from './corporate/usa/USACorporateTaxServiceImpl';
120
+ export {
121
+ Input as USACorporateTaxInput,
122
+ Rules as USACorporateTaxRules,
123
+ Result as USACorporateTaxResult,
124
+ } from './corporate/usa/domain/types';
125
+
126
+ // Germany
127
+ export { GermanyIncomeTaxServiceImpl as GermanyIncomeTaxService } from './income-tax/germany/GermanyIncomeTaxServiceImpl';
128
+ export {
129
+ ComputedIncomeTaxValues as GermanyComputedIncomeTaxValues,
130
+ IncomeTaxRules as GermanyIncomeTaxRules,
131
+ } from './income-tax/germany/domain/types';
132
+
133
+ export { GermanyMortgageServiceImpl as GermanyMortgageService } from './mortgage/germany/GermanyMortgageServiceImpl';
134
+ export {
135
+ MortgageRules as GermanyMortgageRules,
136
+ MortgageInput as GermanyMortgageInput,
137
+ MortgageOutput as GermanyMortgageOutput,
138
+ } from './mortgage/germany/domain/types';
139
+
140
+ export { GermanyCorporateTaxServiceImpl as GermanyCorporateTaxService } from './corporate/germany/GermanyCorporateTaxServiceImpl';
141
+ export {
142
+ Input as GermanyCorporateTaxInput,
143
+ Rules as GermanyCorporateTaxRules,
144
+ Result as GermanyCorporateTaxResult,
145
+ } from './corporate/germany/domain/types';
146
+
147
+ // Capital Gains - Canada
148
+ export { CanadaCapitalGainsServiceImpl as CanadaCapitalGainsService } from './capital-gains/canada/CanadaCapitalGainsServiceImpl';
149
+ export {
150
+ Input as CanadaCapitalGainsInput,
151
+ Rules as CanadaCapitalGainsRules,
152
+ Result as CanadaCapitalGainsResult,
153
+ } from './capital-gains/canada/domain/types';
154
+
155
+ // Capital Gains - France
156
+ export { FranceCapitalGainsServiceImpl as FranceCapitalGainsService } from './capital-gains/france/FranceCapitalGainsServiceImpl';
157
+ export {
158
+ Input as FranceCapitalGainsInput,
159
+ Rules as FranceCapitalGainsRules,
160
+ Result as FranceCapitalGainsResult,
161
+ } from './capital-gains/france/domain/types';
162
+
163
+ // Capital Gains - South Africa
164
+ export { SouthAfricaCapitalGainsServiceImpl as SouthAfricaCapitalGainsService } from './capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl';
165
+ export {
166
+ Input as SouthAfricaCapitalGainsInput,
167
+ Rules as SouthAfricaCapitalGainsRules,
168
+ Result as SouthAfricaCapitalGainsResult,
169
+ } from './capital-gains/south-africa/domain/types';
170
+
171
+ // Capital Gains - Australia
172
+ export { AustraliaCapitalGainsServiceImpl as AustraliaCapitalGainsService } from './capital-gains/australia/AustraliaCapitalGainsServiceImpl';
173
+ export {
174
+ Input as AustraliaCapitalGainsInput,
175
+ Rules as AustraliaCapitalGainsRules,
176
+ Result as AustraliaCapitalGainsResult,
177
+ } from './capital-gains/australia/domain/types';
178
+
179
+ // Capital Gains - UK
180
+ export { UKCapitalGainsServiceImpl as UKCapitalGainsService } from './capital-gains/uk/UKCapitalGainsServiceImpl';
181
+ export {
182
+ Input as UKCapitalGainsInput,
183
+ Rules as UKCapitalGainsRules,
184
+ Result as UKCapitalGainsResult,
185
+ } from './capital-gains/uk/domain/types';
186
+
187
+ // Capital Gains - USA
188
+ export { USACapitalGainsServiceImpl as USACapitalGainsService } from './capital-gains/usa/USACapitalGainsServiceImpl';
189
+ export {
190
+ Input as USACapitalGainsInput,
191
+ Rules as USACapitalGainsRules,
192
+ Result as USACapitalGainsResult,
193
+ } from './capital-gains/usa/domain/types';
194
+
195
+ // Capital Gains - Germany
196
+ export { GermanyCapitalGainsServiceImpl as GermanyCapitalGainsService } from './capital-gains/germany/GermanyCapitalGainsServiceImpl';
197
+ export {
198
+ Input as GermanyCapitalGainsInput,
199
+ Rules as GermanyCapitalGainsRules,
200
+ Result as GermanyCapitalGainsResult,
201
+ } from './capital-gains/germany/domain/types';
202
+
105
203
  export { IncomeTaxCalculatorSchema } from './income-tax/domain/types';
106
204
  export * from './income-tax/domain/types';
@@ -0,0 +1,5 @@
1
+ import { MortgageInput, MortgageOutput, MortgageRules } from "./domain/types";
2
+
3
+ export interface GermanyMortgageService {
4
+ calculate(input: MortgageInput, rules: MortgageRules): MortgageOutput;
5
+ }
@@ -0,0 +1,111 @@
1
+ import { GermanyMortgageService } from "./GermanyMortgageService";
2
+ import {
3
+ AmortizationScheduleItem,
4
+ MortgageInput,
5
+ MortgageOutput,
6
+ MortgageRules,
7
+ } from "./domain/types";
8
+
9
+ export class GermanyMortgageServiceImpl implements GermanyMortgageService {
10
+
11
+ public calculate(input: MortgageInput, rules: MortgageRules): MortgageOutput {
12
+ const loanAmount = input.propertyPrice - input.downPayment;
13
+
14
+ if (loanAmount <= 0) {
15
+ throw new Error('Invalid loan amount: down payment must be less than property price');
16
+ }
17
+
18
+ const totalPayments = input.amortizationYears * 12;
19
+ const monthlyRate = (input.annualInterestRate / 100) / 12;
20
+
21
+ const monthlyPayment = this.calculatePayment(loanAmount, monthlyRate, totalPayments);
22
+ const totalPaid = monthlyPayment * totalPayments;
23
+ const totalInterestPaid = totalPaid - loanAmount;
24
+
25
+ const landTransferTax = input.propertyPrice * rules.landTransferTax.rate;
26
+ const notaryFees = input.propertyPrice * rules.notaryFeeRate;
27
+ const registrationFees = input.propertyPrice * rules.registrationFeeRate;
28
+
29
+ const amortizationSchedule = this.calculateAmortizationSchedule(
30
+ loanAmount,
31
+ monthlyRate,
32
+ monthlyPayment,
33
+ totalPayments,
34
+ input.amortizationYears,
35
+ );
36
+
37
+ return {
38
+ loanAmount,
39
+ totalMortgage: loanAmount,
40
+ monthlyPayment,
41
+ totalInterestPaid,
42
+ totalPaid,
43
+ landTransferTax,
44
+ notaryFees,
45
+ registrationFees,
46
+ amortizationSchedule,
47
+ otherFees: {
48
+ notaryFees: {
49
+ value: notaryFees,
50
+ label: 'NOTARY_FEES',
51
+ },
52
+ bankFees: {
53
+ value: registrationFees,
54
+ label: 'REGISTRATION_FEES',
55
+ },
56
+ monthlyInsuranceFees: {
57
+ value: 0,
58
+ label: 'MONTHLY_INSURANCE_FEES',
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ private calculateAmortizationSchedule(
65
+ principal: number,
66
+ monthlyRate: number,
67
+ monthlyPayment: number,
68
+ totalPayments: number,
69
+ amortizationYears: number,
70
+ ): AmortizationScheduleItem[] {
71
+ const schedule: AmortizationScheduleItem[] = [];
72
+ let balance = principal;
73
+
74
+ for (let year = 1; year <= amortizationYears; year++) {
75
+ let yearlyPrincipal = 0;
76
+ let yearlyInterest = 0;
77
+
78
+ const paymentsInYear = Math.min(12, totalPayments - (year - 1) * 12);
79
+
80
+ for (let payment = 1; payment <= paymentsInYear; payment++) {
81
+ const interestPayment = balance * monthlyRate;
82
+ const principalPayment = monthlyPayment - interestPayment;
83
+
84
+ yearlyInterest += interestPayment;
85
+ yearlyPrincipal += principalPayment;
86
+ balance -= principalPayment;
87
+ }
88
+
89
+ schedule.push({
90
+ year,
91
+ principal: yearlyPrincipal,
92
+ interest: yearlyInterest,
93
+ balance: Math.max(0, balance),
94
+ });
95
+
96
+ if (balance <= 0) break;
97
+ }
98
+
99
+ return schedule;
100
+ }
101
+
102
+ private calculatePayment(principal: number, rate: number, periods: number): number {
103
+ if (rate === 0) {
104
+ return principal / periods;
105
+ }
106
+
107
+ return principal *
108
+ (rate * Math.pow(1 + rate, periods)) /
109
+ (Math.pow(1 + rate, periods) - 1);
110
+ }
111
+ }
@@ -0,0 +1,49 @@
1
+ import { OtherFees } from "../../domain/types";
2
+
3
+ export interface MortgageRules {
4
+ loanConstraints: LoanConstraints;
5
+ interest: InterestRules;
6
+ landTransferTax: LandTransferTaxRules;
7
+ notaryFeeRate: number;
8
+ registrationFeeRate: number;
9
+ }
10
+
11
+ export interface LoanConstraints {
12
+ maxLtvPercent: number;
13
+ maxAmortizationYears: number;
14
+ }
15
+
16
+ export interface InterestRules {
17
+ compounding: 'MONTHLY';
18
+ }
19
+
20
+ export interface LandTransferTaxRules {
21
+ rate: number;
22
+ }
23
+
24
+ export interface AmortizationScheduleItem {
25
+ year: number;
26
+ principal: number;
27
+ interest: number;
28
+ balance: number;
29
+ }
30
+
31
+ export interface MortgageInput {
32
+ propertyPrice: number;
33
+ downPayment: number;
34
+ annualInterestRate: number;
35
+ amortizationYears: number;
36
+ }
37
+
38
+ export interface MortgageOutput {
39
+ loanAmount: number;
40
+ totalMortgage: number;
41
+ monthlyPayment: number;
42
+ totalInterestPaid: number;
43
+ totalPaid: number;
44
+ landTransferTax: number;
45
+ notaryFees: number;
46
+ registrationFees: number;
47
+ amortizationSchedule: AmortizationScheduleItem[];
48
+ otherFees: OtherFees;
49
+ }
@@ -0,0 +1,5 @@
1
+ import { MortgageInput, MortgageOutput, MortgageRules } from "./domain/types";
2
+
3
+ export interface USAMortgageService {
4
+ calculate(input: MortgageInput, rules: MortgageRules): MortgageOutput;
5
+ }
@@ -0,0 +1,134 @@
1
+ import { USAMortgageService } from "./USAMortgageService";
2
+ import {
3
+ AmortizationScheduleItem,
4
+ MortgageInput,
5
+ MortgageOutput,
6
+ MortgageRules,
7
+ TransferTaxBracket,
8
+ } from "./domain/types";
9
+
10
+ export class USAMortgageServiceImpl implements USAMortgageService {
11
+
12
+ public calculate(input: MortgageInput, rules: MortgageRules): MortgageOutput {
13
+ const loanAmount = input.propertyPrice - input.downPayment;
14
+
15
+ if (loanAmount <= 0) {
16
+ throw new Error('Invalid loan amount: down payment must be less than property price');
17
+ }
18
+
19
+ const totalPayments = input.amortizationYears * 12;
20
+ const monthlyRate = (input.annualInterestRate / 100) / 12;
21
+
22
+ const monthlyPayment = this.calculatePayment(loanAmount, monthlyRate, totalPayments);
23
+ const totalPaid = monthlyPayment * totalPayments;
24
+ const totalInterestPaid = totalPaid - loanAmount;
25
+
26
+ const transferTax = this.calculateTransferTax(
27
+ input.propertyPrice,
28
+ rules.transferTax.brackets,
29
+ );
30
+
31
+ const amortizationSchedule = this.calculateAmortizationSchedule(
32
+ loanAmount,
33
+ monthlyRate,
34
+ monthlyPayment,
35
+ totalPayments,
36
+ input.amortizationYears,
37
+ );
38
+
39
+ return {
40
+ loanAmount,
41
+ totalMortgage: loanAmount,
42
+ monthlyPayment,
43
+ totalInterestPaid,
44
+ totalPaid,
45
+ transferTax,
46
+ amortizationSchedule,
47
+ otherFees: {
48
+ notaryFees: {
49
+ value: transferTax,
50
+ label: 'TRANSFER_TAX',
51
+ },
52
+ bankFees: {
53
+ value: 0,
54
+ label: 'BANK_FEES',
55
+ },
56
+ monthlyInsuranceFees: {
57
+ value: 0,
58
+ label: 'MONTHLY_INSURANCE_FEES',
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ private calculateTransferTax(
65
+ propertyPrice: number,
66
+ brackets: TransferTaxBracket[],
67
+ ): number {
68
+ let tax = 0;
69
+ let previousLimit = 0;
70
+
71
+ for (const bracket of brackets) {
72
+ if (bracket.upTo !== undefined && propertyPrice > previousLimit) {
73
+ const taxable = Math.min(propertyPrice, bracket.upTo) - previousLimit;
74
+ tax += taxable * bracket.rate;
75
+ previousLimit = bracket.upTo;
76
+ }
77
+
78
+ if (bracket.above !== undefined && propertyPrice > bracket.above) {
79
+ tax += (propertyPrice - bracket.above) * bracket.rate;
80
+ break;
81
+ }
82
+ }
83
+
84
+ return tax;
85
+ }
86
+
87
+ private calculateAmortizationSchedule(
88
+ principal: number,
89
+ monthlyRate: number,
90
+ monthlyPayment: number,
91
+ totalPayments: number,
92
+ amortizationYears: number,
93
+ ): AmortizationScheduleItem[] {
94
+ const schedule: AmortizationScheduleItem[] = [];
95
+ let balance = principal;
96
+
97
+ for (let year = 1; year <= amortizationYears; year++) {
98
+ let yearlyPrincipal = 0;
99
+ let yearlyInterest = 0;
100
+
101
+ const paymentsInYear = Math.min(12, totalPayments - (year - 1) * 12);
102
+
103
+ for (let payment = 1; payment <= paymentsInYear; payment++) {
104
+ const interestPayment = balance * monthlyRate;
105
+ const principalPayment = monthlyPayment - interestPayment;
106
+
107
+ yearlyInterest += interestPayment;
108
+ yearlyPrincipal += principalPayment;
109
+ balance -= principalPayment;
110
+ }
111
+
112
+ schedule.push({
113
+ year,
114
+ principal: yearlyPrincipal,
115
+ interest: yearlyInterest,
116
+ balance: Math.max(0, balance),
117
+ });
118
+
119
+ if (balance <= 0) break;
120
+ }
121
+
122
+ return schedule;
123
+ }
124
+
125
+ private calculatePayment(principal: number, rate: number, periods: number): number {
126
+ if (rate === 0) {
127
+ return principal / periods;
128
+ }
129
+
130
+ return principal *
131
+ (rate * Math.pow(1 + rate, periods)) /
132
+ (Math.pow(1 + rate, periods) - 1);
133
+ }
134
+ }
@@ -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
+ }
@@ -3,6 +3,7 @@ export enum CalculatorType {
3
3
  MORTGAGE = 'MORTGAGE',
4
4
  LOAN = 'LOAN',
5
5
  CORPORATE_TAX = 'CORPORATE_TAX',
6
+ CAPITAL_GAINS = 'CAPITAL_GAINS',
6
7
  }
7
8
 
8
9
  export interface RuleMeta {
@@ -0,0 +1,80 @@
1
+ import { AustraliaCapitalGainsServiceImpl } from '../src/capital-gains/australia/AustraliaCapitalGainsServiceImpl';
2
+ import { Input, Rules } from '../src/capital-gains/australia/domain/types';
3
+
4
+ // Australia 2024-25 individual marginal tax rates + 50% CGT discount
5
+ const australiaCapitalGainsRules: Rules = {
6
+ cgtDiscount: 0.50,
7
+ cgtDiscountMinMonths: 12,
8
+ taxBrackets: [
9
+ { from: 0, to: 18200, rate: 0.00 },
10
+ { from: 18200, to: 45000, rate: 0.16 },
11
+ { from: 45000, to: 135000, rate: 0.30 },
12
+ { from: 135000, to: 190000, rate: 0.37 },
13
+ { from: 190000, to: null, rate: 0.45 },
14
+ ],
15
+ };
16
+
17
+ describe('AustraliaCapitalGainsServiceImpl', () => {
18
+ it('applies 50% CGT discount for assets held >= 12 months', () => {
19
+ const input: Input = { capitalGain: 100000, totalTaxableIncome: 100000, holdingPeriodMonths: 24 };
20
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
21
+ const result = service.calculate();
22
+
23
+ expect(result.taxableGain).toBe(50000);
24
+ });
25
+
26
+ it('no discount for assets held < 12 months', () => {
27
+ const input: Input = { capitalGain: 100000, totalTaxableIncome: 100000, holdingPeriodMonths: 6 };
28
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
29
+ const result = service.calculate();
30
+
31
+ expect(result.taxableGain).toBe(100000);
32
+ });
33
+
34
+ it('taxes discounted gain at marginal rates', () => {
35
+ const input: Input = { capitalGain: 40000, totalTaxableIncome: 40000, holdingPeriodMonths: 18 };
36
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
37
+ const result = service.calculate();
38
+
39
+ // Taxable gain = 20000, no other income
40
+ // 18200 at 0% = 0, 1800 at 16% = 288
41
+ expect(result.taxableGain).toBe(20000);
42
+ expect(result.capitalGainsTax).toBeCloseTo(288, 2);
43
+ });
44
+
45
+ it('returns zero for zero gain', () => {
46
+ const input: Input = { capitalGain: 0, totalTaxableIncome: 50000, holdingPeriodMonths: 24 };
47
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
48
+ const result = service.calculate();
49
+
50
+ expect(result.capitalGainsTax).toBe(0);
51
+ expect(result.breakdowns).toHaveLength(0);
52
+ });
53
+
54
+ it('returns zero for negative gain', () => {
55
+ const input: Input = { capitalGain: -5000, totalTaxableIncome: 50000, holdingPeriodMonths: 24 };
56
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
57
+ const result = service.calculate();
58
+
59
+ expect(result.capitalGainsTax).toBe(0);
60
+ });
61
+
62
+ it('stacks gain on top of other income', () => {
63
+ const input: Input = { capitalGain: 20000, totalTaxableIncome: 70000, holdingPeriodMonths: 24 };
64
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
65
+ const result = service.calculate();
66
+
67
+ // Other income = 50000, taxable gain = 10000 (50% discount)
68
+ // Gain falls in 50000-60000 range, all at 30%
69
+ expect(result.taxableGain).toBe(10000);
70
+ expect(result.capitalGainsTax).toBe(10000 * 0.30);
71
+ });
72
+
73
+ it('calculates effective rate based on full gain', () => {
74
+ const input: Input = { capitalGain: 20000, totalTaxableIncome: 70000, holdingPeriodMonths: 24 };
75
+ const service = new AustraliaCapitalGainsServiceImpl(input, australiaCapitalGainsRules);
76
+ const result = service.calculate();
77
+
78
+ expect(result.effectiveRate).toBe((result.capitalGainsTax / 20000) * 100);
79
+ });
80
+ });
@@ -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
+ });