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.
- canada_tax_toolkit-0.2.0/LICENSE +21 -0
- canada_tax_toolkit-0.2.0/PKG-INFO +58 -0
- canada_tax_toolkit-0.2.0/README.md +46 -0
- canada_tax_toolkit-0.2.0/pyproject.toml +19 -0
- canada_tax_toolkit-0.2.0/setup.cfg +4 -0
- canada_tax_toolkit-0.2.0/src/canada_tax_toolkit.egg-info/PKG-INFO +58 -0
- canada_tax_toolkit-0.2.0/src/canada_tax_toolkit.egg-info/SOURCES.txt +15 -0
- canada_tax_toolkit-0.2.0/src/canada_tax_toolkit.egg-info/dependency_links.txt +1 -0
- canada_tax_toolkit-0.2.0/src/canada_tax_toolkit.egg-info/top_level.txt +1 -0
- canada_tax_toolkit-0.2.0/src/canadatax/__init__.py +19 -0
- canada_tax_toolkit-0.2.0/src/canadatax/__main__.py +28 -0
- canada_tax_toolkit-0.2.0/src/canadatax/calculator.py +163 -0
- canada_tax_toolkit-0.2.0/src/canadatax/rates.py +68 -0
- canada_tax_toolkit-0.2.0/tests/test_calculator.py +40 -0
- canada_tax_toolkit-0.2.0/tests/test_health_premium.py +32 -0
- canada_tax_toolkit-0.2.0/tests/test_payroll.py +44 -0
- canada_tax_toolkit-0.2.0/tests/test_tax_reduction.py +26 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
canadatax
|
|
@@ -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)
|