@novha/calc-engines 2.0.2 → 3.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 (35) hide show
  1. package/dist/corporate/uk/UKCorporateTaxService.js +3 -0
  2. package/dist/corporate/uk/UKCorporateTaxServiceImpl.js +70 -0
  3. package/dist/corporate/uk/domain/types.js +3 -0
  4. package/dist/income-tax/uk/UKIncomeTaxService.js +3 -0
  5. package/dist/income-tax/uk/UKIncomeTaxServiceImpl.js +96 -0
  6. package/dist/income-tax/uk/domain/types.js +3 -0
  7. package/dist/index.js +9 -2
  8. package/dist/mortgage/uk/UKMortgageService.js +3 -0
  9. package/dist/mortgage/uk/UKMortgageServiceImpl.js +99 -0
  10. package/dist/mortgage/uk/domain/types.js +3 -0
  11. package/dist/types/corporate/uk/UKCorporateTaxService.d.ts +4 -0
  12. package/dist/types/corporate/uk/UKCorporateTaxServiceImpl.d.ts +10 -0
  13. package/dist/types/corporate/uk/domain/types.d.ts +32 -0
  14. package/dist/types/income-tax/uk/UKIncomeTaxService.d.ts +4 -0
  15. package/dist/types/income-tax/uk/UKIncomeTaxServiceImpl.d.ts +12 -0
  16. package/dist/types/income-tax/uk/domain/types.d.ts +27 -0
  17. package/dist/types/index.d.ts +6 -0
  18. package/dist/types/mortgage/uk/UKMortgageService.d.ts +4 -0
  19. package/dist/types/mortgage/uk/UKMortgageServiceImpl.d.ts +9 -0
  20. package/dist/types/mortgage/uk/domain/types.d.ts +49 -0
  21. package/package.json +1 -1
  22. package/src/corporate/uk/UKCorporateTaxService.ts +5 -0
  23. package/src/corporate/uk/UKCorporateTaxServiceImpl.ts +82 -0
  24. package/src/corporate/uk/domain/types.ts +38 -0
  25. package/src/income-tax/uk/UKIncomeTaxService.ts +5 -0
  26. package/src/income-tax/uk/UKIncomeTaxServiceImpl.ts +133 -0
  27. package/src/income-tax/uk/domain/types.ts +31 -0
  28. package/src/index.ts +21 -0
  29. package/src/mortgage/uk/UKMortgageService.ts +5 -0
  30. package/src/mortgage/uk/UKMortgageServiceImpl.ts +140 -0
  31. package/src/mortgage/uk/domain/types.ts +58 -0
  32. package/test/australia-income-tax.test.ts +10 -10
  33. package/test/uk-corporate-tax.test.ts +119 -0
  34. package/test/uk-income-tax.test.ts +146 -0
  35. package/test/uk-mortgage.test.ts +154 -0
