canada-tax-toolkit 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ronit Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: canada-tax-toolkit
3
+ Version: 0.2.0
4
+ Summary: Simple, transparent Canadian (federal + Ontario) income tax calculations
5
+ Author: Ronit Gupta
6
+ License: MIT
7
+ Keywords: tax,canada,ontario,cra,finance
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # Canada Tax Toolkit 🇨🇦
14
+
15
+ Simple, transparent Python tools for Canadian personal income tax — starting with **federal + Ontario** for the **2026** tax year.
16
+
17
+ Every rate lives in one readable file (`src/canadatax/rates.py`), and every calculation is tested against published worked examples.
18
+ **👉 Try it online: [ronitgupta43-ops.github.io/canada-tax-toolkit](https://ronitgupta43-ops.github.io/canada-tax-toolkit/)**
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ git clone https://github.com/ronitgupta43-ops/canada-tax-toolkit.git
24
+ cd canada-tax-toolkit
25
+ pip install -e .
26
+ python -m canadatax 75000
27
+ ```
28
+
29
+ ```python
30
+ from canadatax import total_tax
31
+ print(total_tax(75_000))
32
+ ```
33
+
34
+ ## What's included (v0.1)
35
+
36
+ - 2026 federal brackets (14% lowest rate) and basic personal amount, including the high-income phase-out
37
+ - 2026 Ontario brackets, basic personal amount and two-tier surtax
38
+ - Average and marginal tax rates
39
+ - Command-line tool
40
+ - Ontario low-income tax reduction and Ontario Health Premium
41
+ - Free web calculator that runs in your browser
42
+
43
+ ## Roadmap — contributions welcome!
44
+
45
+ - [x] Ontario tax reduction (low-income)
46
+ - [x] Ontario Health Premium
47
+ - [x] CPP / EI contributions and credits
48
+ - [ ] 2025 rates (with the blended 14.5% federal rate)
49
+ - [ ] Other provinces (BC, Alberta, Quebec…)
50
+ - [ ] RRSP deduction "what if" calculator
51
+
52
+ ## Disclaimer
53
+
54
+ This is an educational tool, not tax advice. Always confirm with CRA or a tax professional.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,46 @@
1
+ # Canada Tax Toolkit 🇨🇦
2
+
3
+ Simple, transparent Python tools for Canadian personal income tax — starting with **federal + Ontario** for the **2026** tax year.
4
+
5
+ Every rate lives in one readable file (`src/canadatax/rates.py`), and every calculation is tested against published worked examples.
6
+ **👉 Try it online: [ronitgupta43-ops.github.io/canada-tax-toolkit](https://ronitgupta43-ops.github.io/canada-tax-toolkit/)**
7
+
8
+ ## Quick start
9
+
10
+ ```bash
11
+ git clone https://github.com/ronitgupta43-ops/canada-tax-toolkit.git
12
+ cd canada-tax-toolkit
13
+ pip install -e .
14
+ python -m canadatax 75000
15
+ ```
16
+
17
+ ```python
18
+ from canadatax import total_tax
19
+ print(total_tax(75_000))
20
+ ```
21
+
22
+ ## What's included (v0.1)
23
+
24
+ - 2026 federal brackets (14% lowest rate) and basic personal amount, including the high-income phase-out
25
+ - 2026 Ontario brackets, basic personal amount and two-tier surtax
26
+ - Average and marginal tax rates
27
+ - Command-line tool
28
+ - Ontario low-income tax reduction and Ontario Health Premium
29
+ - Free web calculator that runs in your browser
30
+
31
+ ## Roadmap — contributions welcome!
32
+
33
+ - [x] Ontario tax reduction (low-income)
34
+ - [x] Ontario Health Premium
35
+ - [x] CPP / EI contributions and credits
36
+ - [ ] 2025 rates (with the blended 14.5% federal rate)
37
+ - [ ] Other provinces (BC, Alberta, Quebec…)
38
+ - [ ] RRSP deduction "what if" calculator
39
+
40
+ ## Disclaimer
41
+
42
+ This is an educational tool, not tax advice. Always confirm with CRA or a tax professional.
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "canada-tax-toolkit"
7
+ version = "0.2.0"
8
+ description = "Simple, transparent Canadian (federal + Ontario) income tax calculations"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "Ronit Gupta"}]
12
+ requires-python = ">=3.9"
13
+ keywords = ["tax", "canada", "ontario", "cra", "finance"]
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [tool.pytest.ini_options]
19
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: canada-tax-toolkit
3
+ Version: 0.2.0
4
+ Summary: Simple, transparent Canadian (federal + Ontario) income tax calculations
5
+ Author: Ronit Gupta
6
+ License: MIT
7
+ Keywords: tax,canada,ontario,cra,finance
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # Canada Tax Toolkit 🇨🇦
14
+
15
+ Simple, transparent Python tools for Canadian personal income tax — starting with **federal + Ontario** for the **2026** tax year.
16
+
17
+ Every rate lives in one readable file (`src/canadatax/rates.py`), and every calculation is tested against published worked examples.
18
+ **👉 Try it online: [ronitgupta43-ops.github.io/canada-tax-toolkit](https://ronitgupta43-ops.github.io/canada-tax-toolkit/)**
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ git clone https://github.com/ronitgupta43-ops/canada-tax-toolkit.git
24
+ cd canada-tax-toolkit
25
+ pip install -e .
26
+ python -m canadatax 75000
27
+ ```
28
+
29
+ ```python
30
+ from canadatax import total_tax
31
+ print(total_tax(75_000))
32
+ ```
33
+
34
+ ## What's included (v0.1)
35
+
36
+ - 2026 federal brackets (14% lowest rate) and basic personal amount, including the high-income phase-out
37
+ - 2026 Ontario brackets, basic personal amount and two-tier surtax
38
+ - Average and marginal tax rates
39
+ - Command-line tool
40
+ - Ontario low-income tax reduction and Ontario Health Premium
41
+ - Free web calculator that runs in your browser
42
+
43
+ ## Roadmap — contributions welcome!
44
+
45
+ - [x] Ontario tax reduction (low-income)
46
+ - [x] Ontario Health Premium
47
+ - [x] CPP / EI contributions and credits
48
+ - [ ] 2025 rates (with the blended 14.5% federal rate)
49
+ - [ ] Other provinces (BC, Alberta, Quebec…)
50
+ - [ ] RRSP deduction "what if" calculator
51
+
52
+ ## Disclaimer
53
+
54
+ This is an educational tool, not tax advice. Always confirm with CRA or a tax professional.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/canada_tax_toolkit.egg-info/PKG-INFO
5
+ src/canada_tax_toolkit.egg-info/SOURCES.txt
6
+ src/canada_tax_toolkit.egg-info/dependency_links.txt
7
+ src/canada_tax_toolkit.egg-info/top_level.txt
8
+ src/canadatax/__init__.py
9
+ src/canadatax/__main__.py
10
+ src/canadatax/calculator.py
11
+ src/canadatax/rates.py
12
+ tests/test_calculator.py
13
+ tests/test_health_premium.py
14
+ tests/test_payroll.py
15
+ tests/test_tax_reduction.py
@@ -0,0 +1,19 @@
1
+ """canadatax: simple, transparent Canadian income tax calculations."""
2
+
3
+ from .calculator import (
4
+ bracket_tax,
5
+ cpp_contributions,
6
+ ei_premium,
7
+ federal_bpa,
8
+ federal_tax,
9
+ ontario_health_premium,
10
+ ontario_tax,
11
+ take_home,
12
+ total_tax,
13
+ )
14
+
15
+ __version__ = "0.2.0"
16
+ __all__ = [
17
+ "bracket_tax", "cpp_contributions", "ei_premium", "federal_bpa", "federal_tax",
18
+ "ontario_health_premium", "ontario_tax", "take_home", "total_tax",
19
+ ]
@@ -0,0 +1,28 @@
1
+ """Command line: python -m canadatax 75000 [year]"""
2
+
3
+ import sys
4
+ from .calculator import take_home
5
+
6
+
7
+ def main():
8
+ if len(sys.argv) < 2:
9
+ print("Usage: python -m canadatax <employment_income> [year]")
10
+ sys.exit(1)
11
+ income = float(sys.argv[1].replace(",", ""))
12
+ year = int(sys.argv[2]) if len(sys.argv) > 2 else 2026
13
+ r = take_home(income, year)
14
+ print(f"Ontario employee, {r['year']} tax year")
15
+ print(f"Employment income: ${r['employment_income']:>12,.2f}")
16
+ print(f"Federal tax: ${r['federal']:>12,.2f}")
17
+ print(f"Ontario tax: ${r['ontario']:>12,.2f}")
18
+ print(f"Health premium: ${r['ontario_health_premium']:>12,.2f}")
19
+ print(f"CPP: ${r['cpp']:>12,.2f}")
20
+ print(f"CPP2: ${r['cpp2']:>12,.2f}")
21
+ print(f"EI: ${r['ei']:>12,.2f}")
22
+ print(f"Total deductions: ${r['total_deductions']:>12,.2f}")
23
+ print(f"Take-home pay: ${r['take_home']:>12,.2f}")
24
+ print(f"Per month: ${r['monthly_take_home']:>12,.2f}")
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -0,0 +1,163 @@
1
+ """Federal and Ontario personal income tax calculator."""
2
+
3
+ from .rates import FEDERAL, ONTARIO, PAYROLL
4
+
5
+
6
+ def _rates(table, year):
7
+ if year not in table:
8
+ raise ValueError(f"No rates for {year}. Available: {sorted(table)}")
9
+ return table[year]
10
+
11
+
12
+ def bracket_tax(income, brackets):
13
+ """Apply progressive brackets to an income amount."""
14
+ tax, lower = 0.0, 0.0
15
+ for upper, rate in brackets:
16
+ if upper is None or income <= upper:
17
+ return tax + (income - lower) * rate
18
+ tax += (upper - lower) * rate
19
+ lower = upper
20
+ return tax
21
+
22
+
23
+ def federal_bpa(net_income, year=2026):
24
+ """Federal basic personal amount, phased down for high incomes."""
25
+ r = _rates(FEDERAL, year)
26
+ start, end = r["bpa_phaseout_start"], r["bpa_phaseout_end"]
27
+ extra = r["bpa_max"] - r["bpa_min"]
28
+ if net_income <= start:
29
+ return r["bpa_max"]
30
+ if net_income >= end:
31
+ return r["bpa_min"]
32
+ return r["bpa_min"] + extra * (end - net_income) / (end - start)
33
+
34
+
35
+ def federal_tax(taxable_income, year=2026, other_credits=0):
36
+ """Basic federal tax after the basic personal amount credit.
37
+
38
+ other_credits: extra credit amounts (like base CPP and EI) that earn
39
+ a credit at the same rate as the basic personal amount.
40
+ """
41
+ r = _rates(FEDERAL, year)
42
+ gross = bracket_tax(taxable_income, r["brackets"])
43
+ credit = (federal_bpa(taxable_income, year) + other_credits) * r["credit_rate"]
44
+ return round(max(0.0, gross - credit), 2)
45
+
46
+
47
+ def ontario_basic_tax(taxable_income, year=2026, other_credits=0):
48
+ """Ontario tax after non-refundable credits, before surtax."""
49
+ r = _rates(ONTARIO, year)
50
+ gross = bracket_tax(taxable_income, r["brackets"])
51
+ credit = (r["bpa"] + other_credits) * r["credit_rate"]
52
+ return max(0.0, gross - credit)
53
+
54
+
55
+ def ontario_surtax(basic_tax, year=2026):
56
+ r = _rates(ONTARIO, year)
57
+ return sum(max(0.0, basic_tax - t) * rate for t, rate in r["surtax"])
58
+
59
+
60
+ def ontario_tax(taxable_income, year=2026, other_credits=0):
61
+ """Ontario tax including surtax, after the low-income tax reduction.
62
+
63
+ The Ontario Health Premium is calculated separately.
64
+ """
65
+ r = _rates(ONTARIO, year)
66
+ basic = ontario_basic_tax(taxable_income, year, other_credits)
67
+ tax = basic + ontario_surtax(basic, year)
68
+ reduction = max(0, 2 * r["tax_reduction"] - tax)
69
+ reduction = min(reduction, tax)
70
+ return round(tax - reduction, 2)
71
+
72
+
73
+ def ontario_health_premium(taxable_income, year=2026):
74
+ """Ontario Health Premium: $0 up to $900, based on taxable income.
75
+
76
+ Each level ramps up at a rate above its threshold, then flattens at a cap.
77
+ """
78
+ r = _rates(ONTARIO, year)
79
+ premium = 0.0
80
+ for threshold, rate, base, cap in r["health_premium"]:
81
+ if taxable_income > threshold:
82
+ premium = min(cap, base + rate * (taxable_income - threshold))
83
+ return round(premium, 2)
84
+
85
+
86
+ def cpp_contributions(employment_income, year=2026):
87
+ """Employee CPP and CPP2 contributions on employment income.
88
+
89
+ Returns a dict:
90
+ cpp - CPP on earnings between the exemption and the YMPE
91
+ cpp2 - CPP2 on earnings between the YMPE and the YAMPE
92
+ base - part of cpp that earns a tax credit
93
+ enhanced - part of cpp that is a deduction from income
94
+ """
95
+ r = _rates(PAYROLL, year)
96
+ pensionable = min(employment_income, r["ympe"]) - r["cpp_exemption"]
97
+ pensionable = max(0.0, pensionable)
98
+ cpp = round(pensionable * r["cpp_rate"], 2)
99
+ base = round(pensionable * r["cpp_base_rate"], 2)
100
+ cpp2_earnings = max(0.0, min(employment_income, r["yampe"]) - r["ympe"])
101
+ cpp2 = round(cpp2_earnings * r["cpp2_rate"], 2)
102
+ return {"cpp": cpp, "cpp2": cpp2, "base": base, "enhanced": round(cpp - base, 2)}
103
+
104
+
105
+ def ei_premium(employment_income, year=2026):
106
+ """Employee EI premium on employment income."""
107
+ r = _rates(PAYROLL, year)
108
+ return round(min(employment_income, r["ei_max_insurable"]) * r["ei_rate"], 2)
109
+
110
+
111
+ def total_tax(taxable_income, year=2026, other_credits=0):
112
+ """Summary of federal + Ontario tax for one taxable income."""
113
+ def tax_at(income):
114
+ return (federal_tax(income, year, other_credits)
115
+ + ontario_tax(income, year, other_credits)
116
+ + ontario_health_premium(income, year))
117
+
118
+ fed = federal_tax(taxable_income, year, other_credits)
119
+ on = ontario_tax(taxable_income, year, other_credits)
120
+ ohp = ontario_health_premium(taxable_income, year)
121
+ total = round(fed + on + ohp, 2)
122
+ return {
123
+ "year": year,
124
+ "income": taxable_income,
125
+ "federal": fed,
126
+ "ontario": on,
127
+ "ontario_health_premium": ohp,
128
+ "total": total,
129
+ "after_tax_income": round(taxable_income - total, 2),
130
+ "average_rate": round(total / taxable_income, 4) if taxable_income else 0.0,
131
+ "marginal_rate": round((tax_at(taxable_income + 100) - total) / 100, 4),
132
+ }
133
+
134
+
135
+ def take_home(employment_income, year=2026):
136
+ """Take-home pay for an Ontario employee: income tax, CPP and EI.
137
+
138
+ How CPP and EI affect income tax:
139
+ - the enhanced part of CPP and all of CPP2 are deducted from income
140
+ - base CPP and EI premiums earn non-refundable tax credits
141
+ """
142
+ c = cpp_contributions(employment_income, year)
143
+ ei = ei_premium(employment_income, year)
144
+ taxable = round(employment_income - c["enhanced"] - c["cpp2"], 2)
145
+ credits = c["base"] + ei
146
+ t = total_tax(taxable, year, other_credits=credits)
147
+ deductions = round(t["total"] + c["cpp"] + c["cpp2"] + ei, 2)
148
+ return {
149
+ "year": year,
150
+ "employment_income": employment_income,
151
+ "taxable_income": taxable,
152
+ "federal": t["federal"],
153
+ "ontario": t["ontario"],
154
+ "ontario_health_premium": t["ontario_health_premium"],
155
+ "income_tax": t["total"],
156
+ "cpp": c["cpp"],
157
+ "cpp2": c["cpp2"],
158
+ "ei": ei,
159
+ "total_deductions": deductions,
160
+ "take_home": round(employment_income - deductions, 2),
161
+ "monthly_take_home": round((employment_income - deductions) / 12, 2),
162
+ "average_rate": round(deductions / employment_income, 4) if employment_income else 0.0,
163
+ }
@@ -0,0 +1,68 @@
1
+ """Tax rates and thresholds by year.
2
+
3
+ Sources (2026): CRA federal rates, CRA T4127 payroll formulas, Ontario TD1ON.
4
+ When adding a new year, copy the latest block, update every number,
5
+ and add a test that checks a known example from an official source.
6
+ """
7
+
8
+ FEDERAL = {
9
+ 2026: {
10
+ # (upper limit of bracket, rate); None = no upper limit
11
+ "brackets": [
12
+ (58_523, 0.14),
13
+ (117_045, 0.205),
14
+ (181_440, 0.26),
15
+ (258_482, 0.29),
16
+ (None, 0.33),
17
+ ],
18
+ "bpa_max": 16_452, # basic personal amount, full
19
+ "bpa_min": 14_829, # BPA once income reaches top bracket
20
+ "bpa_phaseout_start": 181_440,
21
+ "bpa_phaseout_end": 258_482,
22
+ "credit_rate": 0.14,
23
+ },
24
+ }
25
+
26
+ ONTARIO = {
27
+ 2026: {
28
+ "brackets": [
29
+ (53_891, 0.0505),
30
+ (107_785, 0.0915),
31
+ (150_000, 0.1116), # not indexed for inflation
32
+ (220_000, 0.1216), # not indexed for inflation
33
+ (None, 0.1316),
34
+ ],
35
+ "bpa": 12_989,
36
+ "credit_rate": 0.0505,
37
+ # surtax is charged on basic Ontario tax, not on income
38
+ "surtax": [(5_818, 0.20), (7_446, 0.36)],
39
+ # Ontario tax reduction (low income), basic amount for an individual
40
+ # Source: CRA form ON428, Part C; EY Ontario Budget 2026 summary
41
+ "tax_reduction": 300,
42
+ # Ontario Health Premium: (threshold, rate, base, cap)
43
+ # Source: Ontario Taxation Act, 2007; CRA form ON428
44
+ "health_premium": [
45
+ (20_000, 0.06, 0, 300),
46
+ (36_000, 0.06, 300, 450),
47
+ (48_000, 0.25, 450, 600),
48
+ (72_000, 0.25, 600, 750),
49
+ (200_000, 0.25, 750, 900),
50
+ ],
51
+ },
52
+ }
53
+
54
+ # Employee payroll contributions: CPP and EI
55
+ # Source: CRA 2026 CPP and EI rates and maximums
56
+ PAYROLL = {
57
+ 2026: {
58
+ "cpp_exemption": 3_500,
59
+ "ympe": 74_600, # CPP ceiling
60
+ "cpp_rate": 0.0595, # employee rate, $3,500 to YMPE
61
+ "cpp_base_rate": 0.0495, # part of cpp_rate that earns a tax credit;
62
+ # the other 1% is a deduction from income
63
+ "yampe": 85_000, # CPP2 ceiling
64
+ "cpp2_rate": 0.04, # employee rate, YMPE to YAMPE (a deduction)
65
+ "ei_max_insurable": 68_900,
66
+ "ei_rate": 0.0163, # EI premiums earn a tax credit
67
+ },
68
+ }
@@ -0,0 +1,40 @@
1
+ """Tests use worked examples published by tax reference sites / CRA formulas."""
2
+
3
+ from canadatax import bracket_tax, federal_bpa, federal_tax, ontario_tax
4
+ from canadatax.calculator import ontario_basic_tax, ontario_surtax
5
+ from canadatax.rates import FEDERAL
6
+
7
+
8
+ def test_first_federal_bracket():
9
+ # 14% of the first $58,523 = $8,193.22
10
+ assert round(bracket_tax(58_523, FEDERAL[2026]["brackets"]), 2) == 8193.22
11
+
12
+
13
+ def test_federal_bpa_phaseout():
14
+ assert federal_bpa(100_000) == 16_452
15
+ assert federal_bpa(300_000) == 14_829
16
+ assert 14_829 < federal_bpa(220_000) < 16_452
17
+
18
+
19
+ def test_ontario_150k_basic_and_surtax():
20
+ basic = ontario_basic_tax(150_000)
21
+ assert round(basic, 2) == 11708.05
22
+ assert round(ontario_surtax(basic), 2) == round(1178.01 + 1534.34, 2)
23
+
24
+
25
+ def test_ontario_75k_no_surtax():
26
+ basic = ontario_basic_tax(75_000)
27
+ assert abs(basic - 3997.03) < 0.01 # source rounds each bracket
28
+
29
+
30
+ def test_low_income_pays_no_tax():
31
+ assert federal_tax(10_000) == 0
32
+ assert ontario_tax(10_000) == 0
33
+
34
+
35
+ def test_unknown_year_raises():
36
+ try:
37
+ federal_tax(50_000, year=1999)
38
+ except ValueError:
39
+ return
40
+ raise AssertionError("expected ValueError")
@@ -0,0 +1,32 @@
1
+ """Ontario Health Premium tests. Expected values calculated by hand from the ON428 table."""
2
+
3
+ from canadatax import ontario_health_premium, total_tax
4
+
5
+
6
+ def test_ohp_examples():
7
+ cases = {
8
+ 18_000: 0, # below $20,000
9
+ 22_000: 120, # 6% of $2,000
10
+ 30_000: 300, # flat $300 band
11
+ 37_000: 360, # $300 + 6% of $1,000
12
+ 48_200: 500, # $450 + 25% of $200
13
+ 100_000: 750, # flat $750 band
14
+ 250_000: 900, # maximum
15
+ }
16
+ for income, expected in cases.items():
17
+ assert ontario_health_premium(income) == expected, income
18
+
19
+
20
+ def test_ohp_boundaries():
21
+ assert ontario_health_premium(20_000) == 0
22
+ assert ontario_health_premium(25_000) == 300
23
+ assert ontario_health_premium(38_500) == 450
24
+ assert ontario_health_premium(48_600) == 600
25
+ assert ontario_health_premium(72_600) == 750
26
+ assert ontario_health_premium(200_600) == 900
27
+
28
+
29
+ def test_total_includes_ohp():
30
+ r = total_tax(75_000)
31
+ assert r["ontario_health_premium"] == 750
32
+ assert r["total"] == round(r["federal"] + r["ontario"] + 750, 2)
@@ -0,0 +1,44 @@
1
+ """CPP, CPP2 and EI tests. Expected values calculated by hand from 2026 CRA rates."""
2
+
3
+ from canadatax import cpp_contributions, ei_premium, take_home, total_tax
4
+
5
+
6
+ def test_cpp_50k():
7
+ c = cpp_contributions(50_000)
8
+ assert c["cpp"] == 2766.75 # (50,000 - 3,500) x 5.95%
9
+ assert c["cpp2"] == 0 # below the YMPE
10
+ assert c["base"] == 2301.75 # (50,000 - 3,500) x 4.95%
11
+ assert c["enhanced"] == 465.00 # (50,000 - 3,500) x 1%
12
+
13
+
14
+ def test_cpp_80k():
15
+ c = cpp_contributions(80_000)
16
+ assert c["cpp"] == 4230.45 # maximum
17
+ assert c["cpp2"] == 216.00 # (80,000 - 74,600) x 4%
18
+
19
+
20
+ def test_cpp_maximums():
21
+ c = cpp_contributions(150_000)
22
+ assert c["cpp"] == 4230.45
23
+ assert c["cpp2"] == 416.00
24
+
25
+
26
+ def test_cpp_below_exemption():
27
+ assert cpp_contributions(3_000)["cpp"] == 0
28
+
29
+
30
+ def test_ei():
31
+ assert ei_premium(50_000) == 815.00 # 50,000 x 1.63%
32
+ assert ei_premium(80_000) == 1123.07 # maximum
33
+
34
+
35
+ def test_take_home_adds_up():
36
+ r = take_home(75_000)
37
+ parts = r["income_tax"] + r["cpp"] + r["cpp2"] + r["ei"]
38
+ assert abs(r["total_deductions"] - parts) < 0.02
39
+ assert abs(r["take_home"] - (75_000 - r["total_deductions"])) < 0.02
40
+
41
+
42
+ def test_cpp_and_ei_lower_income_tax():
43
+ # credits and the CPP deduction mean less income tax than ignoring them
44
+ assert take_home(75_000)["income_tax"] < total_tax(75_000)["total"]
@@ -0,0 +1,26 @@
1
+ """Ontario low-income tax reduction tests. Expected values calculated by hand.
2
+
3
+ Hand calculations round each step to the cent (like the ON428 form),
4
+ so results may differ from the code by a cent.
5
+ """
6
+
7
+ from canadatax import ontario_tax
8
+
9
+
10
+ def close(a, b):
11
+ return abs(a - b) < 0.02
12
+
13
+
14
+ def test_reduction_examples():
15
+ assert ontario_tax(15_000) == 0 # reduction bigger than tax
16
+ assert close(ontario_tax(20_000), 108.12) # partial reduction
17
+ assert close(ontario_tax(22_000), 310.12) # partial reduction
18
+ assert close(ontario_tax(30_000), 859.06) # no reduction
19
+
20
+
21
+ def test_no_ontario_tax_up_to_18930():
22
+ assert ontario_tax(18_900) == 0
23
+
24
+
25
+ def test_no_reduction_above_24870():
26
+ assert close(ontario_tax(25_000), 606.56)