diff --git a/.claude/subagents/README.md b/.claude/subagents/README.md new file mode 100644 index 000000000..d771ac40d --- /dev/null +++ b/.claude/subagents/README.md @@ -0,0 +1,177 @@ +# PolicyEngine Rules PR Reviewer Subagent + +## Overview +This subagent specializes in reviewing PolicyEngine pull requests that implement or modify tax and benefit rules. It ensures accuracy against source documents, validates calculations, and checks for completeness. + +## Installation +To make this subagent available across all your PolicyEngine repositories: + +1. Copy the `.claude/subagents` folder to your home directory or a shared location +2. Set the environment variable: `export CLAUDE_SUBAGENTS_PATH=~/.claude/subagents` +3. The subagent will be automatically available in all Claude sessions + +## Usage + +### Activating the Subagent +The subagent activates automatically when you: +- Ask to "review a PolicyEngine PR" +- Request to "validate parameters against sources" +- Say "check if this calculation is correct" +- Work with files in `parameters/`, `variables/`, or `tests/` directories + +### Example Commands + +``` +"Review PR #521 that implements EI and CPP payroll taxes" +"Validate the GIS parameters against official government sources" +"Check if the Climate Action Incentive calculation matches the legislation" +"Verify all test cases in this PR by hand calculation" +``` + +### What It Reviews + +#### 1. Parameter Verification +- Checks every parameter value against official sources +- Verifies effective dates match policy implementation +- Validates all reference URLs + +#### 2. Calculation Logic +- Steps through formulas to ensure they match legislation +- Checks edge cases and boundary conditions +- Verifies calculations happen at correct entity level + +#### 3. Completeness +- Identifies missing eligibility criteria +- Checks for special population handling +- Verifies geographic variations are covered +- Ensures proper time period handling + +#### 4. Test Validation +- Manually calculates each test case +- Shows step-by-step work +- Identifies gaps in test coverage +- Suggests additional test scenarios + +#### 5. Documentation +- Reviews parameter descriptions +- Checks variable docstrings +- Verifies changelog entries +- Ensures references are complete + +## Output Format + +The subagent provides a structured review with: + +### ✅ Verified Against Sources +Lists all parameters/rules that match official documentation + +### ⚠️ Discrepancies Found +Highlights any differences between PR and source documents + +### 🔍 Missing Components +Identifies policy aspects not implemented + +### 📊 Test Case Verification +Shows hand calculations for each test with pass/fail status + +### 📝 Documentation Issues +Lists missing or unclear documentation + +### 💡 Recommendations +Provides actionable suggestions for improvement + +### Confidence Level +Rates review confidence (High/Medium/Low) with explanation + +## Example Review + +```markdown +## PR Review: Implement EI and CPP payroll taxes + +### ✅ Verified Against Sources +- EI rate 2024 (0.0166): Matches CRA source +- CPP maximum 2024 ($71,300): Matches official rates +- Basic exemption ($3,500): Correct per legislation + +### ⚠️ Discrepancies Found +- CPP2 rate: PR has 0.04, but source shows 0.0400 (formatting) + +### 🔍 Missing Components +- Not implemented: Quebec separate QPP system +- Missing: EI fishing benefits special rules + +### 📊 Test Case Verification +#### Test: Employee with $50,000 income +**Hand Calculation:** +- Step 1: Pensionable = $50,000 - $3,500 = $46,500 +- Step 2: CPP = $46,500 × 0.0595 = $2,766.75 +- Result: $2,766.75 +- Test expects: $2,766.75 +- Status: ✅ Match + +### 📝 Documentation Issues +- Missing: Explanation of basic exemption application +- Unclear: How mixed employment/self-employment handled + +### 💡 Recommendations +1. Add Quebec QPP as separate parameter tree +2. Include tests for maximum contribution scenarios +3. Document the exemption allocation methodology + +### Confidence Level: High +Based on direct verification against CRA T4127 document +``` + +## Special Features + +### Country-Specific Validation + +#### Canada +- Checks federal vs provincial jurisdiction +- Verifies Quebec special handling +- Validates both English and French sources + +#### United States +- Validates federal vs state rules +- Checks AMT implications +- Verifies filing status variations + +#### United Kingdom +- Checks Scotland/Wales/NI variations +- Validates Universal Credit interactions +- Verifies taper rates + +### Red Flags It Catches +- Hardcoded values that should be parameters +- Missing inflation adjustments +- Incorrect period conversions +- Oversimplified phase-outs +- Missing means-testing +- Ignored household composition + +## Extending the Subagent + +To add new validation rules: + +1. Edit `policyengine_rules_reviewer.py` +2. Add new check methods +3. Update the review process +4. Add to configuration in `config.yaml` + +## Troubleshooting + +If the subagent doesn't activate: +- Check the activation patterns in `config.yaml` +- Ensure file paths match expected patterns +- Verify the subagent is enabled + +## Contributing + +To improve the subagent: +1. Add new validation patterns +2. Enhance source document parsing +3. Improve calculation verification +4. Add support for more jurisdictions + +## License +Same as PolicyEngine repositories (AGPL-3.0) \ No newline at end of file diff --git a/.claude/subagents/config.yaml b/.claude/subagents/config.yaml new file mode 100644 index 000000000..75ef8eaef --- /dev/null +++ b/.claude/subagents/config.yaml @@ -0,0 +1,95 @@ +subagents: + policyengine-rules-reviewer: + name: PolicyEngine Rules PR Reviewer + description: Reviews PolicyEngine pull requests implementing tax/benefit rules for accuracy and completeness + version: 1.0.0 + enabled: true + + activation: + # Activate when these patterns are found in user requests + patterns: + - "review.*PR" + - "check.*PR" + - "validate.*PR" + - "verify.*parameter" + - "check.*calculation" + - "review.*implementation" + - "validate.*rules" + - "PolicyEngine.*PR" + + # Also activate for these file patterns in PRs + file_patterns: + - "parameters/**/*.yaml" + - "variables/**/benefits/*.py" + - "variables/**/tax/*.py" + - "tests/**/*.yaml" + + capabilities: + source_verification: + description: Verify parameter values against government sources + tools_used: + - WebFetch + - WebSearch + + calculation_validation: + description: Validate formulas match legislation + tools_used: + - Read + - Python evaluation + + completeness_checking: + description: Identify missing policy components + tools_used: + - Grep + - Read + + test_reconstruction: + description: Manually calculate test cases + tools_used: + - Mathematical calculation + + documentation_review: + description: Check documentation quality + tools_used: + - Read + - Markdown validation + + review_scope: + countries: + - CA # Canada + - US # United States + - UK # United Kingdom + + domains: + - tax_calculation + - benefit_calculation + - eligibility_determination + - parameter_updates + - formula_implementation + + output: + format: markdown + sections: + - source_verification + - discrepancies + - missing_components + - test_validation + - documentation_issues + - recommendations + - confidence_level + + quality_checks: + - parameter_accuracy + - formula_correctness + - test_coverage + - documentation_completeness + - edge_case_handling + - period_handling + - entity_level_correctness + + escalation_triggers: + - ambiguous_source_documentation + - conflicting_interpretations + - high_monetary_impact + - legal_compliance_issues + - cross_jurisdiction_complexity \ No newline at end of file diff --git a/.claude/subagents/policyengine-rules-reviewer.md b/.claude/subagents/policyengine-rules-reviewer.md new file mode 100644 index 000000000..c7175e749 --- /dev/null +++ b/.claude/subagents/policyengine-rules-reviewer.md @@ -0,0 +1,159 @@ +# PolicyEngine Rules PR Reviewer + +## Purpose +Review PolicyEngine pull requests that implement or modify tax/benefit rules to ensure accuracy, completeness, and proper documentation. + +## Activation +Use this subagent when: +- Reviewing PRs that add or modify parameters (YAML files) +- Reviewing PRs that add or modify benefit/tax calculation logic (Python variables) +- User asks to review a PolicyEngine PR for accuracy +- A PR implements new government programs or updates existing ones + +## Review Process + +### 1. Source Document Verification +- **Fetch and examine primary sources**: Use WebFetch/WebSearch to access government documentation referenced in the PR +- **Cross-reference all parameter values**: Verify each number against official sources +- **Check effective dates**: Ensure parameter date ranges match policy implementation dates +- **Validate references**: Confirm all URLs work and point to authoritative sources + +### 2. Calculation Logic Review +- **Trace the formula**: Step through each calculation to ensure it matches the legislation +- **Check edge cases**: Consider boundary conditions, phase-outs, and special cases +- **Verify entity levels**: Ensure calculations happen at correct level (Person/Household) +- **Review variable dependencies**: Confirm all required inputs are available + +### 3. Completeness Assessment +Identify missing elements: +- **Eligibility criteria**: Age limits, residency requirements, income tests +- **Special populations**: Disabilities, students, seniors, children +- **Geographic variations**: Provincial/state differences +- **Time variations**: Monthly vs annual calculations, carry-forwards +- **Interactions**: Benefit stacking, mutual exclusivity rules +- **Phase-ins/outs**: Gradual implementation or sunset provisions + +### 4. Test Case Validation +Manually calculate each test case: +- **Show your work**: Provide step-by-step calculations +- **Verify test coverage**: Ensure tests cover typical cases, edge cases, and phase-outs +- **Check test values**: Confirm expected outputs match hand calculations +- **Suggest additional tests**: Identify untested scenarios + +### 5. Documentation Review +- **Parameter documentation**: Check descriptions, units, labels +- **Variable documentation**: Verify docstrings explain the calculation +- **Changelog**: Ensure changes are properly documented +- **User-facing impact**: Consider how changes affect end users + +## Output Format + +Provide a structured review with: + +```markdown +## PR Review: [Title] + +### ✅ Verified Against Sources +- [Parameter/Rule]: Matches [Source Document] Section X +- ... + +### ⚠️ Discrepancies Found +- [Parameter/Rule]: PR has X, but [Source] shows Y +- ... + +### 🔍 Missing Components +- Not implemented: [Feature] described in [Source] +- ... + +### 📊 Test Case Verification +#### Test: [Name] +**Hand Calculation:** +- Step 1: ... +- Step 2: ... +- Result: $X +- Test expects: $Y +- Status: ✅ Match / ❌ Mismatch + +### 📝 Documentation Issues +- Missing: ... +- Unclear: ... + +### 💡 Recommendations +1. ... +2. ... + +### Confidence Level: [High/Medium/Low] +Based on [explain factors affecting confidence] +``` + +## Special Considerations + +### For Canadian Rules +- Check both federal and provincial components +- Verify French/English documentation consistency +- Consider Quebec's separate tax system +- Check for First Nations tax exemptions + +### For US Rules +- Verify federal vs state jurisdiction +- Check for AMT (Alternative Minimum Tax) implications +- Consider filing status variations +- Review phase-out calculations carefully + +### For UK Rules +- Check for Scotland/Wales/NI variations +- Verify Universal Credit interactions +- Consider taper rates and work allowances + +## Tools to Use +- WebFetch: Get official documentation +- WebSearch: Find current rates and thresholds +- Read: Examine implementation files +- Grep: Search for related variables +- Calculator: Verify arithmetic + +## Red Flags to Watch For +- Hardcoded values without parameters +- Missing inflation adjustments +- Incorrect period conversions (monthly/annual) +- Missing means-testing +- Ignored household composition rules +- Oversimplified phase-outs +- Missing clawbacks or benefit recovery taxes + +## Example Review Scenarios + +### Scenario 1: New Benefit Implementation +1. Find authoritative source (legislation/regulation) +2. List all eligibility criteria from source +3. Check each criterion is implemented +4. Verify benefit calculation formula +5. Ensure all edge cases handled + +### Scenario 2: Parameter Update +1. Verify new values against official announcements +2. Check if related parameters need updating +3. Confirm effective dates +4. Test retroactive calculations if applicable + +### Scenario 3: Bug Fix +1. Understand the reported issue +2. Verify the fix addresses root cause +3. Check for unintended side effects +4. Ensure tests prevent regression + +## Quality Metrics +Rate the PR on: +- **Accuracy**: Do calculations match legislation exactly? +- **Completeness**: Are all aspects of the policy implemented? +- **Testing**: Is test coverage comprehensive? +- **Documentation**: Will future maintainers understand the code? +- **Performance**: Are calculations efficient? + +## When to Escalate +Flag for human review if: +- Source documents are ambiguous +- Multiple valid interpretations exist +- Significant amounts are at stake +- Legal/compliance implications +- Cross-jurisdiction complications \ No newline at end of file diff --git a/.claude/subagents/policyengine_rules_reviewer.py b/.claude/subagents/policyengine_rules_reviewer.py new file mode 100644 index 000000000..704c9d004 --- /dev/null +++ b/.claude/subagents/policyengine_rules_reviewer.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +PolicyEngine Rules PR Reviewer Subagent + +A specialized agent for reviewing PolicyEngine pull requests that implement +or modify tax and benefit rules, ensuring accuracy against source documents. +""" + +import json +from typing import Dict, List, Any, Optional +from dataclasses import dataclass +from enum import Enum + + +class ReviewConfidence(Enum): + HIGH = "High" + MEDIUM = "Medium" + LOW = "Low" + + +@dataclass +class ParameterVerification: + """Result of verifying a parameter against source documents""" + parameter_name: str + pr_value: Any + source_value: Any + source_reference: str + matches: bool + notes: Optional[str] = None + + +@dataclass +class TestCalculation: + """Hand calculation of a test case""" + test_name: str + steps: List[str] + calculated_result: float + expected_result: float + matches: bool + discrepancy: Optional[float] = None + + +@dataclass +class PRReview: + """Complete review of a PolicyEngine PR""" + pr_title: str + pr_number: int + verified_params: List[ParameterVerification] + missing_components: List[str] + test_validations: List[TestCalculation] + documentation_issues: List[str] + recommendations: List[str] + confidence: ReviewConfidence + confidence_explanation: str + + +class PolicyEngineRulesReviewer: + """ + Subagent for reviewing PolicyEngine PRs implementing tax/benefit rules. + + This agent: + 1. Verifies parameters against source documents + 2. Validates calculation logic + 3. Checks completeness of implementation + 4. Reconstructs test cases by hand + 5. Reviews documentation + """ + + def __init__(self): + self.review_checklist = { + "source_verification": False, + "logic_validation": False, + "completeness_check": False, + "test_validation": False, + "documentation_review": False + } + + def should_activate(self, context: str) -> bool: + """Determine if this subagent should be used""" + triggers = [ + "review pr", "check pr", "validate pr", + "verify parameter", "check calculation", + "review implementation", "validate rules", + "tax pr", "benefit pr", "policyengine pr" + ] + context_lower = context.lower() + return any(trigger in context_lower for trigger in triggers) + + def verify_parameter( + self, + param_path: str, + pr_value: Any, + source_doc_url: str + ) -> ParameterVerification: + """ + Verify a parameter value against source documentation. + + Args: + param_path: Path to parameter (e.g., 'gov.cra.benefits.gis.amount.single') + pr_value: Value in the PR + source_doc_url: URL of authoritative source + + Returns: + ParameterVerification result + """ + # This would use WebFetch to get source and extract value + # Placeholder for demonstration + return ParameterVerification( + parameter_name=param_path, + pr_value=pr_value, + source_value=pr_value, # Would be extracted from source + source_reference=source_doc_url, + matches=True, + notes=None + ) + + def calculate_test_case( + self, + test_name: str, + inputs: Dict[str, Any], + formula: str, + parameters: Dict[str, Any] + ) -> TestCalculation: + """ + Manually calculate a test case step by step. + + Args: + test_name: Name of the test + inputs: Input values for the test + formula: The calculation formula + parameters: Parameter values to use + + Returns: + TestCalculation with step-by-step work + """ + steps = [] + # This would implement step-by-step calculation + # Placeholder for demonstration + return TestCalculation( + test_name=test_name, + steps=steps, + calculated_result=0.0, + expected_result=0.0, + matches=True + ) + + def check_completeness( + self, + implementation: Dict[str, Any], + source_requirements: List[str] + ) -> List[str]: + """ + Check if implementation covers all requirements from source. + + Args: + implementation: What's implemented in the PR + source_requirements: Requirements from legislation/docs + + Returns: + List of missing components + """ + missing = [] + # Compare implementation against requirements + return missing + + def review_pr( + self, + pr_url: str, + focus_areas: Optional[List[str]] = None + ) -> PRReview: + """ + Perform complete review of a PolicyEngine PR. + + Args: + pr_url: GitHub PR URL + focus_areas: Specific areas to focus on + + Returns: + Complete PRReview object + """ + # This would orchestrate the full review process + # Using various verification methods + pass + + def format_review_markdown(self, review: PRReview) -> str: + """Format review results as markdown""" + md = f"""## PR Review: {review.pr_title} + +### ✅ Verified Against Sources +""" + for param in review.verified_params: + if param.matches: + md += f"- {param.parameter_name}: Matches {param.source_reference}\n" + + if any(not p.matches for p in review.verified_params): + md += "\n### ⚠️ Discrepancies Found\n" + for param in review.verified_params: + if not param.matches: + md += f"- {param.parameter_name}: PR has {param.pr_value}, but source shows {param.source_value}\n" + + if review.missing_components: + md += "\n### 🔍 Missing Components\n" + for component in review.missing_components: + md += f"- {component}\n" + + md += "\n### 📊 Test Case Verification\n" + for test in review.test_validations: + md += f"\n#### Test: {test.test_name}\n" + md += "**Hand Calculation:**\n" + for i, step in enumerate(test.steps, 1): + md += f"- Step {i}: {step}\n" + md += f"- Result: ${test.calculated_result:,.2f}\n" + md += f"- Test expects: ${test.expected_result:,.2f}\n" + status = "✅ Match" if test.matches else f"❌ Mismatch (diff: ${test.discrepancy:,.2f})" + md += f"- Status: {status}\n" + + if review.documentation_issues: + md += "\n### 📝 Documentation Issues\n" + for issue in review.documentation_issues: + md += f"- {issue}\n" + + if review.recommendations: + md += "\n### 💡 Recommendations\n" + for i, rec in enumerate(review.recommendations, 1): + md += f"{i}. {rec}\n" + + md += f""" +### Confidence Level: {review.confidence.value} +{review.confidence_explanation} +""" + return md + + +# Subagent metadata for registration +SUBAGENT_METADATA = { + "name": "policyengine-rules-reviewer", + "description": "Reviews PolicyEngine PRs for accuracy against source documents", + "version": "1.0.0", + "author": "PolicyEngine", + "capabilities": [ + "parameter_verification", + "calculation_validation", + "test_reconstruction", + "completeness_checking", + "documentation_review" + ], + "supported_countries": ["CA", "US", "UK"], + "activation_keywords": [ + "review PR", "validate rules", "check parameters", + "verify calculation", "review implementation" + ] +} + + +def create_reviewer() -> PolicyEngineRulesReviewer: + """Factory function to create a reviewer instance""" + return PolicyEngineRulesReviewer() + + +if __name__ == "__main__": + # Example usage + reviewer = create_reviewer() + + # Example: Check if should activate + should_review = reviewer.should_activate("Please review this PolicyEngine PR") + print(f"Should activate: {should_review}") + + # Example: Verify a parameter + param_check = reviewer.verify_parameter( + param_path="gov.cra.benefits.gis.amount.single", + pr_value=1097.75, + source_doc_url="https://www.canada.ca/en/services/benefits/publicpensions/cpp/old-age-security/payments.html" + ) + print(f"Parameter verified: {param_check.matches}") \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/cpp/basic_exemption.yaml b/policyengine_canada/parameters/gov/cra/payroll/cpp/basic_exemption.yaml new file mode 100644 index 000000000..553947905 --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/cpp/basic_exemption.yaml @@ -0,0 +1,10 @@ +description: Basic exemption amount for Canada Pension Plan contributions +values: + 2020-01-01: 3_500 +metadata: + unit: currency-CAD + period: year + label: CPP basic exemption + reference: + - title: CPP contribution rates, maximums and exemptions + href: https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/payroll/payroll-deductions-contributions/canada-pension-plan-cpp/cpp-contribution-rates-maximums-exemptions.html \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/cpp/maximum_pensionable_earnings.yaml b/policyengine_canada/parameters/gov/cra/payroll/cpp/maximum_pensionable_earnings.yaml new file mode 100644 index 000000000..1406d89d2 --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/cpp/maximum_pensionable_earnings.yaml @@ -0,0 +1,15 @@ +description: Year's Maximum Pensionable Earnings (YMPE) for Canada Pension Plan +values: + 2020-01-01: 58_700 + 2021-01-01: 61_600 + 2022-01-01: 64_900 + 2023-01-01: 66_600 + 2024-01-01: 68_500 + 2025-01-01: 71_300 +metadata: + unit: currency-CAD + period: year + label: CPP maximum pensionable earnings (YMPE) + reference: + - title: CPP contribution rates, maximums and exemptions + href: https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/payroll/payroll-deductions-contributions/canada-pension-plan-cpp/cpp-contribution-rates-maximums-exemptions.html \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/cpp/rate.yaml b/policyengine_canada/parameters/gov/cra/payroll/cpp/rate.yaml new file mode 100644 index 000000000..c2f5c27cd --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/cpp/rate.yaml @@ -0,0 +1,16 @@ +description: Canada Pension Plan contribution rate for employees and employers +values: + 2020-01-01: 0.0525 + 2021-01-01: 0.0545 + 2022-01-01: 0.057 + 2023-01-01: 0.0595 + 2024-01-01: 0.0595 + 2025-01-01: 0.0595 +metadata: + unit: /1 + label: CPP contribution rate + reference: + - title: CPP contribution rates, maximums and exemptions + href: https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/payroll/payroll-deductions-contributions/canada-pension-plan-cpp/cpp-contribution-rates-maximums-exemptions.html + - title: Canada Revenue Agency announces maximum pensionable earnings and contributions for 2025 + href: https://www.canada.ca/en/revenue-agency/news/newsroom/tax-tips/tax-tips-2024/canada-revenue-agency-announces-maximum-pensionable-earnings-contributions-2025.html \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/cpp2/additional_maximum_pensionable_earnings.yaml b/policyengine_canada/parameters/gov/cra/payroll/cpp2/additional_maximum_pensionable_earnings.yaml new file mode 100644 index 000000000..7a1c646d0 --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/cpp2/additional_maximum_pensionable_earnings.yaml @@ -0,0 +1,11 @@ +description: Year's Additional Maximum Pensionable Earnings (YAMPE) for CPP2 +values: + 2024-01-01: 73_200 + 2025-01-01: 81_200 +metadata: + unit: currency-CAD + period: year + label: CPP2 additional maximum pensionable earnings (YAMPE) + reference: + - title: Canada Revenue Agency announces maximum pensionable earnings and contributions for 2025 + href: https://www.canada.ca/en/revenue-agency/news/newsroom/tax-tips/tax-tips-2024/canada-revenue-agency-announces-maximum-pensionable-earnings-contributions-2025.html \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/cpp2/rate.yaml b/policyengine_canada/parameters/gov/cra/payroll/cpp2/rate.yaml new file mode 100644 index 000000000..236f33ed2 --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/cpp2/rate.yaml @@ -0,0 +1,12 @@ +description: Second additional Canada Pension Plan (CPP2) contribution rate +values: + 2024-01-01: 0.04 + 2025-01-01: 0.04 +metadata: + unit: /1 + label: CPP2 contribution rate + reference: + - title: CPP and CPP2 explained + href: https://www.cfib-fcei.ca/en/tools-resources/cpp-cpp2-explained + - title: Canada Revenue Agency announces maximum pensionable earnings and contributions for 2025 + href: https://www.canada.ca/en/revenue-agency/news/newsroom/tax-tips/tax-tips-2024/canada-revenue-agency-announces-maximum-pensionable-earnings-contributions-2025.html \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/ei/maximum_insurable_earnings.yaml b/policyengine_canada/parameters/gov/cra/payroll/ei/maximum_insurable_earnings.yaml new file mode 100644 index 000000000..4482b8001 --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/ei/maximum_insurable_earnings.yaml @@ -0,0 +1,17 @@ +description: Maximum annual insurable earnings for Employment Insurance +values: + 2020-01-01: 54_200 + 2021-01-01: 56_300 + 2022-01-01: 60_300 + 2023-01-01: 61_500 + 2024-01-01: 63_200 + 2025-01-01: 65_700 +metadata: + unit: currency-CAD + period: year + label: EI maximum insurable earnings + reference: + - title: EI premium rates and maximums + href: https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/payroll/payroll-deductions-contributions/employment-insurance-ei/ei-premium-rates-maximums.html + - title: Employment Insurance - Important notice about maximum insurable earnings for 2025 + href: https://www.canada.ca/en/employment-social-development/programs/ei/ei-list/ei-employers/premium-reduction-program/2025-maximum-insurable-earnings.html \ No newline at end of file diff --git a/policyengine_canada/parameters/gov/cra/payroll/ei/rate.yaml b/policyengine_canada/parameters/gov/cra/payroll/ei/rate.yaml new file mode 100644 index 000000000..b67c695a6 --- /dev/null +++ b/policyengine_canada/parameters/gov/cra/payroll/ei/rate.yaml @@ -0,0 +1,16 @@ +description: Employment Insurance premium rate per $100 of insurable earnings +values: + 2020-01-01: 0.0158 + 2021-01-01: 0.0158 + 2022-01-01: 0.0158 + 2023-01-01: 0.0163 + 2024-01-01: 0.0166 + 2025-01-01: 0.0164 +metadata: + unit: /1 + label: EI premium rate + reference: + - title: EI premium rates and maximums + href: https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/payroll/payroll-deductions-contributions/employment-insurance-ei/ei-premium-rates-maximums.html + - title: Canada Employment Insurance Commission confirms 2025 Employment Insurance premium rate + href: https://www.canada.ca/en/employment-social-development/news/2024/09/canada-employment-insurance-commission-confirms-2025-employment-insurance-premium-rate.html \ No newline at end of file diff --git a/policyengine_canada/tests/payroll/cpp2_contribution.yaml b/policyengine_canada/tests/payroll/cpp2_contribution.yaml new file mode 100644 index 000000000..92da988a5 --- /dev/null +++ b/policyengine_canada/tests/payroll/cpp2_contribution.yaml @@ -0,0 +1,85 @@ +- name: CPP2 contribution for employee earning above YMPE + period: 2024 + input: + employment_income: 75_000 + self_employment_income: 0 + output: + # 2024: YMPE = $68,500, YAMPE = $73,200 + # CPP2 earnings = min($75,000, $73,200) - $68,500 = $4,700 + # Contribution = $4,700 * 0.04 = $188 + cpp2_contribution: 188 + +- name: CPP2 contribution for self-employed (double rate) + period: 2024 + input: + employment_income: 0 + self_employment_income: 75_000 + output: + # Self-employed pay double rate + # CPP2 earnings = min($75,000, $73,200) - $68,500 = $4,700 + # Contribution = $4,700 * 0.04 * 2 = $376 + cpp2_contribution: 376 + +- name: CPP2 contribution below YMPE (should be zero) + period: 2024 + input: + employment_income: 60_000 + self_employment_income: 0 + output: + cpp2_contribution: 0 + +- name: CPP2 contribution at maximum YAMPE + period: 2024 + input: + employment_income: 100_000 + self_employment_income: 0 + output: + # 2024: YMPE = $68,500, YAMPE = $73,200 + # CPP2 earnings = $73,200 - $68,500 = $4,700 + # Contribution = $4,700 * 0.04 = $188 + cpp2_contribution: 188 + +- name: CPP2 contribution with mixed income + period: 2024 + input: + employment_income: 72_000 + self_employment_income: 2_000 + output: + # Total earnings = $74,000 (capped at YAMPE $73,200) + # CPP2 pensionable = $73,200 - $68,500 = $4,700 + # Employment portion: min($72,000, $73,200) - $68,500 = $3,500 + # Employee contribution = $3,500 * 0.04 = $140 + # Self-employed portion: $4,700 - $3,500 = $1,200 + # Self-employed contribution = $1,200 * 0.04 * 2 = $96 + # Total = $140 + $96 = $236 + cpp2_contribution: 236 + +- name: CPP2 contribution before 2024 (should be zero) + period: 2023 + input: + employment_income: 75_000 + self_employment_income: 0 + output: + cpp2_contribution: 0 + +- name: CPP2 contribution for 2025 with higher YAMPE + period: 2025 + input: + employment_income: 80_000 + self_employment_income: 0 + output: + # 2025: YMPE = $71,300, YAMPE = $81,200 + # CPP2 earnings = min($80,000, $81,200) - $71,300 = $8,700 + # Contribution = $8,700 * 0.04 = $348 + cpp2_contribution: 348 + +- name: CPP2 contribution at maximum for 2025 + period: 2025 + input: + employment_income: 90_000 + self_employment_income: 0 + output: + # 2025: YMPE = $71,300, YAMPE = $81,200 + # CPP2 earnings = $81,200 - $71,300 = $9,900 + # Contribution = $9,900 * 0.04 = $396 + cpp2_contribution: 396 \ No newline at end of file diff --git a/policyengine_canada/tests/payroll/cpp_contribution.yaml b/policyengine_canada/tests/payroll/cpp_contribution.yaml new file mode 100644 index 000000000..5b4ebb42a --- /dev/null +++ b/policyengine_canada/tests/payroll/cpp_contribution.yaml @@ -0,0 +1,64 @@ +- name: CPP contribution for employee + period: 2024 + input: + employment_income: 40_000 + self_employment_income: 0 + output: + cpp_contribution: 2_171.75 + +- name: CPP contribution for self-employed (double rate) + period: 2024 + input: + employment_income: 0 + self_employment_income: 40_000 + output: + cpp_contribution: 4_343.50 + +- name: CPP contribution at maximum pensionable earnings + period: 2024 + input: + employment_income: 100_000 + self_employment_income: 0 + output: + # Maximum pensionable earnings 2024 = $68,500 + # Pensionable = $68,500 - $3,500 = $65,000 + # Contribution = $65,000 * 0.0595 = $3,867.50 + cpp_contribution: 3_867.50 + +- name: CPP contribution with mixed income + period: 2024 + input: + employment_income: 30_000 + self_employment_income: 20_000 + output: + # Employee portion: (30,000 - 3,500) * 0.0595 = 1,576.75 + # Self-employed portion: 20,000 * 0.0595 * 2 = 2,380 + # Total = 3,956.75 + cpp_contribution: 3_956.75 + +- name: CPP contribution below basic exemption + period: 2024 + input: + employment_income: 3_000 + self_employment_income: 0 + output: + cpp_contribution: 0 + +- name: CPP contribution for 2025 + period: 2025 + input: + employment_income: 50_000 + self_employment_income: 0 + output: + cpp_contribution: 2_766.75 + +- name: CPP contribution at maximum for 2025 + period: 2025 + input: + employment_income: 100_000 + self_employment_income: 0 + output: + # Maximum pensionable earnings 2025 = $71,300 + # Pensionable = $71,300 - $3,500 = $67,800 + # Contribution = $67,800 * 0.0595 = $4,034.10 + cpp_contribution: 4_034.10 \ No newline at end of file diff --git a/policyengine_canada/tests/payroll/ei_premium.yaml b/policyengine_canada/tests/payroll/ei_premium.yaml new file mode 100644 index 000000000..a25bf7d3c --- /dev/null +++ b/policyengine_canada/tests/payroll/ei_premium.yaml @@ -0,0 +1,41 @@ +- name: Basic EI premium calculation + period: 2024 + input: + employment_income: 30_000 + output: + ei_premium: 498 + +- name: EI premium at maximum insurable earnings + period: 2024 + input: + employment_income: 100_000 + output: + ei_premium: 1_049.12 + +- name: EI premium for 2025 with new rate + period: 2025 + input: + employment_income: 50_000 + output: + ei_premium: 820 + +- name: No EI premium with no employment income + period: 2024 + input: + employment_income: 0 + output: + ei_premium: 0 + +- name: EI premium at exactly maximum insurable earnings 2024 + period: 2024 + input: + employment_income: 63_200 + output: + ei_premium: 1_049.12 + +- name: EI premium at exactly maximum insurable earnings 2025 + period: 2025 + input: + employment_income: 65_700 + output: + ei_premium: 1_077.48 \ No newline at end of file diff --git a/policyengine_canada/variables/gov/cra/payroll/cpp2_contribution.py b/policyengine_canada/variables/gov/cra/payroll/cpp2_contribution.py new file mode 100644 index 000000000..8822ff2bd --- /dev/null +++ b/policyengine_canada/variables/gov/cra/payroll/cpp2_contribution.py @@ -0,0 +1,56 @@ +from policyengine_canada.model_api import * + + +class cpp2_contribution(Variable): + value_type = float + entity = Person + label = "CPP2 contribution" + definition_period = YEAR + unit = CAD + documentation = "Second additional Canada Pension Plan contribution (enhanced tier)" + + def formula(person, period, parameters): + # CPP2 only started in 2024 + if period.start.year < 2024: + return person.empty_array() + + employment_income = person("employment_income", period) + self_employment_income = person("self_employment_income", period) + p_cpp = parameters(period).gov.cra.payroll.cpp + p_cpp2 = parameters(period).gov.cra.payroll.cpp2 + + # Total earnings + total_earnings = employment_income + self_employment_income + + # CPP2 applies to earnings between YMPE and YAMPE + cpp2_earnings_floor = p_cpp.maximum_pensionable_earnings + cpp2_earnings_ceiling = p_cpp2.additional_maximum_pensionable_earnings + + # Calculate pensionable earnings for CPP2 (earnings between YMPE and YAMPE) + cpp2_pensionable = max_(0, min_(total_earnings, cpp2_earnings_ceiling) - cpp2_earnings_floor) + + if total_earnings == 0 or cpp2_pensionable == 0: + return person.empty_array() + + # Allocate CPP2 pensionable earnings proportionally + # First check how much of total earnings is above YMPE + earnings_above_ympe = max_(0, total_earnings - cpp2_earnings_floor) + + if earnings_above_ympe == 0: + return person.empty_array() + + # Allocate the CPP2 pensionable amount based on which income is above YMPE + employment_above_ympe = max_(0, employment_income - cpp2_earnings_floor) + self_employment_above_ympe = max_(0, earnings_above_ympe - employment_above_ympe) + + # Calculate actual pensionable amounts (capped at YAMPE - YMPE) + employment_cpp2_pensionable = min_(employment_above_ympe, cpp2_pensionable) + self_employment_cpp2_pensionable = min_(self_employment_above_ympe, cpp2_pensionable - employment_cpp2_pensionable) + + # Employment income: employee rate only + employee_cpp2 = employment_cpp2_pensionable * p_cpp2.rate + + # Self-employment income: double rate (employee + employer) + self_employed_cpp2 = self_employment_cpp2_pensionable * p_cpp2.rate * 2 + + return employee_cpp2 + self_employed_cpp2 \ No newline at end of file diff --git a/policyengine_canada/variables/gov/cra/payroll/cpp_contribution.py b/policyengine_canada/variables/gov/cra/payroll/cpp_contribution.py new file mode 100644 index 000000000..e18ce1f2d --- /dev/null +++ b/policyengine_canada/variables/gov/cra/payroll/cpp_contribution.py @@ -0,0 +1,40 @@ +from policyengine_canada.model_api import * + + +class cpp_contribution(Variable): + value_type = float + entity = Person + label = "Canada Pension Plan contribution" + definition_period = YEAR + unit = CAD + documentation = "Annual Canada Pension Plan contribution (first tier)" + + def formula(person, period, parameters): + employment_income = person("employment_income", period) + self_employment_income = person("self_employment_income", period) + p = parameters(period).gov.cra.payroll.cpp + + # Total earnings + total_earnings = employment_income + self_employment_income + + # Apply basic exemption and maximum to total earnings + total_pensionable = max_(0, min_(total_earnings, p.maximum_pensionable_earnings) - p.basic_exemption) + + # The basic exemption is applied first to employment income, then to self-employment + # This matches how it's done in practice for mixed income earners + employment_after_exemption = max_(0, employment_income - p.basic_exemption) + remaining_exemption = max_(0, p.basic_exemption - employment_income) + self_employment_after_exemption = max_(0, self_employment_income - remaining_exemption) + + # Cap at maximum pensionable earnings + employment_pensionable = min_(employment_after_exemption, p.maximum_pensionable_earnings - p.basic_exemption) + remaining_room = max_(0, p.maximum_pensionable_earnings - p.basic_exemption - employment_pensionable) + self_employment_pensionable = min_(self_employment_after_exemption, remaining_room) + + # Employment income: employee rate only + employee_contribution = employment_pensionable * p.rate + + # Self-employment income: double rate (employee + employer) + self_employed_contribution = self_employment_pensionable * p.rate * 2 + + return employee_contribution + self_employed_contribution \ No newline at end of file diff --git a/policyengine_canada/variables/gov/cra/payroll/ei_premium.py b/policyengine_canada/variables/gov/cra/payroll/ei_premium.py new file mode 100644 index 000000000..644eeb033 --- /dev/null +++ b/policyengine_canada/variables/gov/cra/payroll/ei_premium.py @@ -0,0 +1,22 @@ +from policyengine_canada.model_api import * + + +class ei_premium(Variable): + value_type = float + entity = Person + label = "Employment Insurance premium" + definition_period = YEAR + unit = CAD + documentation = "Annual Employment Insurance premium paid by employee" + + def formula(person, period, parameters): + employment_income = person("employment_income", period) + p = parameters(period).gov.cra.payroll.ei + + # EI applies to employment income up to the maximum + insurable_earnings = min_(employment_income, p.maximum_insurable_earnings) + + # Calculate premium + premium = insurable_earnings * p.rate + + return premium \ No newline at end of file