@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.
- package/dist/capital-gains/australia/AustraliaCapitalGainsService.js +3 -0
- package/dist/capital-gains/australia/AustraliaCapitalGainsServiceImpl.js +56 -0
- package/dist/capital-gains/australia/domain/types.js +3 -0
- package/dist/capital-gains/canada/CanadaCapitalGainsService.js +3 -0
- package/dist/capital-gains/canada/CanadaCapitalGainsServiceImpl.js +55 -0
- package/dist/capital-gains/canada/domain/types.js +3 -0
- package/dist/capital-gains/domain/types.js +3 -0
- package/dist/capital-gains/france/FranceCapitalGainsService.js +3 -0
- package/dist/capital-gains/france/FranceCapitalGainsServiceImpl.js +41 -0
- package/dist/capital-gains/france/domain/types.js +3 -0
- package/dist/capital-gains/germany/GermanyCapitalGainsService.js +3 -0
- package/dist/capital-gains/germany/GermanyCapitalGainsServiceImpl.js +65 -0
- package/dist/capital-gains/germany/domain/types.js +3 -0
- package/dist/capital-gains/south-africa/SouthAfricaCapitalGainsService.js +3 -0
- package/dist/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.js +69 -0
- package/dist/capital-gains/south-africa/domain/types.js +3 -0
- package/dist/capital-gains/uk/UKCapitalGainsService.js +3 -0
- package/dist/capital-gains/uk/UKCapitalGainsServiceImpl.js +72 -0
- package/dist/capital-gains/uk/domain/types.js +3 -0
- package/dist/capital-gains/usa/USACapitalGainsService.js +3 -0
- package/dist/capital-gains/usa/USACapitalGainsServiceImpl.js +67 -0
- package/dist/capital-gains/usa/domain/types.js +3 -0
- package/dist/corporate/germany/GermanyCorporateTaxService.js +3 -0
- package/dist/corporate/germany/GermanyCorporateTaxServiceImpl.js +56 -0
- package/dist/corporate/germany/domain/types.js +3 -0
- package/dist/corporate/usa/USACorporateTaxService.js +3 -0
- package/dist/corporate/usa/USACorporateTaxServiceImpl.js +29 -0
- package/dist/corporate/usa/domain/types.js +3 -0
- package/dist/income-tax/germany/GermanyIncomeTaxService.js +3 -0
- package/dist/income-tax/germany/GermanyIncomeTaxServiceImpl.js +86 -0
- package/dist/income-tax/germany/domain/types.js +3 -0
- package/dist/income-tax/usa/USAIncomeTaxService.js +3 -0
- package/dist/income-tax/usa/USAIncomeTaxServiceImpl.js +87 -0
- package/dist/income-tax/usa/domain/types.js +3 -0
- package/dist/index.js +37 -2
- package/dist/mortgage/germany/GermanyMortgageService.js +3 -0
- package/dist/mortgage/germany/GermanyMortgageServiceImpl.js +80 -0
- package/dist/mortgage/germany/domain/types.js +3 -0
- package/dist/mortgage/usa/USAMortgageService.js +3 -0
- package/dist/mortgage/usa/USAMortgageServiceImpl.js +92 -0
- package/dist/mortgage/usa/domain/types.js +3 -0
- package/dist/shared/domain/types.js +2 -1
- package/dist/types/capital-gains/australia/AustraliaCapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/australia/AustraliaCapitalGainsServiceImpl.d.ts +9 -0
- package/dist/types/capital-gains/australia/domain/types.d.ts +22 -0
- package/dist/types/capital-gains/canada/CanadaCapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/canada/CanadaCapitalGainsServiceImpl.d.ts +9 -0
- package/dist/types/capital-gains/canada/domain/types.d.ts +20 -0
- package/dist/types/capital-gains/domain/types.d.ts +6 -0
- package/dist/types/capital-gains/france/FranceCapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/france/FranceCapitalGainsServiceImpl.d.ts +8 -0
- package/dist/types/capital-gains/france/domain/types.d.ts +15 -0
- package/dist/types/capital-gains/germany/GermanyCapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/germany/GermanyCapitalGainsServiceImpl.d.ts +8 -0
- package/dist/types/capital-gains/germany/domain/types.d.ts +17 -0
- package/dist/types/capital-gains/south-africa/SouthAfricaCapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.d.ts +9 -0
- package/dist/types/capital-gains/south-africa/domain/types.d.ts +21 -0
- package/dist/types/capital-gains/uk/UKCapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/uk/UKCapitalGainsServiceImpl.d.ts +8 -0
- package/dist/types/capital-gains/uk/domain/types.d.ts +17 -0
- package/dist/types/capital-gains/usa/USACapitalGainsService.d.ts +4 -0
- package/dist/types/capital-gains/usa/USACapitalGainsServiceImpl.d.ts +9 -0
- package/dist/types/capital-gains/usa/domain/types.d.ts +26 -0
- package/dist/types/corporate/germany/GermanyCorporateTaxService.d.ts +4 -0
- package/dist/types/corporate/germany/GermanyCorporateTaxServiceImpl.d.ts +8 -0
- package/dist/types/corporate/germany/domain/types.d.ts +24 -0
- package/dist/types/corporate/usa/USACorporateTaxService.d.ts +4 -0
- package/dist/types/corporate/usa/USACorporateTaxServiceImpl.d.ts +8 -0
- package/dist/types/corporate/usa/domain/types.d.ts +15 -0
- package/dist/types/income-tax/germany/GermanyIncomeTaxService.d.ts +4 -0
- package/dist/types/income-tax/germany/GermanyIncomeTaxServiceImpl.d.ts +12 -0
- package/dist/types/income-tax/germany/domain/types.d.ts +23 -0
- package/dist/types/income-tax/usa/USAIncomeTaxService.d.ts +4 -0
- package/dist/types/income-tax/usa/USAIncomeTaxServiceImpl.d.ts +11 -0
- package/dist/types/income-tax/usa/domain/types.d.ts +30 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/mortgage/germany/GermanyMortgageService.d.ts +4 -0
- package/dist/types/mortgage/germany/GermanyMortgageServiceImpl.d.ts +7 -0
- package/dist/types/mortgage/germany/domain/types.d.ts +42 -0
- package/dist/types/mortgage/usa/USAMortgageService.d.ts +4 -0
- package/dist/types/mortgage/usa/USAMortgageServiceImpl.d.ts +8 -0
- package/dist/types/mortgage/usa/domain/types.d.ts +44 -0
- package/dist/types/shared/domain/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/capital-gains/australia/AustraliaCapitalGainsService.ts +5 -0
- package/src/capital-gains/australia/AustraliaCapitalGainsServiceImpl.ts +73 -0
- package/src/capital-gains/australia/domain/types.ts +26 -0
- package/src/capital-gains/canada/CanadaCapitalGainsService.ts +5 -0
- package/src/capital-gains/canada/CanadaCapitalGainsServiceImpl.ts +71 -0
- package/src/capital-gains/canada/domain/types.ts +24 -0
- package/src/capital-gains/domain/types.ts +6 -0
- package/src/capital-gains/france/FranceCapitalGainsService.ts +5 -0
- package/src/capital-gains/france/FranceCapitalGainsServiceImpl.ts +48 -0
- package/src/capital-gains/france/domain/types.ts +18 -0
- package/src/capital-gains/germany/GermanyCapitalGainsService.ts +5 -0
- package/src/capital-gains/germany/GermanyCapitalGainsServiceImpl.ts +74 -0
- package/src/capital-gains/germany/domain/types.ts +20 -0
- package/src/capital-gains/south-africa/SouthAfricaCapitalGainsService.ts +5 -0
- package/src/capital-gains/south-africa/SouthAfricaCapitalGainsServiceImpl.ts +87 -0
- package/src/capital-gains/south-africa/domain/types.ts +25 -0
- package/src/capital-gains/uk/UKCapitalGainsService.ts +5 -0
- package/src/capital-gains/uk/UKCapitalGainsServiceImpl.ts +84 -0
- package/src/capital-gains/uk/domain/types.ts +20 -0
- package/src/capital-gains/usa/USACapitalGainsService.ts +5 -0
- package/src/capital-gains/usa/USACapitalGainsServiceImpl.ts +86 -0
- package/src/capital-gains/usa/domain/types.ts +27 -0
- package/src/corporate/germany/GermanyCorporateTaxService.ts +5 -0
- package/src/corporate/germany/GermanyCorporateTaxServiceImpl.ts +64 -0
- package/src/corporate/germany/domain/types.ts +20 -0
- package/src/corporate/usa/USACorporateTaxService.ts +5 -0
- package/src/corporate/usa/USACorporateTaxServiceImpl.ts +35 -0
- package/src/corporate/usa/domain/types.ts +15 -0
- package/src/income-tax/germany/GermanyIncomeTaxService.ts +5 -0
- package/src/income-tax/germany/GermanyIncomeTaxServiceImpl.ts +121 -0
- package/src/income-tax/germany/domain/types.ts +27 -0
- package/src/income-tax/usa/USAIncomeTaxService.ts +5 -0
- package/src/income-tax/usa/USAIncomeTaxServiceImpl.ts +113 -0
- package/src/income-tax/usa/domain/types.ts +24 -0
- package/src/index.ts +98 -0
- package/src/mortgage/germany/GermanyMortgageService.ts +5 -0
- package/src/mortgage/germany/GermanyMortgageServiceImpl.ts +111 -0
- package/src/mortgage/germany/domain/types.ts +49 -0
- package/src/mortgage/usa/USAMortgageService.ts +5 -0
- package/src/mortgage/usa/USAMortgageServiceImpl.ts +134 -0
- package/src/mortgage/usa/domain/types.ts +52 -0
- package/src/shared/domain/types.ts +1 -0
- package/test/australia-capital-gains.test.ts +80 -0
- package/test/canada-capital-gains.test.ts +71 -0
- package/test/france-capital-gains.test.ts +66 -0
- package/test/germany-capital-gains.test.ts +85 -0
- package/test/germany-corporate-tax.test.ts +77 -0
- package/test/germany-income-tax.test.ts +114 -0
- package/test/germany-mortgage.test.ts +105 -0
- package/test/south-africa-capital-gains.test.ts +84 -0
- package/test/uk-capital-gains.test.ts +79 -0
- package/test/usa-capital-gains.test.ts +88 -0
- package/test/usa-corporate-tax.test.ts +59 -0
- package/test/usa-income-tax.test.ts +138 -0
- 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,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,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
|
+
}
|
|
@@ -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
|
+
});
|