@@ -0,0 +1,82 @@
1
+ import { Breakdown } from "../domain/types";
2
+ import { UKCorporateTaxService } from "./UKCorporateTaxService";
3
+ import { Input, MarginalReliefRule, Result, Rules } from "./domain/types";
4
+
5
+ export class UKCorporateTaxServiceImpl implements UKCorporateTaxService {
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 income = this._input.taxableIncome;
16
+ const { smallProfits, main, marginalRelief } = this._rules.regimes;
17
+
18
+ let tax = 0;
19
+ let breakdowns: Breakdown[] = [];
20
+
21
+ if (income <= 0) {
22
+ return { corporateTax: 0, effectiveTaxRate: 0, breakdowns: [] };
23
+ }
24
+
25
+ if (income <= marginalRelief.lowerLimit) {
26
+ tax = income * smallProfits.rate;
27
+ breakdowns = [{
28
+ from: '0',
29
+ to: `${marginalRelief.lowerLimit}`,
30
+ rate: smallProfits.rate,
31
+ amount: tax,
32
+ }];
33
+ } else if (income >= marginalRelief.upperLimit) {
34
+ tax = income * main.rate;
35
+ breakdowns = [{
36
+ from: '0',
37
+ to: 'Above',
38
+ rate: main.rate,
39
+ amount: tax,
40
+ }];
41
+ } else {
42
+ tax = this.applyMarginalRelief(income, marginalRelief);
43
+ breakdowns = this.buildMarginalReliefBreakdowns(income, tax, marginalRelief);
44
+ }
45
+
46
+ return {
47
+ corporateTax: tax,
48
+ effectiveTaxRate: income > 0 ? (tax / income) * 100 : 0,
49
+ breakdowns,
50
+ };
51
+ }
52
+
53
+ private applyMarginalRelief(income: number, rules: MarginalReliefRule): number {
54
+ const grossTax = income * rules.mainRate;
55
+ const relief = rules.standardFraction * (rules.upperLimit - income);
56
+ return grossTax - relief;
57
+ }
58
+
59
+ private buildMarginalReliefBreakdowns(
60
+ income: number,
61
+ tax: number,
62
+ rules: MarginalReliefRule,
63
+ ): Breakdown[] {
64
+ const grossTax = income * rules.mainRate;
65
+ const relief = rules.standardFraction * (rules.upperLimit - income);
66
+
67
+ return [
68
+ {
69
+ from: '0',
70
+ to: 'Above',
71
+ rate: rules.mainRate,
72
+ amount: grossTax,
73
+ },
74
+ {
75
+ from: `${rules.lowerLimit}`,
76
+ to: `${rules.upperLimit}`,
77
+ rate: -(rules.standardFraction),
78
+ amount: -relief,
79
+ },
80
+ ];
81
+ }
82
+ }
@@ -0,0 +1,38 @@
1
+ import { Breakdown } from "../../domain/types";
2
+
3
+ export interface FlatTaxRule {
4
+ type: 'flat';
5
+ rate: number;
6
+ conditions?: {
7
+ maxIncome?: number;
8
+ };
9
+ }
10
+
11
+ export interface MarginalReliefRule {
12
+ type: 'marginal_relief';
13
+ mainRate: number;
14
+ smallProfitsRate: number;
15
+ upperLimit: number;
16
+ lowerLimit: number;
17
+ standardFraction: number;
18
+ }
19
+
20
+ export type TaxRule = FlatTaxRule | MarginalReliefRule;
21
+
22
+ export interface Rules {
23
+ regimes: {
24
+ smallProfits: FlatTaxRule;
25
+ main: FlatTaxRule;
26
+ marginalRelief: MarginalReliefRule;
27
+ };
28
+ }
29
+
30
+ export interface Input {
31
+ taxableIncome: number;
32
+ }
33
+
34
+ export interface Result {
35
+ corporateTax: number;
36
+ effectiveTaxRate: number;
37
+ breakdowns: Breakdown[];
38
+ }
@@ -0,0 +1,5 @@
1
+ import { ComputedIncomeTaxValues } from "./domain/types";
2
+
3
+ export interface UKIncomeTaxService {
4
+ calculateNetIncome(): ComputedIncomeTaxValues;
5
+ }
@@ -0,0 +1,133 @@
1
+ import { BracketAllocation } from "../domain/types";
2
+ import { UKIncomeTaxService } from "./UKIncomeTaxService";
3
+ import {
4
+ ComputedIncomeTaxValues,
5
+ IncomeTaxRules,
6
+ NationalInsuranceRules,
7
+ PersonalAllowanceRules,
8
+ } from "./domain/types";
9
+
10
+ export class UKIncomeTaxServiceImpl implements UKIncomeTaxService {
11
+ private _income: number;
12
+ private _rules: IncomeTaxRules;
13
+
14
+ constructor(income: number, rules: IncomeTaxRules) {
15
+ this._income = income;
16
+ this._rules = rules;
17
+ }
18
+
19
+ public calculateNetIncome(): ComputedIncomeTaxValues {
20
+ const effectivePA = this.computePersonalAllowance(
21
+ this._income,
22
+ this._rules.personalAllowance,
23
+ );
24
+ const taxableIncome = Math.max(0, this._income - effectivePA);
25
+
26
+ const { grossTax, bracketBreakdown } = this.computeTaxBrackets(taxableIncome);
27
+
28
+ const ni = this.computeNationalInsurance(
29
+ this._income,
30
+ this._rules.nationalInsurance,
31
+ );
32
+
33
+ const totalDeductions = grossTax + ni;
34
+ const netIncome = this._income - totalDeductions;
35
+
36
+ return {
37
+ grossIncome: this._income,
38
+ incomeTax: this.round(grossTax),
39
+ nationalInsurance: this.round(ni),
40
+ personalAllowance: effectivePA,
41
+ totalDeductions: this.round(totalDeductions),
42
+ netIncome: this.round(netIncome),
43
+ effectiveTaxRate: this._income > 0 ? this.round(grossTax / this._income, 4) : 0,
44
+ taxBracketBreakdown: bracketBreakdown,
45
+ };
46
+ }
47
+
48
+ private computePersonalAllowance(
49
+ income: number,
50
+ rules: PersonalAllowanceRules,
51
+ ): number {
52
+ if (income <= rules.taperThreshold) {
53
+ return rules.amount;
54
+ }
55
+
56
+ const reduction = Math.floor((income - rules.taperThreshold) * rules.taperRate);
57
+ return Math.max(0, rules.amount - reduction);
58
+ }
59
+
60
+ private computeTaxBrackets(
61
+ taxableIncome: number,
62
+ ): { grossTax: number; bracketBreakdown: BracketAllocation[] } {
63
+ let tax = 0;
64
+ const bracketBreakdown: BracketAllocation[] = [];
65
+
66
+ for (let index = 0; index < this._rules.taxBrackets.length; index++) {
67
+ const bracket = this._rules.taxBrackets[index];
68
+
69
+ if (taxableIncome <= bracket.from) {
70
+ bracketBreakdown.push({
71
+ bracketIndex: index,
72
+ bracketName: `Bracket ${index + 1}`,
73
+ from: bracket.from,
74
+ to: bracket.to ?? null,
75
+ rate: bracket.rate,
76
+ amountInBracket: 0,
77
+ taxOnAmount: 0,
78
+ });
79
+ break;
80
+ }
81
+
82
+ const upper = bracket.to ?? taxableIncome;
83
+ const taxableAmount = Math.min(upper, taxableIncome) - bracket.from;
84
+
85
+ if (taxableAmount > 0) {
86
+ const taxOnAmount = taxableAmount * bracket.rate;
87
+ tax += taxOnAmount;
88
+ bracketBreakdown.push({
89
+ bracketIndex: index,
90
+ bracketName: `Bracket ${index + 1}`,
91
+ from: bracket.from,
92
+ to: bracket.to ?? null,
93
+ rate: bracket.rate,
94
+ amountInBracket: taxableAmount,
95
+ taxOnAmount,
96
+ });
97
+ } else {
98
+ bracketBreakdown.push({
99
+ bracketIndex: index,
100
+ bracketName: `Bracket ${index + 1}`,
101
+ from: bracket.from,
102
+ to: bracket.to ?? null,
103
+ rate: bracket.rate,
104
+ amountInBracket: 0,
105
+ taxOnAmount: 0,
106
+ });
107
+ }
108
+ }
109
+
110
+ return { grossTax: tax, bracketBreakdown };
111
+ }
112
+
113
+ private computeNationalInsurance(
114
+ income: number,
115
+ rules: NationalInsuranceRules,
116
+ ): number {
117
+ if (income <= rules.primaryThreshold) {
118
+ return 0;
119
+ }
120
+
121
+ if (income <= rules.upperEarningsLimit) {
122
+ return (income - rules.primaryThreshold) * rules.mainRate;
123
+ }
124
+
125
+ const mainBand = (rules.upperEarningsLimit - rules.primaryThreshold) * rules.mainRate;
126
+ const upperBand = (income - rules.upperEarningsLimit) * rules.upperRate;
127
+ return mainBand + upperBand;
128
+ }
129
+
130
+ private round(value: number, decimals = 2): number {
131
+ return Number(value.toFixed(decimals));
132
+ }
133
+ }
@@ -0,0 +1,31 @@
1
+ import { BracketAllocation, TaxBracket } from "../../domain/types";
2
+
3
+ export interface IncomeTaxRules {
4
+ taxBrackets: TaxBracket[];
5
+ personalAllowance: PersonalAllowanceRules;
6
+ nationalInsurance: NationalInsuranceRules;
7
+ }
8
+
9
+ export interface PersonalAllowanceRules {
10
+ amount: number;
11
+ taperThreshold: number;
12
+ taperRate: number;
13
+ }
14
+
15
+ export interface NationalInsuranceRules {
16
+ primaryThreshold: number;
17
+ upperEarningsLimit: number;
18
+ mainRate: number;
19
+ upperRate: number;
20
+ }
21
+
22
+ export interface ComputedIncomeTaxValues {
23
+ grossIncome: number;
24
+ incomeTax: number;
25
+ nationalInsurance: number;
26
+ personalAllowance: number;
27
+ totalDeductions: number;
28
+ netIncome: number;
29
+ effectiveTaxRate: number;
30
+ taxBracketBreakdown: BracketAllocation[];
31
+ }
package/src/index.ts CHANGED
@@ -81,5 +81,26 @@ export {
81
81
  Result as AustraliaCorporateTaxResult,
82
82
  } from './corporate/australia/domain/types';
