@classytic/ca-tax
v0.0.36
Published
Canadian corporate tax engine for @classytic/tax-core — federal T2, Alberta AT1 and Québec CO-17 with form definitions traced to the published forms, plus GST/HST/PST/QST with dated provincial regimes.
Readme
@classytic/ca-tax
Canadian corporate tax engine for @classytic/tax-core — federal T2,
Alberta AT1, and Québec CO-17, with the sales-tax pack (GST/HST/PST/QST)
it grew out of.
What distinguishes it: every line number is traced to the published form. The form definitions are extracted from the CRA PDFs, the Alberta TRA specification, and the Revenu Québec forms — never typed from memory — and a test refuses any line number that does not appear in one of them. A wrong line number is worse than a missing one: it files a real figure against the wrong box and nothing looks broken.
Verified rules + provenance: docs/research-2026-07.md.
Corporate tax
| Area | Coverage |
|---|---|
| Federal T2 | Schedule 1 book-to-tax reconciliation, CCA (S8), capital gains (S6), losses (S4), SBD with both grinds (S7/S23/S33), Part IV/IV.1/VI.1 (S3/S43), GRIP & LRIP (S53/S54), Part III.1 on excessive designations (S55), SR&ED ITC (S31), foreign credits (S21), EIFEL (S130) |
| Alberta AT1 | Alberta tax, Reg 402 allocation (S2), the reconciliation to federal income (S12), Alberta CCA (S13), reserves (S17), dispositions (S18), donations (S20), loss continuity (S21/S10), the Innovation Employment Grant (S29), and the Net File payload |
| Québec CO-17 | Québec tax over the allocation factor, and the form definition |
| Form definitions | 30 forms as typed data — T2 jacket + 16 schedules, AT1 jacket + 8, CO-17 — each carrying its own provenance (document, revision, retrieval date). Exposed on the ./forms subpath, 29 kB rather than pulling the 249 kB engine |
| Three numbering schemes | CRA three-digit lines, Alberta SSSFFFOOO line-item ids, and Québec boxes that carry letters (420c) — modelled distinctly, because assuming three digits truncates the third |
| Fail-closed | An unclassified line throws rather than defaulting; a return missing a mandatory field refuses rather than filing a zero; an unanswered yes/no stays unanswered rather than becoming "No" |
Sales tax
| Area | Coverage |
|---|---|
| Rates | All 13 provinces/territories, effective-dated — Nova Scotia's 15%→14% (1 Apr 2025) ships as two windows, so early-2025 reprints stay period-accurate |
| Place of supply | Goods → delivery province; services/intangibles → recipient address with performed-province fallback (ETA Sch. IX simplified) |
| Multi-authority legs | calculateCaSalesTax returns per-authority legs: QC = GST(CRA) + QST(RQ) both on the GST-exclusive base; BC/SK/MB = GST + non-recoverable PST/RST — including Manitoba's RST-on-SaaS expansion from 1 Jan 2026 as a dated rule |
| Zero-rated vs exempt | The VAT truth table: zero-rated (groceries, prescription, exports…) = 0% with ITCs; exempt (rent, health, financial…) = no tax, no ITCs |
| ITC restrictions | Meals 50%, club dues denied, passenger-vehicle ceiling dated ($38k 2025 / $39k 2026), exempt-output denial |
| Registration | Small supplier $30k (four-quarter + single-quarter tests), PSB $50k, simplified digital regime threshold |
| GST34 | The CRA return as a tax-core TaxEngine (negative totalOwing = refund/line 114) over a tax-core TaxReportTemplate line grid — completing the /ledger seam validation — plus gst34LedgerAdapter posting balanced settlements via wireTaxToLedger |
| Resolver | createCaTaxResolver({ province }) — per-province instances; accountFor('input') returns the ITC-receivable role (the recoverable side the US pack proves absent) |
| Hot-patch | extendCaTaxRules — dated provincial-rate and PST-base overrides |
Quick start
import { calculateCaSalesTax, createCaTaxResolver } from '@classytic/ca-tax';
// A Québec sale: two legs, two authorities, GST-exclusive QST base
const r = calculateCaSalesTax({
supplyType: 'goods', deliveryProvince: 'QC',
category: 'STANDARD', amountMinor: 1_000_00,
});
// r.legs → [{ tax:'GST', authority:'CRA', … }, { tax:'QST', authority:'RQ', … }]
// Period close — post the GST34 through your ledger
import { wireTaxToLedger } from '@classytic/tax-core';
import { gst34Engine, gst34LedgerAdapter } from '@classytic/ca-tax';
const postReturn = wireTaxToLedger(gst34Engine, gst34LedgerAdapter, engine.repositories.journalEntries);Deliberately out of scope
Transmission. The engine renders the filing payload; sending it is a host concern, and in two of three jurisdictions the wire format is not public. Alberta publishes its WSDL and response codes, so AT1 transmission is implementable. CRA's certified CIF schema and Revenu Québec's ImpôtNet element names are released only through their software-certification programmes — so both renderers produce a clearly-labelled draft rather than pretending otherwise.
Québec's small business deduction is computed on form CO-771, which applies a paid-hours test with no federal equivalent. Not inferable from the T2.
Also out: Quick Method (NS post-cut cells await RC4058 verification), T1 and payroll, the Bill C-4 first-time-homebuyer GST rebate, provincial PST returns.
Where the numbers come from
Every shipped tax figure carries an @statutory-data block naming its source and
the date it was last checked, enforced by tests/statutory-data-provenance.test.ts
— a rate table cannot be added without provenance, a cited document that stops
existing fails the build, and a book's declared years are checked against the book.
The shipped rate books stop at 2024, deliberately. They are reference values
behind a seam: a host supplies authoritative figures per year through
CORP_TAX_RATE_BOOK and its siblings. If you are filing 2025 or later, wire your
own book — t2/rates/province-rates.ts in particular is unsourced for all 22 of
its figures.
See docs/STATUTORY-DATA.md for the convention, the current provenance table, and the yearly revision workflow.
ESM only · Node ≥ 20 · integer cents · effective-dated with provenance · MIT
Trademark
MIT-licensed code. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see TRADEMARK.md.
