@novha/calc-engines 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/index.js +23 -2
- 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/index.d.ts +14 -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/index.ts +56 -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/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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Breakdown } from "../domain/types";
|
|
2
|
+
import { FranceCapitalGainsService } from "./FranceCapitalGainsService";
|
|
3
|
+
import { Input, Result, Rules } from "./domain/types";
|
|
4
|
+
|
|
5
|
+
export class FranceCapitalGainsServiceImpl implements FranceCapitalGainsService {
|
|
6
|
+
private _input: Input;
|
|
7
|
+
private _rules: Rules;
|
|
8
|
+
|
|
9
|
+
constructor(input: Input, rules: Rules) {
|
|
10
|
+
this._input = input;
|
|
11
|
+
this._rules = rules;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
calculate(): Result {
|
|
15
|
+
const gain = this._input.capitalGain;
|
|
16
|
+
|
|
17
|
+
if (gain <= 0) {
|
|
18
|
+
return { incomeTax: 0, socialContributions: 0, totalTax: 0, effectiveRate: 0, breakdowns: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const incomeTax = gain * this._rules.flatTaxRate;
|
|
22
|
+
const socialContributions = gain * this._rules.socialContributionsRate;
|
|
23
|
+
const totalTax = incomeTax + socialContributions;
|
|
24
|
+
|
|
25
|
+
const breakdowns: Breakdown[] = [
|
|
26
|
+
{
|
|
27
|
+
from: '0',
|
|
28
|
+
to: 'All',
|
|
29
|
+
rate: this._rules.flatTaxRate,
|
|
30
|
+
amount: incomeTax,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
from: '0',
|
|
34
|
+
to: 'Social',
|
|
35
|
+
rate: this._rules.socialContributionsRate,
|
|
36
|
+
amount: socialContributions,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
incomeTax,
|
|
42
|
+
socialContributions,
|
|
43
|
+
totalTax,
|
|
44
|
+
effectiveRate: (totalTax / gain) * 100,
|
|
45
|
+
breakdowns,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Breakdown } from "../../domain/types";
|
|
2
|
+
|
|
3
|
+
export interface Rules {
|
|
4
|
+
flatTaxRate: number;
|
|
5
|
+
socialContributionsRate: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Input {
|
|
9
|
+
capitalGain: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Result {
|
|
13
|
+
incomeTax: number;
|
|
14
|
+
socialContributions: number;
|
|
15
|
+
totalTax: number;
|
|
16
|
+
effectiveRate: number;
|
|
17
|
+
breakdowns: Breakdown[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Breakdown } from "../domain/types";
|
|
2
|
+
import { GermanyCapitalGainsService } from "./GermanyCapitalGainsService";
|
|
3
|
+
import { Input, Result, Rules } from "./domain/types";
|
|
4
|
+
|
|
5
|
+
export class GermanyCapitalGainsServiceImpl implements GermanyCapitalGainsService {
|
|
6
|
+
private _input: Input;
|
|
7
|
+
private _rules: Rules;
|
|
8
|
+
|
|
9
|
+
constructor(input: Input, rules: Rules) {
|
|
10
|
+
this._input = input;
|
|
11
|
+
this._rules = rules;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
calculate(): Result {
|
|
15
|
+
const gain = this._input.capitalGain;
|
|
16
|
+
|
|
17
|
+
if (gain <= 0) {
|
|
18
|
+
return {
|
|
19
|
+
taxableGain: 0,
|
|
20
|
+
capitalGainsTax: 0,
|
|
21
|
+
solidaritySurcharge: 0,
|
|
22
|
+
totalTax: 0,
|
|
23
|
+
effectiveRate: 0,
|
|
24
|
+
breakdowns: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const taxableGain = Math.max(0, gain - this._rules.annualExemption);
|
|
29
|
+
|
|
30
|
+
if (taxableGain <= 0) {
|
|
31
|
+
return {
|
|
32
|
+
taxableGain: 0,
|
|
33
|
+
capitalGainsTax: 0,
|
|
34
|
+
solidaritySurcharge: 0,
|
|
35
|
+
totalTax: 0,
|
|
36
|
+
effectiveRate: 0,
|
|
37
|
+
breakdowns: [{
|
|
38
|
+
from: '0',
|
|
39
|
+
to: `${this._rules.annualExemption}`,
|
|
40
|
+
rate: 0,
|
|
41
|
+
amount: 0,
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const capitalGainsTax = taxableGain * this._rules.flatTaxRate;
|
|
47
|
+
const solidaritySurcharge = capitalGainsTax * this._rules.solidaritySurchargeRate;
|
|
48
|
+
const totalTax = capitalGainsTax + solidaritySurcharge;
|
|
49
|
+
|
|
50
|
+
const breakdowns: Breakdown[] = [
|
|
51
|
+
{
|
|
52
|
+
from: '0',
|
|
53
|
+
to: 'All',
|
|
54
|
+
rate: this._rules.flatTaxRate,
|
|
55
|
+
amount: capitalGainsTax,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
from: 'CGT',
|
|
59
|
+
to: 'Solidarity',
|
|
60
|
+
rate: this._rules.solidaritySurchargeRate,
|
|
61
|
+
amount: solidaritySurcharge,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
taxableGain,
|
|
67
|
+
capitalGainsTax,
|
|
68
|
+
solidaritySurcharge,
|
|
69
|
+
totalTax,
|
|
70
|
+
effectiveRate: gain > 0 ? (totalTax / gain) * 100 : 0,
|
|
71
|
+
breakdowns,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Breakdown } from "../../domain/types";
|
|
2
|
+
|
|
3
|
+
export interface Rules {
|
|
4
|
+
flatTaxRate: number;
|
|
5
|
+
solidaritySurchargeRate: number;
|
|
6
|
+
annualExemption: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Input {
|
|
10
|
+
capitalGain: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Result {
|
|
14
|
+
taxableGain: number;
|
|
15
|
+
capitalGainsTax: number;
|
|
16
|
+
solidaritySurcharge: number;
|
|
17
|
+
totalTax: number;
|
|
18
|
+
effectiveRate: number;
|
|
19
|
+
breakdowns: Breakdown[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Breakdown } from "../domain/types";
|
|
2
|
+
import { SouthAfricaCapitalGainsService } from "./SouthAfricaCapitalGainsService";
|
|
3
|
+
import { Input, Result, Rules, TaxBracket } from "./domain/types";
|
|
4
|
+
|
|
5
|
+
export class SouthAfricaCapitalGainsServiceImpl implements SouthAfricaCapitalGainsService {
|
|
6
|
+
private _input: Input;
|
|
7
|
+
private _rules: Rules;
|
|
8
|
+
|
|
9
|
+
constructor(input: Input, rules: Rules) {
|
|
10
|
+
this._input = input;
|
|
11
|
+
this._rules = rules;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
calculate(): Result {
|
|
15
|
+
const gain = this._input.capitalGain;
|
|
16
|
+
|
|
17
|
+
if (gain <= 0) {
|
|
18
|
+
return { taxableGain: 0, capitalGainsTax: 0, effectiveRate: 0, breakdowns: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const netGain = Math.max(0, gain - this._rules.annualExclusion);
|
|
22
|
+
const taxableGain = netGain * this._rules.inclusionRate;
|
|
23
|
+
|
|
24
|
+
if (taxableGain <= 0) {
|
|
25
|
+
return {
|
|
26
|
+
taxableGain: 0,
|
|
27
|
+
capitalGainsTax: 0,
|
|
28
|
+
effectiveRate: 0,
|
|
29
|
+
breakdowns: [{
|
|
30
|
+
from: '0',
|
|
31
|
+
to: `${this._rules.annualExclusion}`,
|
|
32
|
+
rate: 0,
|
|
33
|
+
amount: 0,
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const otherIncome = this._input.totalTaxableIncome - gain;
|
|
39
|
+
const { tax, breakdowns } = this.applyBrackets(taxableGain, otherIncome, this._rules.taxBrackets);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
taxableGain,
|
|
43
|
+
capitalGainsTax: tax,
|
|
44
|
+
effectiveRate: gain > 0 ? (tax / gain) * 100 : 0,
|
|
45
|
+
breakdowns,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private applyBrackets(
|
|
50
|
+
taxableGain: number,
|
|
51
|
+
otherIncome: number,
|
|
52
|
+
brackets: TaxBracket[],
|
|
53
|
+
): { tax: number; breakdowns: Breakdown[] } {
|
|
54
|
+
let tax = 0;
|
|
55
|
+
const breakdowns: Breakdown[] = [];
|
|
56
|
+
let remaining = taxableGain;
|
|
57
|
+
let incomeUsed = otherIncome;
|
|
58
|
+
|
|
59
|
+
for (const bracket of brackets) {
|
|
60
|
+
if (remaining <= 0) break;
|
|
61
|
+
|
|
62
|
+
const upper = bracket.to ?? Infinity;
|
|
63
|
+
|
|
64
|
+
if (incomeUsed >= upper) continue;
|
|
65
|
+
|
|
66
|
+
const bracketStart = Math.max(bracket.from, incomeUsed);
|
|
67
|
+
const bracketSpace = upper - bracketStart;
|
|
68
|
+
const taxable = Math.min(bracketSpace, remaining);
|
|
69
|
+
|
|
70
|
+
if (taxable > 0) {
|
|
71
|
+
const bracketTax = taxable * bracket.rate;
|
|
72
|
+
tax += bracketTax;
|
|
73
|
+
remaining -= taxable;
|
|
74
|
+
incomeUsed += taxable;
|
|
75
|
+
|
|
76
|
+
breakdowns.push({
|
|
77
|
+
from: `${bracket.from}`,
|
|
78
|
+
to: `${bracket.to ?? 'Above'}`,
|
|
79
|
+
rate: bracket.rate,
|
|
80
|
+
amount: bracketTax,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { tax, breakdowns };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Breakdown } from "../../domain/types";
|
|
2
|
+
|
|
3
|
+
export interface TaxBracket {
|
|
4
|
+
from: number;
|
|
5
|
+
to: number | null;
|
|
6
|
+
rate: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Rules {
|
|
10
|
+
inclusionRate: number;
|
|
11
|
+
annualExclusion: number;
|
|
12
|
+
taxBrackets: TaxBracket[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Input {
|
|
16
|
+
capitalGain: number;
|
|
17
|
+
totalTaxableIncome: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Result {
|
|
21
|
+
taxableGain: number;
|
|
22
|
+
capitalGainsTax: number;
|
|
23
|
+
effectiveRate: number;
|
|
24
|
+
breakdowns: Breakdown[];
|
|
25
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Breakdown } from "../domain/types";
|
|
2
|
+
import { UKCapitalGainsService } from "./UKCapitalGainsService";
|
|
3
|
+
import { Input, Result, Rules } from "./domain/types";
|
|
4
|
+
|
|
5
|
+
export class UKCapitalGainsServiceImpl implements UKCapitalGainsService {
|
|
6
|
+
private _input: Input;
|
|
7
|
+
private _rules: Rules;
|
|
8
|
+
|
|
9
|
+
constructor(input: Input, rules: Rules) {
|
|
10
|
+
this._input = input;
|
|
11
|
+
this._rules = rules;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
calculate(): Result {
|
|
15
|
+
const gain = this._input.capitalGain;
|
|
16
|
+
|
|
17
|
+
if (gain <= 0) {
|
|
18
|
+
return { taxableGain: 0, capitalGainsTax: 0, effectiveRate: 0, breakdowns: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const taxableGain = Math.max(0, gain - this._rules.annualExemption);
|
|
22
|
+
|
|
23
|
+
if (taxableGain <= 0) {
|
|
24
|
+
return {
|
|
25
|
+
taxableGain: 0,
|
|
26
|
+
capitalGainsTax: 0,
|
|
27
|
+
effectiveRate: 0,
|
|
28
|
+
breakdowns: [{
|
|
29
|
+
from: '0',
|
|
30
|
+
to: `${this._rules.annualExemption}`,
|
|
31
|
+
rate: 0,
|
|
32
|
+
amount: 0,
|
|
33
|
+
}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const basicRateRemaining = Math.max(0, this._rules.basicRateLimit - this._input.totalTaxableIncome);
|
|
38
|
+
const breakdowns: Breakdown[] = [];
|
|
39
|
+
let tax = 0;
|
|
40
|
+
|
|
41
|
+
if (basicRateRemaining > 0) {
|
|
42
|
+
const basicRateGain = Math.min(taxableGain, basicRateRemaining);
|
|
43
|
+
const basicTax = basicRateGain * this._rules.basicRate;
|
|
44
|
+
tax += basicTax;
|
|
45
|
+
|
|
46
|
+
breakdowns.push({
|
|
47
|
+
from: '0',
|
|
48
|
+
to: `${basicRateRemaining}`,
|
|
49
|
+
rate: this._rules.basicRate,
|
|
50
|
+
amount: basicTax,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const higherRateGain = taxableGain - basicRateGain;
|
|
54
|
+
if (higherRateGain > 0) {
|
|
55
|
+
const higherTax = higherRateGain * this._rules.higherRate;
|
|
56
|
+
tax += higherTax;
|
|
57
|
+
|
|
58
|
+
breakdowns.push({
|
|
59
|
+
from: `${basicRateRemaining}`,
|
|
60
|
+
to: 'Above',
|
|
61
|
+
rate: this._rules.higherRate,
|
|
62
|
+
amount: higherTax,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const higherTax = taxableGain * this._rules.higherRate;
|
|
67
|
+
tax += higherTax;
|
|
68
|
+
|
|
69
|
+
breakdowns.push({
|
|
70
|
+
from: '0',
|
|
71
|
+
to: 'Above',
|
|
72
|
+
rate: this._rules.higherRate,
|
|
73
|
+
amount: higherTax,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
taxableGain,
|
|
79
|
+
capitalGainsTax: tax,
|
|
80
|
+
effectiveRate: gain > 0 ? (tax / gain) * 100 : 0,
|
|
81
|
+
breakdowns,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Breakdown } from "../../domain/types";
|
|
2
|
+
|
|
3
|
+
export interface Rules {
|
|
4
|
+
annualExemption: number;
|
|
5
|
+
basicRate: number;
|
|
6
|
+
higherRate: number;
|
|
7
|
+
basicRateLimit: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Input {
|
|
11
|
+
capitalGain: number;
|
|
12
|
+
totalTaxableIncome: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Result {
|
|
16
|
+
taxableGain: number;
|
|
17
|
+
capitalGainsTax: number;
|
|
18
|
+
effectiveRate: number;
|
|
19
|
+
breakdowns: Breakdown[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Breakdown } from "../domain/types";
|
|
2
|
+
import { USACapitalGainsService } from "./USACapitalGainsService";
|
|
3
|
+
import { Input, LongTermBracket, Result, Rules } from "./domain/types";
|
|
4
|
+
|
|
5
|
+
export class USACapitalGainsServiceImpl implements USACapitalGainsService {
|
|
6
|
+
private _input: Input;
|
|
7
|
+
private _rules: Rules;
|
|
8
|
+
|
|
9
|
+
constructor(input: Input, rules: Rules) {
|
|
10
|
+
this._input = input;
|
|
11
|
+
this._rules = rules;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
calculate(): Result {
|
|
15
|
+
const gain = this._input.capitalGain;
|
|
16
|
+
|
|
17
|
+
if (gain <= 0) {
|
|
18
|
+
return { capitalGainsTax: 0, netInvestmentIncomeTax: 0, totalTax: 0, effectiveRate: 0, breakdowns: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isLongTerm = this._input.holdingPeriodMonths > 12;
|
|
22
|
+
const brackets = isLongTerm ? this._rules.longTermBrackets : this._rules.shortTermBrackets;
|
|
23
|
+
|
|
24
|
+
const { tax, breakdowns } = this.applyBrackets(gain, this._input.totalTaxableIncome, brackets);
|
|
25
|
+
|
|
26
|
+
let niit = 0;
|
|
27
|
+
if (this._input.totalTaxableIncome > this._rules.netInvestmentIncomeTax.threshold) {
|
|
28
|
+
niit = gain * this._rules.netInvestmentIncomeTax.rate;
|
|
29
|
+
breakdowns.push({
|
|
30
|
+
from: 'NIIT',
|
|
31
|
+
to: 'All Gain',
|
|
32
|
+
rate: this._rules.netInvestmentIncomeTax.rate,
|
|
33
|
+
amount: niit,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const totalTax = tax + niit;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
capitalGainsTax: tax,
|
|
41
|
+
netInvestmentIncomeTax: niit,
|
|
42
|
+
totalTax,
|
|
43
|
+
effectiveRate: gain > 0 ? (totalTax / gain) * 100 : 0,
|
|
44
|
+
breakdowns,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private applyBrackets(
|
|
49
|
+
gain: number,
|
|
50
|
+
totalIncome: number,
|
|
51
|
+
brackets: LongTermBracket[],
|
|
52
|
+
): { tax: number; breakdowns: Breakdown[] } {
|
|
53
|
+
let tax = 0;
|
|
54
|
+
const breakdowns: Breakdown[] = [];
|
|
55
|
+
let remaining = gain;
|
|
56
|
+
let incomeUsed = totalIncome - gain;
|
|
57
|
+
|
|
58
|
+
for (const bracket of brackets) {
|
|
59
|
+
if (remaining <= 0) break;
|
|
60
|
+
|
|
61
|
+
const upper = bracket.to ?? Infinity;
|
|
62
|
+
|
|
63
|
+
if (incomeUsed >= upper) continue;
|
|
64
|
+
|
|
65
|
+
const bracketStart = Math.max(bracket.from, incomeUsed);
|
|
66
|
+
const bracketSpace = upper - bracketStart;
|
|
67
|
+
const taxable = Math.min(bracketSpace, remaining);
|
|
68
|
+
|
|
69
|
+
if (taxable > 0) {
|
|
70
|
+
const bracketTax = taxable * bracket.rate;
|
|
71
|
+
tax += bracketTax;
|
|
72
|
+
remaining -= taxable;
|
|
73
|
+
incomeUsed += taxable;
|
|
74
|
+
|
|
75
|
+
breakdowns.push({
|
|
76
|
+
from: `${bracket.from}`,
|
|
77
|
+
to: `${bracket.to ?? 'Above'}`,
|
|
78
|
+
rate: bracket.rate,
|
|
79
|
+
amount: bracketTax,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { tax, breakdowns };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Breakdown } from "../../domain/types";
|
|
2
|
+
|
|
3
|
+
export interface LongTermBracket {
|
|
4
|
+
from: number;
|
|
5
|
+
to: number | null;
|
|
6
|
+
rate: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Rules {
|
|
10
|
+
longTermBrackets: LongTermBracket[];
|
|
11
|
+
shortTermBrackets: LongTermBracket[];
|
|
12
|
+
netInvestmentIncomeTax: { rate: number; threshold: number };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Input {
|
|
16
|
+
capitalGain: number;
|
|
17
|
+
totalTaxableIncome: number;
|
|
18
|
+
holdingPeriodMonths: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Result {
|
|
22
|
+
capitalGainsTax: number;
|
|
23
|
+
netInvestmentIncomeTax: number;
|
|
24
|
+
totalTax: number;
|
|
25
|
+
effectiveRate: number;
|
|
26
|
+
breakdowns: Breakdown[];
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -144,5 +144,61 @@ export {
|
|
|
144
144
|
Result as GermanyCorporateTaxResult,
|
|
145
145
|
} from './corporate/germany/domain/types';
|
|
146
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
|
+
|
|
147
203
|
export { IncomeTaxCalculatorSchema } from './income-tax/domain/types';
|
|
148
204
|
export * from './income-tax/domain/types';
|
|
@@ -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
|
+
});
|