83
83
 
84
+ // United Kingdom
85
+ export { UKIncomeTaxServiceImpl as UKIncomeTaxService } from './income-tax/uk/UKIncomeTaxServiceImpl';
86
+ export {
87
+ ComputedIncomeTaxValues as UKComputedIncomeTaxValues,
88
+ IncomeTaxRules as UKIncomeTaxRules,
89
+ } from './income-tax/uk/domain/types';
90
+
91
+ export { UKCorporateTaxServiceImpl as UKCorporateTaxService } from './corporate/uk/UKCorporateTaxServiceImpl';
92
+ export {
93
+ Input as UKCorporateTaxInput,
94
+ Rules as UKCorporateTaxRules,
95
+ Result as UKCorporateTaxResult,
96
+ } from './corporate/uk/domain/types';
97
+
98
+ export { UKMortgageServiceImpl as UKMortgageService } from './mortgage/uk/UKMortgageServiceImpl';
99
+ export {
100
+ MortgageRules as UKMortgageRules,
101
+ MortgageInput as UKMortgageInput,
102
+ MortgageOutput as UKMortgageOutput,
103
+ } from './mortgage/uk/domain/types';
104
+
84
105
  export { IncomeTaxCalculatorSchema } from './income-tax/domain/types';
85
106
  export * from './income-tax/domain/types';
