From 51003f371e5b0197e9a03ea2c15b0b47f4267e69 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 15:43:34 +0000 Subject: [PATCH 1/2] Add CPP/QPP enhanced contribution deduction (CRA line 22215) Models the deduction for the enhanced portion of CPP/QPP first-tier contributions plus the entire CPP2/QPP2 contribution, applied against taxable income for federal and provincial brackets per CRA line 22215. Configs gain a `baseRate` (4.95% CPP, 5.40% QPP); the calculator computes the enhanced share proportionally to the contribution and reduces the income used for bracket calculations. The tax-visualizer surfaces the deduction inline with the federal or provincial card depending on which plan applies. https://claude.ai/code/session_01DTfhD3S8sQTsuY5ggEQs26 --- src/app/[lang]/(main)/tax-visualizer/page.tsx | 220 ++++++++++++++++-- src/lib/tax/calculator.deduction.test.ts | 78 +++++++ src/lib/tax/calculator.ts | 59 +++-- src/lib/tax/configs/2023/federal.ts | 1 + src/lib/tax/configs/2023/quebec.ts | 1 + src/lib/tax/configs/2024/federal.ts | 1 + src/lib/tax/configs/2024/quebec.ts | 1 + src/lib/tax/configs/2025/federal.ts | 1 + src/lib/tax/configs/2025/quebec.ts | 1 + src/lib/tax/configs/2026/federal.ts | 1 + src/lib/tax/configs/2026/quebec.ts | 1 + src/lib/tax/types.ts | 9 +- 12 files changed, 333 insertions(+), 41 deletions(-) create mode 100644 src/lib/tax/calculator.deduction.test.ts diff --git a/src/app/[lang]/(main)/tax-visualizer/page.tsx b/src/app/[lang]/(main)/tax-visualizer/page.tsx index a60d5b5a..ed4f56b8 100644 --- a/src/app/[lang]/(main)/tax-visualizer/page.tsx +++ b/src/app/[lang]/(main)/tax-visualizer/page.tsx @@ -193,6 +193,8 @@ function IncomeTaxBracketsSection({ title, config, income, + deduction = 0, + deductionLabel, }: { title: string; config: { @@ -200,8 +202,11 @@ function IncomeTaxBracketsSection({ basicPersonalAmount: number; }; income: number; + deduction?: number; + deductionLabel?: string; }) { - const breakdown = getBracketTaxBreakdown(income, config.brackets); + const taxableIncome = Math.max(0, income - deduction); + const breakdown = getBracketTaxBreakdown(taxableIncome, config.brackets); const lowestRate = config.brackets[0]?.rate ?? 0; const bpaCredit = config.basicPersonalAmount * lowestRate; const totalBeforeCredit = breakdown.reduce((sum, b) => sum + b.taxAmount, 0); @@ -210,6 +215,15 @@ function IncomeTaxBracketsSection({ return (

{title}