@@ -0,0 +1,5 @@
1
+ import { MortgageInput, MortgageOutput, MortgageRules } from "./domain/types";
2
+
3
+ export interface UKMortgageService {
4
+ calculate(input: MortgageInput, rules: MortgageRules): MortgageOutput;
5
+ }
@@ -0,0 +1,140 @@
1
+ import { UKMortgageService } from "./UKMortgageService";
2
+ import {
3
+ AmortizationScheduleItem,
4
+ MortgageInput,
5
+ MortgageOutput,
6
+ MortgageRules,
7
+ StampDutyBracket,
8
+ } from "./domain/types";
9
+
10
+ export class UKMortgageServiceImpl implements UKMortgageService {
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 stampDuty = this.calculateStampDuty(input, rules);
27
+
28
+ const amortizationSchedule = this.calculateAmortizationSchedule(
29
+ loanAmount,
30
+ monthlyRate,
31
+ monthlyPayment,
32
+ totalPayments,
33
+ input.amortizationYears,
34
+ );
35
+
36
+ return {
37
+ loanAmount,
38
+ totalMortgage: loanAmount,
39
+ monthlyPayment,
40
+ totalInterestPaid,
41
+ totalPaid,
42
+ stampDuty,
43
+ amortizationSchedule,
44
+ otherFees: {
45
+ notaryFees: {
46
+ value: stampDuty,
47
+ label: 'STAMP_DUTY',
48
+ },
49
+ bankFees: {
50
+ value: 0,
51
+ label: 'BANK_FEES',
52
+ },
53
+ monthlyInsuranceFees: {
54
+ value: 0,
55
+ label: 'MONTHLY_INSURANCE_FEES',
56
+ },
57
+ },
58
+ };
59
+ }
60
+
61
+ private calculateStampDuty(input: MortgageInput, rules: MortgageRules): number {
62
+ const { propertyPrice, isFirstTimeBuyer } = input;
63
+ const { firstTimeBuyer, standardBrackets } = rules.stampDuty;
64
+
65
+ const useFirstTimeBuyerRules =
66
+ isFirstTimeBuyer && propertyPrice <= firstTimeBuyer.maxEligiblePropertyPrice;
67
+
68
+ const brackets = useFirstTimeBuyerRules ? firstTimeBuyer.brackets : standardBrackets;
69
+
70
+ return this.applyStampDutyBrackets(propertyPrice, brackets);
71
+ }
72
+
73
+ private applyStampDutyBrackets(propertyPrice: number, brackets: StampDutyBracket[]): number {
74
+ let duty = 0;
75
+ let previousLimit = 0;
76
+
77
+ for (const bracket of brackets) {
78
+ if (bracket.upTo !== undefined && propertyPrice > previousLimit) {
79
+ const taxable = Math.min(propertyPrice, bracket.upTo) - previousLimit;
80
+ duty += taxable * bracket.rate;
81
+ previousLimit = bracket.upTo;
82
+ }
83
+
84
+ if (bracket.above !== undefined && propertyPrice > bracket.above) {
85
+ duty += (propertyPrice - bracket.above) * bracket.rate;
86
+ break;
87
+ }
88
+ }
89
+
90
+ return duty;
91
+ }
92
+
93
+ private calculateAmortizationSchedule(
94
+ principal: number,
95
+ monthlyRate: number,
96
+ monthlyPayment: number,
97
+ totalPayments: number,
98
+ amortizationYears: number,
99
+ ): AmortizationScheduleItem[] {
100
+ const schedule: AmortizationScheduleItem[] = [];
101
+ let balance = principal;
102
+
103
+ for (let year = 1; year <= amortizationYears; year++) {
104
+ let yearlyPrincipal = 0;
105
+ let yearlyInterest = 0;
106
+
107
+ const paymentsInYear = Math.min(12, totalPayments - (year - 1) * 12);
108
+
109
+ for (let payment = 1; payment <= paymentsInYear; payment++) {
110
+ const interestPayment = balance * monthlyRate;
111
+ const principalPayment = monthlyPayment - interestPayment;
112
+
113
+ yearlyInterest += interestPayment;
114
+ yearlyPrincipal += principalPayment;
115
+ balance -= principalPayment;
116
+ }
117
+
118
+ schedule.push({
119
+ year,
120
+ principal: yearlyPrincipal,
121
+ interest: yearlyInterest,
122
+ balance: Math.max(0, balance),
123
+ });
124
+
125
+ if (balance <= 0) break;
126
+ }
127
+
128
+ return schedule;
129
+ }
130
+
131
+ private calculatePayment(principal: number, rate: number, periods: number): number {
132
+ if (rate === 0) {
133
+ return principal / periods;
134
+ }
135
+
136
+ return principal *
137
+ (rate * Math.pow(1 + rate, periods)) /
138
+ (Math.pow(1 + rate, periods) - 1);
139
+ }
140
+ }
@@ -0,0 +1,58 @@
1
+ import { OtherFees } from "../../domain/types";
2
+
3
+ export interface MortgageRules {
4
+ loanConstraints: LoanConstraints;
5
+ interest: InterestRules;
6
+ stampDuty: StampDutyRules;
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 StampDutyBracket {
19
+ upTo?: number;
20
+ above?: number;
21
+ rate: number;
22
+ }
23
+
24
+ export interface StampDutyRules {
25
+ standardBrackets: StampDutyBracket[];
26
+ firstTimeBuyer: FirstTimeBuyerStampDuty;
27
+ }
28
+
29
+ export interface FirstTimeBuyerStampDuty {
30
+ brackets: StampDutyBracket[];
31
+ maxEligiblePropertyPrice: number;
32
+ }
33
+
34
+ export interface AmortizationScheduleItem {
35
+ year: number;
36
+ principal: number;
37
+ interest: number;
38
+ balance: number;
39
+ }
40
+
41
+ export interface MortgageInput {
42
+ propertyPrice: number;
43
+ downPayment: number;
44
+ annualInterestRate: number;
45
+ amortizationYears: number;
46
+ isFirstTimeBuyer: boolean;
47
+ }
48
+
49
+ export interface MortgageOutput {
50
+ loanAmount: number;
51
+ totalMortgage: number;
52
+ monthlyPayment: number;
53
+ totalInterestPaid: number;
54
+ totalPaid: number;
55
+ stampDuty: number;
56
+ amortizationSchedule: AmortizationScheduleItem[];
57
+ otherFees: OtherFees;
58
+ }
@@ -31,7 +31,7 @@ const australiaRules: IncomeTaxRules = {
31
31
 
32
32
  describe('AustraliaIncomeTaxServiceImpl', () => {
33
33
  it('returns zero tax for income below the tax-free threshold', () => {
34
- const service = new AustraliaIncomeTaxServiceImpl(18200, australiaRules);
34
+ const service = new AustraliaIncomeTaxServiceImpl(18200, australiaRules, true, false);
35
35
  const result = service.calculateNetIncome();
36
36
 
37
37
  expect(result.incomeTax).toBe(0);
@@ -40,7 +40,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
40
40
  });
41
41
 
42
42
  it('correctly calculates income tax for $60,000 income', () => {
43
- const service = new AustraliaIncomeTaxServiceImpl(60000, australiaRules);
43
+ const service = new AustraliaIncomeTaxServiceImpl(60000, australiaRules, true, false);
44
44
  const result = service.calculateNetIncome();
45
45
 
46
46
  // Bracket 1: 0 → 18200 @ 0% = 0
@@ -54,7 +54,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
54
54
  });
55
55
 
56
56
  it('applies the Low Income Tax Offset for low incomes', () => {
57
- const service = new AustraliaIncomeTaxServiceImpl(30000, australiaRules);
57
+ const service = new AustraliaIncomeTaxServiceImpl(30000, australiaRules, true, false);
58
58
  const result = service.calculateNetIncome();
59
59
 
60
60
  // Bracket 2: 18200 → 30000 @ 19% = 2242
@@ -65,7 +65,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
65
65
  });
66
66
 
67
67
  it('partially phases out LITO between $37,500 and $45,000', () => {
68
- const service = new AustraliaIncomeTaxServiceImpl(40000, australiaRules);
68
+ const service = new AustraliaIncomeTaxServiceImpl(40000, australiaRules, true, false);
69
69
  const result = service.calculateNetIncome();
70
70
 
71
71
  // Gross tax: 18200→40000 @ 19% = 3382
@@ -74,7 +74,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
74
74
  });
75
75
 
76
76
  it('applies Medicare Levy correctly', () => {
77
- const service = new AustraliaIncomeTaxServiceImpl(60000, australiaRules);
77
+ const service = new AustraliaIncomeTaxServiceImpl(60000, australiaRules, true, true);
78
78
  const result = service.calculateNetIncome();
79
79
 
80
80
  // Medicare Levy: 60000 * 2% = 1200
@@ -83,7 +83,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
83
83
 
84
84
  it('applies shading-in Medicare Levy for income in the shading-in range', () => {
85
85
  const income = 28000;
86
- const service = new AustraliaIncomeTaxServiceImpl(income, australiaRules);
86
+ const service = new AustraliaIncomeTaxServiceImpl(income, australiaRules, true, true);
87
87
  const result = service.calculateNetIncome();
88
88
 
89
89
  // income (28000) > shadingInThreshold (26000), < fullLevyThreshold (32500)
@@ -92,14 +92,14 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
92
92
  });
93
93
 
94
94
  it('returns zero Medicare Levy below shading-in threshold', () => {
95
- const service = new AustraliaIncomeTaxServiceImpl(25000, australiaRules);
95
+ const service = new AustraliaIncomeTaxServiceImpl(25000, australiaRules, true, true);
96
96
  const result = service.calculateNetIncome();
97
97
 
98
98
  expect(result.medicareLevy).toBe(0);
99
99
  });
100
100
 
101
101
  it('calculates high income tax correctly', () => {
102
- const service = new AustraliaIncomeTaxServiceImpl(200000, australiaRules);
102
+ const service = new AustraliaIncomeTaxServiceImpl(200000, australiaRules, true, true);
103
103
  const result = service.calculateNetIncome();
104
104
 
105
105
  // Bracket 1: 0→18200 @ 0% = 0
@@ -115,7 +115,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
115
115
  });
116
116
 
117
117
  it('net income equals gross income minus total deductions', () => {
118
- const service = new AustraliaIncomeTaxServiceImpl(80000, australiaRules);
118
+ const service = new AustraliaIncomeTaxServiceImpl(80000, australiaRules, true, true);
119
119
  const result = service.calculateNetIncome();
120
120
 
121
121
  expect(result.netIncome).toBeCloseTo(
@@ -125,7 +125,7 @@ describe('AustraliaIncomeTaxServiceImpl', () => {
125
125
  });
126
126
 
127
127
  it('effective tax rate is income tax divided by gross income', () => {
128
- const service = new AustraliaIncomeTaxServiceImpl(100000, australiaRules);
128
+ const service = new AustraliaIncomeTaxServiceImpl(100000, australiaRules, true, false);
129
129
  const result = service.calculateNetIncome();
130
130
 
131
131
  expect(result.effectiveTaxRate).toBeCloseTo(result.incomeTax / result.grossIncome, 4);