+ {deduction > 0 && ( +

+ + Brackets applied to taxable income {formatAmount(taxableIncome)} ( + {formatAmount(income)} less {formatAmount(deduction)} + {deductionLabel ? ` ${deductionLabel}` : ""}) + +

+ )} @@ -282,16 +296,6 @@ function FederalTaxCard({ provincialConfig, income, }: FederalTaxCardProps) { - // Calculate income tax - const breakdown = getBracketTaxBreakdown(income, config.incomeTax.brackets); - const lowestRate = config.incomeTax.brackets[0]?.rate ?? 0; - const bpaCredit = config.incomeTax.basicPersonalAmount * lowestRate; - const incomeTaxBeforeCredit = breakdown.reduce( - (sum, b) => sum + b.taxAmount, - 0, - ); - const incomeTaxAmount = Math.max(0, incomeTaxBeforeCredit - bpaCredit); - // Resolve EI override (Quebec residents pay a reduced rate). The pension // plan, when overridden, is administered provincially (Retraite Québec) // and is rendered in the Provincial card instead. @@ -306,6 +310,36 @@ function FederalTaxCard({ const eiAmount = calculateCappedContribution(income, eiConfig); const payrollTotal = cppAmount + cpp2Amount + eiAmount; + // Line 22215 deduction is applied at the provincial level when the + // province administers its own pension plan (e.g., Quebec QPP), and at + // the federal level otherwise. This card reduces taxable income for the + // federal income tax bracket calculation only when CPP applies. + const cppEnhancedRate = + config.cpp.baseRate !== undefined + ? Math.max(0, config.cpp.rate - config.cpp.baseRate) + : 0; + const cppEnhancedPortion = + !hasProvincialPension && config.cpp.rate > 0 + ? cppAmount * (cppEnhancedRate / config.cpp.rate) + : 0; + const cppQppEnhancedDeduction = hasProvincialPension + ? 0 + : cppEnhancedPortion + cpp2Amount; + + // Calculate income tax on taxable income (after the line 22215 deduction). + const taxableIncome = Math.max(0, income - cppQppEnhancedDeduction); + const breakdown = getBracketTaxBreakdown( + taxableIncome, + config.incomeTax.brackets, + ); + const lowestRate = config.incomeTax.brackets[0]?.rate ?? 0; + const bpaCredit = config.incomeTax.basicPersonalAmount * lowestRate; + const incomeTaxBeforeCredit = breakdown.reduce( + (sum, b) => sum + b.taxAmount, + 0, + ); + const incomeTaxAmount = Math.max(0, incomeTaxBeforeCredit - bpaCredit); + // Calculate federal abatement (Quebec Abatement) const federalAbatementConfig = provincialConfig.federalAbatement; const federalAbatementAmount = federalAbatementConfig @@ -328,8 +362,62 @@ function FederalTaxCard({ title={config.incomeTax.name} config={config.incomeTax} income={income} + deduction={cppQppEnhancedDeduction} + deductionLabel="CPP/CPP2 enhanced deduction (line 22215)" /> + {/* CPP/CPP2 Enhanced Deduction (Line 22215) */} + {cppQppEnhancedDeduction > 0 && ( +
+

+ + Deduction for CPP enhanced contributions (Line 22215) + +

+
+ + {cppEnhancedPortion > 0 && ( + + + + + )} + {cpp2Amount > 0 && ( + + + + + )} + + + + + +
+
+ {config.cpp.shortName} enhanced portion +
+
+ {formatPercent(cppEnhancedRate)} of pensionable earnings +
+
+ -{formatAmount(cppEnhancedPortion)} +
+
+ + {config.cpp2.shortName} (fully deductible) + +
+
+ -{formatAmount(cpp2Amount)} +
+ Total deduction from taxable income + + -{formatAmount(cppQppEnhancedDeduction)} +
+
+ )} + {/* Federal Abatement (if applicable) */} {federalAbatementConfig && federalAbatementAmount > 0 && (
@@ -443,19 +531,6 @@ function ProvincialTaxCard({ config, income, }: ProvincialTaxCardProps) { - // Calculate provincial income tax for surtax calculation - const breakdown = getBracketTaxBreakdown(income, config.incomeTax.brackets); - const lowestRate = config.incomeTax.brackets[0]?.rate ?? 0; - const bpaCredit = config.incomeTax.basicPersonalAmount * lowestRate; - const provincialTaxBeforeCredit = breakdown.reduce( - (sum, b) => sum + b.taxAmount, - 0, - ); - const provincialTax = Math.max(0, provincialTaxBeforeCredit - bpaCredit); - - const surtaxAmount = config.surtax - ? calculateSurtax(provincialTax, config.surtax) - : 0; const healthPremiumAmount = config.healthPremium ? calculateHealthPremium(income, config.healthPremium) : 0; @@ -470,6 +545,44 @@ function ProvincialTaxCard({ ? calculateCpp2Contribution(income, config.pensionPlanAdditionalOverride) : 0; + // QPP enhanced deduction (line 22215) — applies when the province has its + // own pension plan (e.g., Quebec QPP). For provinces that use the federal + // CPP, the deduction is shown on the federal card. + const qppEnhancedRate = + config.pensionPlanOverride?.baseRate !== undefined + ? Math.max( + 0, + config.pensionPlanOverride.rate - config.pensionPlanOverride.baseRate, + ) + : 0; + const qppEnhancedPortion = + config.pensionPlanOverride && config.pensionPlanOverride.rate > 0 + ? provincialPensionAmount * + (qppEnhancedRate / config.pensionPlanOverride.rate) + : 0; + const qppEnhancedDeduction = config.pensionPlanOverride + ? qppEnhancedPortion + provincialPensionAdditionalAmount + : 0; + + // Calculate provincial income tax on taxable income (after line 22215 + // deduction when applicable). + const taxableIncome = Math.max(0, income - qppEnhancedDeduction); + const breakdown = getBracketTaxBreakdown( + taxableIncome, + config.incomeTax.brackets, + ); + const lowestRate = config.incomeTax.brackets[0]?.rate ?? 0; + const bpaCredit = config.incomeTax.basicPersonalAmount * lowestRate; + const provincialTaxBeforeCredit = breakdown.reduce( + (sum, b) => sum + b.taxAmount, + 0, + ); + const provincialTax = Math.max(0, provincialTaxBeforeCredit - bpaCredit); + + const surtaxAmount = config.surtax + ? calculateSurtax(provincialTax, config.surtax) + : 0; + // Overall total const totalProvincialTax = provincialTax + @@ -491,8 +604,67 @@ function ProvincialTaxCard({ title={config.incomeTax.name} config={config.incomeTax} income={income} + deduction={qppEnhancedDeduction} + deductionLabel="QPP/QPP2 enhanced deduction (line 22215)" /> + {/* QPP/QPP2 Enhanced Deduction (Line 22215) */} + {qppEnhancedDeduction > 0 && config.pensionPlanOverride && ( +
+

+ + Deduction for QPP enhanced contributions (Line 22215) + +

+ + + {qppEnhancedPortion > 0 && ( + + + + + )} + {provincialPensionAdditionalAmount > 0 && + config.pensionPlanAdditionalOverride && ( + + + + + )} + + + + + +
+
+ + {config.pensionPlanOverride.shortName} enhanced + portion + +
+
+ {formatPercent(qppEnhancedRate)} of pensionable earnings +
+
+ -{formatAmount(qppEnhancedPortion)} +
+
+ + {config.pensionPlanAdditionalOverride.shortName}{" "} + (fully deductible) + +
+
+ -{formatAmount(provincialPensionAdditionalAmount)} +
+ Total deduction from taxable income + + -{formatAmount(qppEnhancedDeduction)} +
+
+ )} + {/* Surtax (if applicable) */} {config.surtax && (
diff --git a/src/lib/tax/calculator.deduction.test.ts b/src/lib/tax/calculator.deduction.test.ts new file mode 100644 index 00000000..e907e07c --- /dev/null +++ b/src/lib/tax/calculator.deduction.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; + +import { calculateDetailedTax } from "./calculator"; + +// CRA line 22215: deduction for the "enhanced" portion of CPP/QPP +// contributions on employment income. +// - For CPP: the rate above the pre-2019 base of 4.95% (currently 1.0%) +// on first-tier pensionable earnings is deductible, plus 100% of CPP2. +// - For QPP: the rate above the pre-2019 base of 5.40% on first-tier +// pensionable earnings is deductible, plus 100% of QPP2. + +describe("CPP/QPP enhanced contribution deduction (line 22215)", () => { + it("computes CPP enhanced deduction for Ontario 2024 above YMPE", () => { + // 2024 CPP: rate 5.95%, base 4.95%, exemption $3,500, YMPE $68,500. + // CPP1 = ($68,500 − $3,500) × 5.95% = $3,867.50 + // Enhanced portion = $3,867.50 × (1.0% / 5.95%) = $650.00 + // CPP2 = ($73,200 − $68,500) × 4% = $188.00 (fully deductible) + // Total deduction = $650.00 + $188.00 = $838.00 + const detailed = calculateDetailedTax(80000, "ontario", "2024"); + expect(detailed.cppQppEnhancedDeduction).toBeCloseTo(838, 2); + }); + + it("computes QPP enhanced deduction for Quebec 2024 above YMPE", () => { + // 2024 QPP: rate 6.40%, base 5.40%, exemption $3,500, YMPE $68,500. + // QPP1 = ($68,500 − $3,500) × 6.40% = $4,160.00 + // Enhanced portion = $4,160.00 × (1.0% / 6.40%) = $650.00 + // QPP2 = ($73,200 − $68,500) × 4% = $188.00 (fully deductible) + // Total deduction = $650.00 + $188.00 = $838.00 + const detailed = calculateDetailedTax(80000, "quebec", "2024"); + expect(detailed.cppQppEnhancedDeduction).toBeCloseTo(838, 2); + }); + + it("scales the deduction proportionally for low income", () => { + // 2024 Ontario at $30,000: + // CPP1 = ($30,000 − $3,500) × 5.95% = $1,576.75 + // Enhanced portion = $1,576.75 × (1.0% / 5.95%) ≈ $264.99 + // CPP2 = 0 (income below YMPE) + const detailed = calculateDetailedTax(30000, "ontario", "2024"); + expect(detailed.cppQppEnhancedDeduction).toBeCloseTo(264.99, 1); + }); + + it("reduces federal income tax by deducting enhanced contributions", () => { + // Without the deduction, federal income tax at $80k Ontario 2024 would be + // higher. With the $838 deduction, tax is lowered by ~ $838 × 20.5% + // (the marginal bracket the deduction comes off of). + const detailedWithLine22215 = calculateDetailedTax( + 80000, + "ontario", + "2024", + ); + + // Verify the deduction at least reduces taxable income (federal tax is + // computed on income − deduction). The exact federal tax is checked via + // the bracket calculator in other tests; here we just confirm the + // deduction takes effect. + expect(detailedWithLine22215.cppQppEnhancedDeduction).toBeGreaterThan(0); + expect(detailedWithLine22215.federalIncomeTax).toBeGreaterThan(0); + }); + + it("reports the deduction as a negative line item at the federal level for non-Quebec", () => { + const detailed = calculateDetailedTax(80000, "ontario", "2024"); + const line = detailed.lineItems.find( + (l) => l.id === "cpp-qpp-enhanced-deduction", + ); + expect(line).toBeDefined(); + expect(line!.amount).toBeLessThan(0); + expect(line!.level).toBe("federal"); + }); + + it("reports the deduction at the provincial level for Quebec", () => { + const detailed = calculateDetailedTax(80000, "quebec", "2024"); + const line = detailed.lineItems.find( + (l) => l.id === "cpp-qpp-enhanced-deduction", + ); + expect(line).toBeDefined(); + expect(line!.level).toBe("provincial"); + }); +}); diff --git a/src/lib/tax/calculator.ts b/src/lib/tax/calculator.ts index b626aa73..edbd4dd4 100644 --- a/src/lib/tax/calculator.ts +++ b/src/lib/tax/calculator.ts @@ -58,20 +58,6 @@ function calculateWithConfig( ? "provincial" : "federal"; - // Federal income tax - const federalIncomeTax = calculateBracketTax( - income, - config.federal.incomeTax, - ); - lineItems.push({ - id: "federal-income-tax", - name: "Federal Income Tax", - level: "federal", - amount: federalIncomeTax, - effectiveRate: income > 0 ? (federalIncomeTax / income) * 100 : 0, - category: "incomeTax", - }); - // EI contribution (Quebec residents pay a reduced rate; see eiConfig) const eiContribution = calculateCappedContribution(income, eiConfig); lineItems.push({ @@ -130,9 +116,49 @@ function calculateWithConfig( } } - // Provincial income tax + // CRA line 22215: deduction for the "enhanced" portion of CPP/QPP + // contributions on employment income. The first-additional enhancement + // (rate above pre-2019 baseRate) and the entire second-additional + // contribution (CPP2/QPP2) are deductible from taxable income. + const cppEnhancedRate = + pensionConfig.baseRate !== undefined + ? Math.max(0, pensionConfig.rate - pensionConfig.baseRate) + : 0; + const cppEnhancedPortion = + pensionConfig.rate > 0 + ? cppContribution * (cppEnhancedRate / pensionConfig.rate) + : 0; + const cppQppEnhancedDeduction = cppEnhancedPortion + cpp2Contribution; + const taxableIncome = Math.max(0, income - cppQppEnhancedDeduction); + if (cppQppEnhancedDeduction > 0) { + lineItems.push({ + id: "cpp-qpp-enhanced-deduction", + name: "Deduction for CPP/QPP Enhanced Contributions", + level: pensionLevel, + amount: -cppQppEnhancedDeduction, + effectiveRate: income > 0 ? (-cppQppEnhancedDeduction / income) * 100 : 0, + category: "cppQppEnhancedDeduction", + }); + } + + // Federal income tax (computed on taxable income after the line 22215 + // deduction). + const federalIncomeTax = calculateBracketTax( + taxableIncome, + config.federal.incomeTax, + ); + lineItems.push({ + id: "federal-income-tax", + name: "Federal Income Tax", + level: "federal", + amount: federalIncomeTax, + effectiveRate: income > 0 ? (federalIncomeTax / income) * 100 : 0, + category: "incomeTax", + }); + + // Provincial income tax (also on taxable income after the deduction). const provincialIncomeTax = calculateBracketTax( - income, + taxableIncome, config.provincial.incomeTax, ); const provinceName = @@ -241,6 +267,7 @@ function calculateWithConfig( surtax, healthPremium, federalAbatement, + cppQppEnhancedDeduction, // Metadata year: config.year, diff --git a/src/lib/tax/configs/2023/federal.ts b/src/lib/tax/configs/2023/federal.ts index 5e2354c1..0d70ebbd 100644 --- a/src/lib/tax/configs/2023/federal.ts +++ b/src/lib/tax/configs/2023/federal.ts @@ -27,6 +27,7 @@ export const FEDERAL_TAX_CONFIG: FederalTaxConfig = { name: "Canada Pension Plan", shortName: "CPP", rate: 0.0595, + baseRate: 0.0495, exemption: 3500, maxEarnings: 66600, maxContribution: 3754.45, diff --git a/src/lib/tax/configs/2023/quebec.ts b/src/lib/tax/configs/2023/quebec.ts index 6ad1f1aa..c5c9e52c 100644 --- a/src/lib/tax/configs/2023/quebec.ts +++ b/src/lib/tax/configs/2023/quebec.ts @@ -37,6 +37,7 @@ export const QUEBEC_TAX_CONFIG: ProvincialTaxConfig = { name: "Québec Pension Plan", shortName: "QPP", rate: 0.064, + baseRate: 0.054, exemption: 3500, maxEarnings: 66600, maxContribution: 4038.4, diff --git a/src/lib/tax/configs/2024/federal.ts b/src/lib/tax/configs/2024/federal.ts index 8a449043..6c934808 100644 --- a/src/lib/tax/configs/2024/federal.ts +++ b/src/lib/tax/configs/2024/federal.ts @@ -27,6 +27,7 @@ export const FEDERAL_TAX_CONFIG: FederalTaxConfig = { name: "Canada Pension Plan", shortName: "CPP", rate: 0.0595, + baseRate: 0.0495, exemption: 3500, maxEarnings: 68500, maxContribution: 3867.5, diff --git a/src/lib/tax/configs/2024/quebec.ts b/src/lib/tax/configs/2024/quebec.ts index 1d82e783..07066aec 100644 --- a/src/lib/tax/configs/2024/quebec.ts +++ b/src/lib/tax/configs/2024/quebec.ts @@ -39,6 +39,7 @@ export const QUEBEC_TAX_CONFIG: ProvincialTaxConfig = { name: "Québec Pension Plan", shortName: "QPP", rate: 0.064, + baseRate: 0.054, exemption: 3500, maxEarnings: 68500, maxContribution: 4160, diff --git a/src/lib/tax/configs/2025/federal.ts b/src/lib/tax/configs/2025/federal.ts index f7832d88..fc861737 100644 --- a/src/lib/tax/configs/2025/federal.ts +++ b/src/lib/tax/configs/2025/federal.ts @@ -27,6 +27,7 @@ export const FEDERAL_TAX_CONFIG: FederalTaxConfig = { name: "Canada Pension Plan", shortName: "CPP", rate: 0.0595, + baseRate: 0.0495, exemption: 3500, maxEarnings: 71300, maxContribution: 4034.1, diff --git a/src/lib/tax/configs/2025/quebec.ts b/src/lib/tax/configs/2025/quebec.ts index 21e79101..2a8ee09f 100644 --- a/src/lib/tax/configs/2025/quebec.ts +++ b/src/lib/tax/configs/2025/quebec.ts @@ -38,6 +38,7 @@ export const QUEBEC_TAX_CONFIG: ProvincialTaxConfig = { name: "Québec Pension Plan", shortName: "QPP", rate: 0.064, + baseRate: 0.054, exemption: 3500, maxEarnings: 71300, maxContribution: 4339.2, diff --git a/src/lib/tax/configs/2026/federal.ts b/src/lib/tax/configs/2026/federal.ts index a2b33568..44b636f2 100644 --- a/src/lib/tax/configs/2026/federal.ts +++ b/src/lib/tax/configs/2026/federal.ts @@ -27,6 +27,7 @@ export const FEDERAL_TAX_CONFIG: FederalTaxConfig = { name: "Canada Pension Plan", shortName: "CPP", rate: 0.0595, + baseRate: 0.0495, exemption: 3500, maxEarnings: 74600, maxContribution: 4230.45, diff --git a/src/lib/tax/configs/2026/quebec.ts b/src/lib/tax/configs/2026/quebec.ts index 97348d75..dfe32b79 100644 --- a/src/lib/tax/configs/2026/quebec.ts +++ b/src/lib/tax/configs/2026/quebec.ts @@ -45,6 +45,7 @@ export const QUEBEC_TAX_CONFIG: ProvincialTaxConfig = { name: "Québec Pension Plan", shortName: "QPP", rate: 0.063, + baseRate: 0.054, exemption: 3500, maxEarnings: 74600, maxContribution: 4479.3, diff --git a/src/lib/tax/types.ts b/src/lib/tax/types.ts index 2f81684d..c281ee23 100644 --- a/src/lib/tax/types.ts +++ b/src/lib/tax/types.ts @@ -22,6 +22,11 @@ export interface CappedContributionConfig { exemption: number; maxEarnings: number; maxContribution: number; + // For CPP/QPP: the historical "base" rate (pre-2019 enhancement). The + // portion of the contribution attributable to (rate - baseRate) is the + // "enhanced" CPP/QPP contribution and is deductible from taxable income + // on CRA line 22215. If unset, no enhanced deduction is computed. + baseRate?: number; } // Surtax config (Ontario surtax - tiers applied to base tax) @@ -135,7 +140,8 @@ export interface TaxLineItem { | "healthPremium" | "incomeTaxProvincial" | "federalAbatement" - | "parentalInsurance"; + | "parentalInsurance" + | "cppQppEnhancedDeduction"; } // Detailed calculation result @@ -161,6 +167,7 @@ export interface DetailedTaxCalculation { surtax: number; healthPremium: number; federalAbatement: number; + cppQppEnhancedDeduction: number; // Metadata year: string; From 9434e4e50b4b4fa7bdb3cba46294df7ef9b284ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 20:31:14 +0000 Subject: [PATCH 2/2] Move line 22215 deduction into Total Taxable Income summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure each tax card so the line 22215 CPP/QPP enhanced deduction is visualized as a reduction to taxable income — not as a credit against total tax owing. Each card now leads with a "Total Taxable Income" line, with the gross income and per-contribution deduction details collapsed behind an expandable
summary. The deduction is computed once in TaxDetails and shared with both the Federal and Provincial cards so their bracket calculations apply to the same taxable income. https://claude.ai/code/session_01DTfhD3S8sQTsuY5ggEQs26 --- src/app/[lang]/(main)/tax-visualizer/page.tsx | 347 +++++++++--------- 1 file changed, 168 insertions(+), 179 deletions(-) diff --git a/src/app/[lang]/(main)/tax-visualizer/page.tsx b/src/app/[lang]/(main)/tax-visualizer/page.tsx index ed4f56b8..e2c092ae 100644 --- a/src/app/[lang]/(main)/tax-visualizer/page.tsx +++ b/src/app/[lang]/(main)/tax-visualizer/page.tsx @@ -193,8 +193,6 @@ function IncomeTaxBracketsSection({ title, config, income, - deduction = 0, - deductionLabel, }: { title: string; config: { @@ -202,11 +200,8 @@ function IncomeTaxBracketsSection({ basicPersonalAmount: number; }; income: number; - deduction?: number; - deductionLabel?: string; }) { - const taxableIncome = Math.max(0, income - deduction); - const breakdown = getBracketTaxBreakdown(taxableIncome, config.brackets); + const breakdown = getBracketTaxBreakdown(income, config.brackets); const lowestRate = config.brackets[0]?.rate ?? 0; const bpaCredit = config.basicPersonalAmount * lowestRate; const totalBeforeCredit = breakdown.reduce((sum, b) => sum + b.taxAmount, 0); @@ -215,15 +210,6 @@ function IncomeTaxBracketsSection({ return (

{title}

- {deduction > 0 && ( -

- - Brackets applied to taxable income {formatAmount(taxableIncome)} ( - {formatAmount(income)} less {formatAmount(deduction)} - {deductionLabel ? ` ${deductionLabel}` : ""}) - -

- )} @@ -284,21 +270,154 @@ function IncomeTaxBracketsSection({ ); } +interface DeductionItem { + label: string; + sublabel?: string; + amount: number; +} + +interface TaxableIncomeBreakdown { + taxableIncome: number; + deductionTotal: number; + deductionItems: DeductionItem[]; +} + +// Compute the line 22215 CPP/QPP enhanced-contribution deduction for the +// active province. The deduction reduces taxable income for both federal +// and provincial brackets, so it's computed once and shared by both cards. +function computeTaxableIncomeBreakdown( + income: number, + config: TaxYearProvinceConfig, +): TaxableIncomeBreakdown { + const pensionConfig = + config.provincial.pensionPlanOverride ?? config.federal.cpp; + const pensionAdditionalConfig = + config.provincial.pensionPlanAdditionalOverride ?? config.federal.cpp2; + const pensionAmount = calculateCappedContribution(income, pensionConfig); + const pensionAdditionalAmount = calculateCpp2Contribution( + income, + pensionAdditionalConfig, + ); + const enhancedRate = + pensionConfig.baseRate !== undefined + ? Math.max(0, pensionConfig.rate - pensionConfig.baseRate) + : 0; + const enhancedPortion = + pensionConfig.rate > 0 + ? pensionAmount * (enhancedRate / pensionConfig.rate) + : 0; + const deductionTotal = enhancedPortion + pensionAdditionalAmount; + const taxableIncome = Math.max(0, income - deductionTotal); + + const deductionItems: DeductionItem[] = []; + if (enhancedPortion > 0) { + deductionItems.push({ + label: `${pensionConfig.shortName} enhanced portion (line 22215)`, + sublabel: `${formatPercent(enhancedRate)} of pensionable earnings`, + amount: enhancedPortion, + }); + } + if (pensionAdditionalAmount > 0) { + deductionItems.push({ + label: `${pensionAdditionalConfig.shortName} (line 22215, fully deductible)`, + amount: pensionAdditionalAmount, + }); + } + + return { taxableIncome, deductionTotal, deductionItems }; +} + +// Taxable income section: shows gross income reduced by the line 22215 +// CPP/QPP enhanced contribution deduction. The deduction detail is hidden +// behind an expandable `
` so it doesn't read as a reduction in +// total tax owing. +function TaxableIncomeSection({ + income, + taxableIncome, + deductionItems, + deductionTotal, +}: { + income: number; + taxableIncome: number; + deductionItems: DeductionItem[]; + deductionTotal: number; +}) { + const hasDeduction = deductionTotal > 0; + + return ( +
+
+

+ Total Taxable Income +

+ + {formatAmount(taxableIncome)} + +
+ {hasDeduction && ( +
+ + + {formatAmount(income)} gross income less{" "} + {formatAmount(deductionTotal)} CPP/QPP enhanced deduction (line + 22215) + + +
+ + + + + + {deductionItems.map((item, index) => ( + + + + + ))} + + + + + +
+ Gross employment income + + {formatAmount(income)} +
+
{item.label}
+ {item.sublabel && ( +
+ {item.sublabel} +
+ )} +
+ -{formatAmount(item.amount)} +
+ Taxable income + + {formatAmount(taxableIncome)} +
+
+ )} +
+ ); +} + // Federal Tax Card - all federal taxes in one card interface FederalTaxCardProps { config: TaxYearProvinceConfig["federal"]; provincialConfig: TaxYearProvinceConfig["provincial"]; income: number; + taxableBreakdown: TaxableIncomeBreakdown; } function FederalTaxCard({ config, provincialConfig, income, + taxableBreakdown, }: FederalTaxCardProps) { - // Resolve EI override (Quebec residents pay a reduced rate). The pension - // plan, when overridden, is administered provincially (Retraite Québec) - // and is rendered in the Provincial card instead. const eiConfig = provincialConfig.eiOverride ?? config.ei; const hasProvincialPension = !!provincialConfig.pensionPlanOverride; const cppAmount = hasProvincialPension @@ -310,24 +429,9 @@ function FederalTaxCard({ const eiAmount = calculateCappedContribution(income, eiConfig); const payrollTotal = cppAmount + cpp2Amount + eiAmount; - // Line 22215 deduction is applied at the provincial level when the - // province administers its own pension plan (e.g., Quebec QPP), and at - // the federal level otherwise. This card reduces taxable income for the - // federal income tax bracket calculation only when CPP applies. - const cppEnhancedRate = - config.cpp.baseRate !== undefined - ? Math.max(0, config.cpp.rate - config.cpp.baseRate) - : 0; - const cppEnhancedPortion = - !hasProvincialPension && config.cpp.rate > 0 - ? cppAmount * (cppEnhancedRate / config.cpp.rate) - : 0; - const cppQppEnhancedDeduction = hasProvincialPension - ? 0 - : cppEnhancedPortion + cpp2Amount; - - // Calculate income tax on taxable income (after the line 22215 deduction). - const taxableIncome = Math.max(0, income - cppQppEnhancedDeduction); + // Federal income tax is computed on taxable income (after the line 22215 + // CPP/QPP enhanced deduction). + const { taxableIncome } = taxableBreakdown; const breakdown = getBracketTaxBreakdown( taxableIncome, config.incomeTax.brackets, @@ -340,13 +444,11 @@ function FederalTaxCard({ ); const incomeTaxAmount = Math.max(0, incomeTaxBeforeCredit - bpaCredit); - // Calculate federal abatement (Quebec Abatement) const federalAbatementConfig = provincialConfig.federalAbatement; const federalAbatementAmount = federalAbatementConfig ? incomeTaxAmount * federalAbatementConfig.rate : 0; - // Overall total (minus abatement) const totalFederalTax = incomeTaxAmount + payrollTotal - federalAbatementAmount; @@ -357,67 +459,21 @@ function FederalTaxCard({ Federal Taxes - {/* Income Tax Brackets */} + {/* Taxable Income (with line 22215 deduction details collapsed) */} + + + {/* Income Tax Brackets (applied to taxable income) */} - {/* CPP/CPP2 Enhanced Deduction (Line 22215) */} - {cppQppEnhancedDeduction > 0 && ( -
-

- - Deduction for CPP enhanced contributions (Line 22215) - -

- - - {cppEnhancedPortion > 0 && ( - - - - - )} - {cpp2Amount > 0 && ( - - - - - )} - - - - - -
-
- {config.cpp.shortName} enhanced portion -
-
- {formatPercent(cppEnhancedRate)} of pensionable earnings -
-
- -{formatAmount(cppEnhancedPortion)} -
-
- - {config.cpp2.shortName} (fully deductible) - -
-
- -{formatAmount(cpp2Amount)} -
- Total deduction from taxable income - - -{formatAmount(cppQppEnhancedDeduction)} -
-
- )} - {/* Federal Abatement (if applicable) */} {federalAbatementConfig && federalAbatementAmount > 0 && (
@@ -524,12 +580,14 @@ interface ProvincialTaxCardProps { provinceName: string; config: TaxYearProvinceConfig["provincial"]; income: number; + taxableBreakdown: TaxableIncomeBreakdown; } function ProvincialTaxCard({ provinceName, config, income, + taxableBreakdown, }: ProvincialTaxCardProps) { const healthPremiumAmount = config.healthPremium ? calculateHealthPremium(income, config.healthPremium) @@ -537,7 +595,6 @@ function ProvincialTaxCard({ const parentalInsuranceAmount = config.parentalInsurance ? calculateCappedContribution(income, config.parentalInsurance) : 0; - // Province-administered pension plan (e.g., Quebec QPP / QPP2) const provincialPensionAmount = config.pensionPlanOverride ? calculateCappedContribution(income, config.pensionPlanOverride) : 0; @@ -545,28 +602,9 @@ function ProvincialTaxCard({ ? calculateCpp2Contribution(income, config.pensionPlanAdditionalOverride) : 0; - // QPP enhanced deduction (line 22215) — applies when the province has its - // own pension plan (e.g., Quebec QPP). For provinces that use the federal - // CPP, the deduction is shown on the federal card. - const qppEnhancedRate = - config.pensionPlanOverride?.baseRate !== undefined - ? Math.max( - 0, - config.pensionPlanOverride.rate - config.pensionPlanOverride.baseRate, - ) - : 0; - const qppEnhancedPortion = - config.pensionPlanOverride && config.pensionPlanOverride.rate > 0 - ? provincialPensionAmount * - (qppEnhancedRate / config.pensionPlanOverride.rate) - : 0; - const qppEnhancedDeduction = config.pensionPlanOverride - ? qppEnhancedPortion + provincialPensionAdditionalAmount - : 0; - - // Calculate provincial income tax on taxable income (after line 22215 - // deduction when applicable). - const taxableIncome = Math.max(0, income - qppEnhancedDeduction); + // Provincial income tax is computed on taxable income (after the line + // 22215 CPP/QPP enhanced deduction). + const { taxableIncome } = taxableBreakdown; const breakdown = getBracketTaxBreakdown( taxableIncome, config.incomeTax.brackets, @@ -583,7 +621,6 @@ function ProvincialTaxCard({ ? calculateSurtax(provincialTax, config.surtax) : 0; - // Overall total const totalProvincialTax = provincialTax + surtaxAmount + @@ -599,72 +636,21 @@ function ProvincialTaxCard({ {provinceName} Taxes - {/* Income Tax Brackets */} + {/* Taxable Income (with line 22215 deduction details collapsed) */} + + + {/* Income Tax Brackets (applied to taxable income) */} - {/* QPP/QPP2 Enhanced Deduction (Line 22215) */} - {qppEnhancedDeduction > 0 && config.pensionPlanOverride && ( -
-

- - Deduction for QPP enhanced contributions (Line 22215) - -

- - - {qppEnhancedPortion > 0 && ( - - - - - )} - {provincialPensionAdditionalAmount > 0 && - config.pensionPlanAdditionalOverride && ( - - - - - )} - - - - - -
-
- - {config.pensionPlanOverride.shortName} enhanced - portion - -
-
- {formatPercent(qppEnhancedRate)} of pensionable earnings -
-
- -{formatAmount(qppEnhancedPortion)} -
-
- - {config.pensionPlanAdditionalOverride.shortName}{" "} - (fully deductible) - -
-
- -{formatAmount(provincialPensionAdditionalAmount)} -
- Total deduction from taxable income - - -{formatAmount(qppEnhancedDeduction)} -
-
- )} - {/* Surtax (if applicable) */} {config.surtax && (
@@ -854,6 +840,7 @@ function TaxDetails({ setYear, }: TaxDetailsProps) { const provinceName = PROVINCE_NAMES[config.province] || config.province; + const taxableBreakdown = computeTaxableIncomeBreakdown(income, config); return (
@@ -882,11 +869,13 @@ function TaxDetails({ config={config.federal} provincialConfig={config.provincial} income={income} + taxableBreakdown={taxableBreakdown} />