@classytic/ca-tax 0.0.1
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.
- package/CHANGELOG.md +62 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/forms/index.d.mts +2 -0
- package/dist/forms/index.mjs +2 -0
- package/dist/forms.mjs +7786 -0
- package/dist/gst-hst/index.d.mts +2 -0
- package/dist/gst-hst/index.mjs +2 -0
- package/dist/gst-hst.mjs +675 -0
- package/dist/index.d.mts +622 -0
- package/dist/index.mjs +98 -0
- package/dist/index2.d.mts +182 -0
- package/dist/index3.d.mts +5603 -0
- package/dist/index4.d.mts +32 -0
- package/dist/t2/index.d.mts +3 -0
- package/dist/t2/index.mjs +3 -0
- package/dist/t2.mjs +6176 -0
- package/docs/research-2026-07.md +72 -0
- package/package.json +92 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as GST_RATE, C as CaSupplyCategory, D as classifySupply, E as PstBaseRule, F as getProvincialRegime, M as ProvincialRegime, N as ProvincialRegimeEntry, O as pstApplies, P as getCombinedRate, S as CA_SUPPLY_CATEGORIES, T as CaSupplyTreatment, _ as VEHICLE_ITC_CEILINGS, a as gst34Template, b as isSmallSupplier, c as CaSupplyType, d as ItcInput, f as PSB_SMALL_SUPPLIER_THRESHOLD_MINOR, g as SmallSupplierInput, h as SMALL_SUPPLIER_THRESHOLD_MINOR, i as gst34LedgerAdapter, j as PROVINCIAL_REGIMES, k as CaProvince, l as CaTaxLeg, m as SIMPLIFIED_DIGITAL_THRESHOLD_MINOR, n as Gst34Input, o as CaSalesTaxInput, p as PlaceOfSupplyInput, r as gst34Engine, s as CaSalesTaxResult, t as Gst34Breakdown, u as ItcExpenseKind, v as calculateCaSalesTax, w as CaSupplyClassification, x as placeOfSupply, y as claimableItc } from "../index2.mjs";
|
|
2
|
+
export { CA_SUPPLY_CATEGORIES, type CaProvince, type CaSalesTaxInput, type CaSalesTaxResult, type CaSupplyCategory, type CaSupplyClassification, type CaSupplyTreatment, type CaSupplyType, type CaTaxLeg, GST_RATE, type Gst34Breakdown, type Gst34Input, type ItcExpenseKind, type ItcInput, PROVINCIAL_REGIMES, PSB_SMALL_SUPPLIER_THRESHOLD_MINOR, type PlaceOfSupplyInput, type ProvincialRegime, type ProvincialRegimeEntry, type PstBaseRule, SIMPLIFIED_DIGITAL_THRESHOLD_MINOR, SMALL_SUPPLIER_THRESHOLD_MINOR, type SmallSupplierInput, VEHICLE_ITC_CEILINGS, calculateCaSalesTax, claimableItc, classifySupply, getCombinedRate, getProvincialRegime, gst34Engine, gst34LedgerAdapter, gst34Template, isSmallSupplier, placeOfSupply, pstApplies };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as getCombinedRate, a as SIMPLIFIED_DIGITAL_THRESHOLD_MINOR, c as calculateCaSalesTax, d as placeOfSupply, f as CA_SUPPLY_CATEGORIES, g as PROVINCIAL_REGIMES, h as GST_RATE, i as PSB_SMALL_SUPPLIER_THRESHOLD_MINOR, l as claimableItc, m as pstApplies, n as gst34LedgerAdapter, o as SMALL_SUPPLIER_THRESHOLD_MINOR, p as classifySupply, r as gst34Template, s as VEHICLE_ITC_CEILINGS, t as gst34Engine, u as isSmallSupplier, v as getProvincialRegime } from "../gst-hst.mjs";
|
|
2
|
+
export { CA_SUPPLY_CATEGORIES, GST_RATE, PROVINCIAL_REGIMES, PSB_SMALL_SUPPLIER_THRESHOLD_MINOR, SIMPLIFIED_DIGITAL_THRESHOLD_MINOR, SMALL_SUPPLIER_THRESHOLD_MINOR, VEHICLE_ITC_CEILINGS, calculateCaSalesTax, claimableItc, classifySupply, getCombinedRate, getProvincialRegime, gst34Engine, gst34LedgerAdapter, gst34Template, isSmallSupplier, placeOfSupply, pstApplies };
|
package/dist/gst-hst.mjs
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { createLayeredRules } from "@classytic/tax-core/registry";
|
|
2
|
+
import { firstEffectiveAt, isEffectiveAt, latestEntry } from "@classytic/tax-core/effective";
|
|
3
|
+
//#region src/extend.ts
|
|
4
|
+
/**
|
|
5
|
+
* CA rule extensions — the boot-time hot-patch seam, composed from
|
|
6
|
+
* `@classytic/tax-core/registry` (same pattern as bd-tax and us-tax).
|
|
7
|
+
* Provincial budgets move rates and PST bases mid-year (NS's April-2025
|
|
8
|
+
* cut, MB's January-2026 cloud expansion are exactly this shape) —
|
|
9
|
+
* extensions carry dated windows so a patch is period-accurate.
|
|
10
|
+
*/
|
|
11
|
+
const rateLayer = createLayeredRules();
|
|
12
|
+
const pstLayer = createLayeredRules();
|
|
13
|
+
/** Register extensions (additive; later registrations matched first). */
|
|
14
|
+
function extendCaTaxRules(extensions) {
|
|
15
|
+
if (extensions.provincialRates) rateLayer.register(extensions.provincialRates);
|
|
16
|
+
if (extensions.pstBaseRules) pstLayer.register(extensions.pstBaseRules);
|
|
17
|
+
}
|
|
18
|
+
/** Clear all registered extensions (tests / hot-reload). */
|
|
19
|
+
function resetCaTaxRuleExtensions() {
|
|
20
|
+
rateLayer.reset();
|
|
21
|
+
pstLayer.reset();
|
|
22
|
+
}
|
|
23
|
+
/** @internal */
|
|
24
|
+
function getCaRuleExtensions() {
|
|
25
|
+
return {
|
|
26
|
+
provincialRates: rateLayer.all(),
|
|
27
|
+
pstBaseRules: pstLayer.all()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/gst-hst/rates.ts
|
|
32
|
+
/** Federal GST rate (percent) — unchanged since 2008. */
|
|
33
|
+
const GST_RATE = 5;
|
|
34
|
+
const D = (iso) => new Date(iso);
|
|
35
|
+
const ERA = D("2024-01-01");
|
|
36
|
+
const PROVINCIAL_REGIMES = [
|
|
37
|
+
{
|
|
38
|
+
province: "AB",
|
|
39
|
+
regime: "GST",
|
|
40
|
+
effectiveFrom: ERA
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
province: "NT",
|
|
44
|
+
regime: "GST",
|
|
45
|
+
effectiveFrom: ERA
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
province: "NU",
|
|
49
|
+
regime: "GST",
|
|
50
|
+
effectiveFrom: ERA
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
province: "YT",
|
|
54
|
+
regime: "GST",
|
|
55
|
+
effectiveFrom: ERA
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
province: "ON",
|
|
59
|
+
regime: "HST",
|
|
60
|
+
hstRate: 13,
|
|
61
|
+
effectiveFrom: ERA
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
province: "NB",
|
|
65
|
+
regime: "HST",
|
|
66
|
+
hstRate: 15,
|
|
67
|
+
effectiveFrom: ERA
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
province: "NL",
|
|
71
|
+
regime: "HST",
|
|
72
|
+
hstRate: 15,
|
|
73
|
+
effectiveFrom: ERA
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
province: "PE",
|
|
77
|
+
regime: "HST",
|
|
78
|
+
hstRate: 15,
|
|
79
|
+
effectiveFrom: ERA
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
province: "NS",
|
|
83
|
+
regime: "HST",
|
|
84
|
+
hstRate: 15,
|
|
85
|
+
effectiveFrom: ERA,
|
|
86
|
+
effectiveTo: D("2025-03-31")
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
province: "NS",
|
|
90
|
+
regime: "HST",
|
|
91
|
+
hstRate: 14,
|
|
92
|
+
effectiveFrom: D("2025-04-01"),
|
|
93
|
+
notes: "Provincial part 10% → 9% (NOTICE342); 2026-27 budget maintains 14%"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
province: "BC",
|
|
97
|
+
regime: "GST+PST",
|
|
98
|
+
provincialRate: 7,
|
|
99
|
+
provincialName: "PST",
|
|
100
|
+
effectiveFrom: ERA
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
province: "SK",
|
|
104
|
+
regime: "GST+PST",
|
|
105
|
+
provincialRate: 6,
|
|
106
|
+
provincialName: "PST",
|
|
107
|
+
effectiveFrom: ERA
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
province: "MB",
|
|
111
|
+
regime: "GST+PST",
|
|
112
|
+
provincialRate: 7,
|
|
113
|
+
provincialName: "RST",
|
|
114
|
+
effectiveFrom: ERA,
|
|
115
|
+
notes: "Rate 7% since 2019; BASE extended to cloud/SaaS 1 Jan 2026 (see taxability)"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
province: "QC",
|
|
119
|
+
regime: "GST+QST",
|
|
120
|
+
provincialRate: 9.975,
|
|
121
|
+
provincialName: "QST",
|
|
122
|
+
effectiveFrom: ERA,
|
|
123
|
+
notes: "Charged on the GST-EXCLUSIVE price (since 2013); administered by Revenu Québec"
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
/** Regime entry for a province at a date (host extensions first). Null for unknown codes. */
|
|
127
|
+
function getProvincialRegime(province, asOf = /* @__PURE__ */ new Date()) {
|
|
128
|
+
const code = province.toUpperCase();
|
|
129
|
+
const candidates = [...getCaRuleExtensions().provincialRates, ...PROVINCIAL_REGIMES].filter((e) => e.province === code);
|
|
130
|
+
if (candidates.length === 0) return null;
|
|
131
|
+
return firstEffectiveAt(candidates, asOf) ?? latestEntry(candidates);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Combined nominal rate for display (percent). NOTE: for GST+QST/PST this
|
|
135
|
+
* is additive on the same base (both charge on the pre-GST price) — use
|
|
136
|
+
* `calculateCaSalesTax` for actual leg amounts, not this sum.
|
|
137
|
+
*/
|
|
138
|
+
function getCombinedRate(province, asOf = /* @__PURE__ */ new Date()) {
|
|
139
|
+
const r = getProvincialRegime(province, asOf);
|
|
140
|
+
if (!r) return null;
|
|
141
|
+
if (r.regime === "HST") return r.hstRate ?? null;
|
|
142
|
+
if (r.regime === "GST") return 5;
|
|
143
|
+
return 5 + (r.provincialRate ?? 0);
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/gst-hst/taxability.ts
|
|
147
|
+
/**
|
|
148
|
+
* Supply classification — the VAT-family distinction the US pack could
|
|
149
|
+
* not exercise and BD only partially does:
|
|
150
|
+
*
|
|
151
|
+
* - TAXABLE → charge tax, claim ITCs.
|
|
152
|
+
* - ZERO_RATED → charge 0%, STILL claim ITCs (groceries, prescription
|
|
153
|
+
* drugs, medical devices, exports, agriculture).
|
|
154
|
+
* - EXEMPT → charge nothing, NO ITCs on related inputs
|
|
155
|
+
* (residential rent, health, education, financial
|
|
156
|
+
* services, childcare).
|
|
157
|
+
*
|
|
158
|
+
* `inputCreditAllowed` is therefore NOT derivable from the rate — 0% can
|
|
159
|
+
* mean either. This is the semantic tax-core's `TaxClassRate` field was
|
|
160
|
+
* designed to carry.
|
|
161
|
+
*
|
|
162
|
+
* PST/RST bases are narrower than GST's and province-specific; the
|
|
163
|
+
* headline dated rule here: MANITOBA extended RST to cloud/SaaS services
|
|
164
|
+
* effective 1 Jan 2026 (MLT Aikins / BDO, corroborated).
|
|
165
|
+
*
|
|
166
|
+
* Verified 4 Jul 2026 — research sheet §6 (+ the expired Dec-2024/Feb-2025
|
|
167
|
+
* "GST holiday", deliberately NOT modelled: it lapsed 15 Feb 2025 with no
|
|
168
|
+
* successor).
|
|
169
|
+
*/
|
|
170
|
+
const CA_SUPPLY_CATEGORIES = [
|
|
171
|
+
"STANDARD",
|
|
172
|
+
"GROCERY",
|
|
173
|
+
"PRESCRIPTION_DRUGS",
|
|
174
|
+
"MEDICAL_DEVICE",
|
|
175
|
+
"EXPORT",
|
|
176
|
+
"AGRICULTURE_FISHING",
|
|
177
|
+
"FEMININE_HYGIENE",
|
|
178
|
+
"RESIDENTIAL_RENT",
|
|
179
|
+
"HEALTH_SERVICES",
|
|
180
|
+
"EDUCATION",
|
|
181
|
+
"FINANCIAL_SERVICES",
|
|
182
|
+
"CHILDCARE",
|
|
183
|
+
"SAAS",
|
|
184
|
+
"GENERAL_SERVICE"
|
|
185
|
+
];
|
|
186
|
+
const ZERO_RATED = /* @__PURE__ */ new Set([
|
|
187
|
+
"GROCERY",
|
|
188
|
+
"PRESCRIPTION_DRUGS",
|
|
189
|
+
"MEDICAL_DEVICE",
|
|
190
|
+
"EXPORT",
|
|
191
|
+
"AGRICULTURE_FISHING",
|
|
192
|
+
"FEMININE_HYGIENE"
|
|
193
|
+
]);
|
|
194
|
+
const EXEMPT = /* @__PURE__ */ new Set([
|
|
195
|
+
"RESIDENTIAL_RENT",
|
|
196
|
+
"HEALTH_SERVICES",
|
|
197
|
+
"EDUCATION",
|
|
198
|
+
"FINANCIAL_SERVICES",
|
|
199
|
+
"CHILDCARE"
|
|
200
|
+
]);
|
|
201
|
+
/** Classify a category under the GST/HST/QST (VAT-side) rules. */
|
|
202
|
+
function classifySupply(category) {
|
|
203
|
+
if (ZERO_RATED.has(category)) return {
|
|
204
|
+
treatment: "ZERO_RATED",
|
|
205
|
+
itcEligible: true
|
|
206
|
+
};
|
|
207
|
+
if (EXEMPT.has(category)) return {
|
|
208
|
+
treatment: "EXEMPT",
|
|
209
|
+
itcEligible: false
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
treatment: "TAXABLE",
|
|
213
|
+
itcEligible: true
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const PST_BASE_RULES = [
|
|
217
|
+
{
|
|
218
|
+
province: "BC",
|
|
219
|
+
category: "SAAS",
|
|
220
|
+
taxable: true
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
province: "SK",
|
|
224
|
+
category: "SAAS",
|
|
225
|
+
taxable: true
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
province: "MB",
|
|
229
|
+
category: "SAAS",
|
|
230
|
+
taxable: true,
|
|
231
|
+
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01"),
|
|
232
|
+
notes: "RST base extended to cloud computing (SaaS/PaaS/IaaS, storage, hosting) 1 Jan 2026"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
province: "BC",
|
|
236
|
+
category: "GENERAL_SERVICE",
|
|
237
|
+
taxable: false
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
province: "SK",
|
|
241
|
+
category: "GENERAL_SERVICE",
|
|
242
|
+
taxable: false
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
province: "MB",
|
|
246
|
+
category: "GENERAL_SERVICE",
|
|
247
|
+
taxable: false
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
province: "BC",
|
|
251
|
+
category: "GROCERY",
|
|
252
|
+
taxable: false
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
province: "SK",
|
|
256
|
+
category: "GROCERY",
|
|
257
|
+
taxable: false
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
province: "MB",
|
|
261
|
+
category: "GROCERY",
|
|
262
|
+
taxable: false
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
/**
|
|
266
|
+
* Does the province's PST/RST apply to this category on this date?
|
|
267
|
+
* Defaults: STANDARD (tangible goods) → true; zero-rated/exempt VAT
|
|
268
|
+
* families → false; unlisted service categories → false (PST bases are
|
|
269
|
+
* enumerative). Host extensions win.
|
|
270
|
+
*/
|
|
271
|
+
function pstApplies(province, category, asOf = /* @__PURE__ */ new Date()) {
|
|
272
|
+
const code = province.toUpperCase();
|
|
273
|
+
const rule = [...getCaRuleExtensions().pstBaseRules, ...PST_BASE_RULES].find((r) => r.province === code && r.category === category && (r.effectiveFrom === void 0 || isEffectiveAt({
|
|
274
|
+
effectiveFrom: r.effectiveFrom,
|
|
275
|
+
effectiveTo: r.effectiveTo
|
|
276
|
+
}, asOf)));
|
|
277
|
+
if (rule) return rule.taxable;
|
|
278
|
+
if (category === "STANDARD") return true;
|
|
279
|
+
if (ZERO_RATED.has(category) || EXEMPT.has(category)) return false;
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/gst-hst/calculate.ts
|
|
284
|
+
/**
|
|
285
|
+
* Place-of-supply (ETA Sch. IX; CRA Memoranda 3-3-3/-5/-6, simplified):
|
|
286
|
+
* goods → delivery province; services/intangibles → recipient address,
|
|
287
|
+
* falling back to where performed.
|
|
288
|
+
*/
|
|
289
|
+
function placeOfSupply(input) {
|
|
290
|
+
if (input.supplyType === "goods") {
|
|
291
|
+
const p = input.deliveryProvince ?? input.recipientProvince;
|
|
292
|
+
if (!p) throw new TypeError("goods supply requires deliveryProvince (destination governs)");
|
|
293
|
+
return p.toUpperCase();
|
|
294
|
+
}
|
|
295
|
+
const p = input.recipientProvince ?? input.performedProvince;
|
|
296
|
+
if (!p) throw new TypeError("service/intangible supply requires recipientProvince (or performedProvince fallback)");
|
|
297
|
+
return p.toUpperCase();
|
|
298
|
+
}
|
|
299
|
+
function calculateCaSalesTax(input) {
|
|
300
|
+
const asOf = input.asOf ?? /* @__PURE__ */ new Date();
|
|
301
|
+
const province = placeOfSupply(input);
|
|
302
|
+
const classification = classifySupply(input.category);
|
|
303
|
+
const regime = getProvincialRegime(province, asOf);
|
|
304
|
+
if (!regime) throw new TypeError(`Unknown province/territory code: ${province}`);
|
|
305
|
+
const legs = [];
|
|
306
|
+
if (classification.treatment === "TAXABLE") if (regime.regime === "HST") legs.push({
|
|
307
|
+
tax: "HST",
|
|
308
|
+
rate: regime.hstRate ?? 0,
|
|
309
|
+
amountMinor: Math.round(input.amountMinor * (regime.hstRate ?? 0) / 100),
|
|
310
|
+
authority: "CRA",
|
|
311
|
+
recoverable: true
|
|
312
|
+
});
|
|
313
|
+
else {
|
|
314
|
+
legs.push({
|
|
315
|
+
tax: "GST",
|
|
316
|
+
rate: 5,
|
|
317
|
+
amountMinor: Math.round(input.amountMinor * 5 / 100),
|
|
318
|
+
authority: "CRA",
|
|
319
|
+
recoverable: true
|
|
320
|
+
});
|
|
321
|
+
if (regime.regime === "GST+QST") legs.push({
|
|
322
|
+
tax: "QST",
|
|
323
|
+
rate: regime.provincialRate ?? 0,
|
|
324
|
+
amountMinor: Math.round(input.amountMinor * (regime.provincialRate ?? 0) / 100),
|
|
325
|
+
authority: "RQ",
|
|
326
|
+
recoverable: true
|
|
327
|
+
});
|
|
328
|
+
else if (regime.regime === "GST+PST" && pstApplies(province, input.category, asOf)) legs.push({
|
|
329
|
+
tax: regime.provincialName ?? "PST",
|
|
330
|
+
rate: regime.provincialRate ?? 0,
|
|
331
|
+
amountMinor: Math.round(input.amountMinor * (regime.provincialRate ?? 0) / 100),
|
|
332
|
+
authority: province,
|
|
333
|
+
recoverable: false
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
province,
|
|
338
|
+
classification,
|
|
339
|
+
legs,
|
|
340
|
+
totalTaxMinor: legs.reduce((s, l) => s + l.amountMinor, 0)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** Small-supplier threshold: $30,000 over four consecutive calendar quarters (CRA Memo 2-2). */
|
|
344
|
+
const SMALL_SUPPLIER_THRESHOLD_MINOR = 3e6;
|
|
345
|
+
/** Public service bodies: $50,000. */
|
|
346
|
+
const PSB_SMALL_SUPPLIER_THRESHOLD_MINOR = 5e6;
|
|
347
|
+
/** Simplified digital regime (non-resident vendors/platforms): $30,000 over 12 months, no ITCs. */
|
|
348
|
+
const SIMPLIFIED_DIGITAL_THRESHOLD_MINOR = 3e6;
|
|
349
|
+
/** Is the person still a small supplier (no obligation to register/collect)? */
|
|
350
|
+
function isSmallSupplier(input) {
|
|
351
|
+
const threshold = input.publicServiceBody ? PSB_SMALL_SUPPLIER_THRESHOLD_MINOR : SMALL_SUPPLIER_THRESHOLD_MINOR;
|
|
352
|
+
if ((input.currentQuarterMinor ?? 0) > threshold) return false;
|
|
353
|
+
return input.trailingFourQuartersMinor <= threshold;
|
|
354
|
+
}
|
|
355
|
+
/** Class 10.1 passenger-vehicle ITC cost ceilings (pre-tax, cents) — dated. */
|
|
356
|
+
const VEHICLE_ITC_CEILINGS = [
|
|
357
|
+
{
|
|
358
|
+
ceilingMinor: 37e5,
|
|
359
|
+
effectiveFrom: /* @__PURE__ */ new Date("2024-01-01"),
|
|
360
|
+
effectiveTo: /* @__PURE__ */ new Date("2024-12-31")
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
ceilingMinor: 38e5,
|
|
364
|
+
effectiveFrom: /* @__PURE__ */ new Date("2025-01-01"),
|
|
365
|
+
effectiveTo: /* @__PURE__ */ new Date("2025-12-31")
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
ceilingMinor: 39e5,
|
|
369
|
+
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01")
|
|
370
|
+
}
|
|
371
|
+
];
|
|
372
|
+
/**
|
|
373
|
+
* Claimable ITC for an input (cents). Rules: inputs relating to EXEMPT
|
|
374
|
+
* outputs → 0; meals & entertainment → 50%; club dues → 0; passenger
|
|
375
|
+
* vehicles → prorated to the dated cost ceiling.
|
|
376
|
+
*/
|
|
377
|
+
function claimableItc(input) {
|
|
378
|
+
if (input.relatedSupplyCategory) {
|
|
379
|
+
const { itcEligible } = classifySupply(input.relatedSupplyCategory);
|
|
380
|
+
if (!itcEligible) return 0;
|
|
381
|
+
}
|
|
382
|
+
switch (input.kind) {
|
|
383
|
+
case "club_dues": return 0;
|
|
384
|
+
case "meals_entertainment": return Math.round(input.taxPaidMinor * .5);
|
|
385
|
+
case "passenger_vehicle": {
|
|
386
|
+
const asOf = input.asOf ?? /* @__PURE__ */ new Date();
|
|
387
|
+
const ceiling = VEHICLE_ITC_CEILINGS.find((c) => asOf.getTime() >= c.effectiveFrom.getTime() && (c.effectiveTo === void 0 || asOf.getTime() <= c.effectiveTo.getTime())) ?? VEHICLE_ITC_CEILINGS[VEHICLE_ITC_CEILINGS.length - 1];
|
|
388
|
+
const cost = input.vehicleCostMinor ?? 0;
|
|
389
|
+
if (cost <= 0 || cost <= ceiling.ceilingMinor) return input.taxPaidMinor;
|
|
390
|
+
return Math.round(input.taxPaidMinor * ceiling.ceilingMinor / cost);
|
|
391
|
+
}
|
|
392
|
+
default: return input.taxPaidMinor;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/gst-hst/gst34.ts
|
|
397
|
+
const LINES = Object.freeze({
|
|
398
|
+
101: {
|
|
399
|
+
line: 101,
|
|
400
|
+
name: "Total Sales and Other Revenue",
|
|
401
|
+
description: "Total sales before GST/HST",
|
|
402
|
+
type: "input",
|
|
403
|
+
section: "Sales"
|
|
404
|
+
},
|
|
405
|
+
103: {
|
|
406
|
+
line: 103,
|
|
407
|
+
name: "GST/HST Collected or Collectible",
|
|
408
|
+
description: "Tax collected on taxable supplies",
|
|
409
|
+
type: "input",
|
|
410
|
+
section: "Collected"
|
|
411
|
+
},
|
|
412
|
+
104: {
|
|
413
|
+
line: 104,
|
|
414
|
+
name: "Adjustments (increase)",
|
|
415
|
+
description: "Adjustments increasing net tax",
|
|
416
|
+
type: "manual",
|
|
417
|
+
section: "Collected"
|
|
418
|
+
},
|
|
419
|
+
105: {
|
|
420
|
+
line: 105,
|
|
421
|
+
name: "Total GST/HST and Adjustments",
|
|
422
|
+
description: "103 + 104",
|
|
423
|
+
type: "calculated",
|
|
424
|
+
section: "Collected",
|
|
425
|
+
calculate: (d) => (d[103] || 0) + (d[104] || 0)
|
|
426
|
+
},
|
|
427
|
+
106: {
|
|
428
|
+
line: 106,
|
|
429
|
+
name: "Input Tax Credits (ITCs)",
|
|
430
|
+
description: "ITCs for the period",
|
|
431
|
+
type: "input",
|
|
432
|
+
section: "ITCs"
|
|
433
|
+
},
|
|
434
|
+
107: {
|
|
435
|
+
line: 107,
|
|
436
|
+
name: "Adjustments (decrease)",
|
|
437
|
+
description: "Adjustments decreasing net tax",
|
|
438
|
+
type: "manual",
|
|
439
|
+
section: "ITCs"
|
|
440
|
+
},
|
|
441
|
+
108: {
|
|
442
|
+
line: 108,
|
|
443
|
+
name: "Total ITCs and Adjustments",
|
|
444
|
+
description: "106 + 107",
|
|
445
|
+
type: "calculated",
|
|
446
|
+
section: "ITCs",
|
|
447
|
+
calculate: (d) => (d[106] || 0) + (d[107] || 0)
|
|
448
|
+
},
|
|
449
|
+
109: {
|
|
450
|
+
line: 109,
|
|
451
|
+
name: "Net Tax",
|
|
452
|
+
description: "105 − 108",
|
|
453
|
+
type: "calculated",
|
|
454
|
+
section: "Net Tax",
|
|
455
|
+
calculate: (d) => (d[105] || 0) - (d[108] || 0)
|
|
456
|
+
},
|
|
457
|
+
110: {
|
|
458
|
+
line: 110,
|
|
459
|
+
name: "Instalments Paid",
|
|
460
|
+
description: "Instalments and net-tax payments already made",
|
|
461
|
+
type: "manual",
|
|
462
|
+
section: "Credits"
|
|
463
|
+
},
|
|
464
|
+
111: {
|
|
465
|
+
line: 111,
|
|
466
|
+
name: "Rebates",
|
|
467
|
+
description: "Rebates claimed on this return",
|
|
468
|
+
type: "manual",
|
|
469
|
+
section: "Credits"
|
|
470
|
+
},
|
|
471
|
+
112: {
|
|
472
|
+
line: 112,
|
|
473
|
+
name: "Total Other Credits",
|
|
474
|
+
description: "110 + 111",
|
|
475
|
+
type: "calculated",
|
|
476
|
+
section: "Credits",
|
|
477
|
+
calculate: (d) => (d[110] || 0) + (d[111] || 0)
|
|
478
|
+
},
|
|
479
|
+
113: {
|
|
480
|
+
line: 113,
|
|
481
|
+
name: "Balance",
|
|
482
|
+
description: "109 − 112",
|
|
483
|
+
type: "calculated",
|
|
484
|
+
section: "Balance",
|
|
485
|
+
calculate: (d) => (d[109] || 0) - (d[112] || 0)
|
|
486
|
+
},
|
|
487
|
+
205: {
|
|
488
|
+
line: 205,
|
|
489
|
+
name: "GST/HST Due on Real Property",
|
|
490
|
+
description: "Self-assessed on taxable real property",
|
|
491
|
+
type: "manual",
|
|
492
|
+
section: "Self-Assessed"
|
|
493
|
+
},
|
|
494
|
+
405: {
|
|
495
|
+
line: 405,
|
|
496
|
+
name: "Other GST/HST Self-Assessed",
|
|
497
|
+
description: "Imported services, other self-assessments",
|
|
498
|
+
type: "manual",
|
|
499
|
+
section: "Self-Assessed"
|
|
500
|
+
},
|
|
501
|
+
"113C": {
|
|
502
|
+
line: "113C",
|
|
503
|
+
name: "Total Due or Refund",
|
|
504
|
+
description: "113 + 205 + 405",
|
|
505
|
+
type: "calculated",
|
|
506
|
+
section: "Result",
|
|
507
|
+
calculate: (d) => (d[113] || 0) + (d[205] || 0) + (d[405] || 0)
|
|
508
|
+
},
|
|
509
|
+
114: {
|
|
510
|
+
line: 114,
|
|
511
|
+
name: "Refund Claimed",
|
|
512
|
+
description: "When 113C is negative",
|
|
513
|
+
type: "calculated",
|
|
514
|
+
section: "Result",
|
|
515
|
+
calculate: (d) => {
|
|
516
|
+
const t = d["113C"] || 0;
|
|
517
|
+
return t < 0 ? Math.abs(t) : 0;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
115: {
|
|
521
|
+
line: 115,
|
|
522
|
+
name: "Amount Owing",
|
|
523
|
+
description: "When 113C is positive",
|
|
524
|
+
type: "calculated",
|
|
525
|
+
section: "Result",
|
|
526
|
+
calculate: (d) => {
|
|
527
|
+
const t = d["113C"] || 0;
|
|
528
|
+
return t > 0 ? t : 0;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
const CALC_ORDER = [
|
|
533
|
+
105,
|
|
534
|
+
108,
|
|
535
|
+
109,
|
|
536
|
+
112,
|
|
537
|
+
113,
|
|
538
|
+
"113C",
|
|
539
|
+
114,
|
|
540
|
+
115
|
|
541
|
+
];
|
|
542
|
+
/** The GST34 grid as a tax-core `TaxReportTemplate`. */
|
|
543
|
+
const gst34Template = {
|
|
544
|
+
name: "CRA GST/HST Return (GST34)",
|
|
545
|
+
lines: LINES,
|
|
546
|
+
calculate(inputData, manualData = {}) {
|
|
547
|
+
const result = {
|
|
548
|
+
...inputData,
|
|
549
|
+
...manualData
|
|
550
|
+
};
|
|
551
|
+
for (const line of CALC_ORDER) {
|
|
552
|
+
const def = LINES[line];
|
|
553
|
+
if (def?.calculate) result[line] = def.calculate(result);
|
|
554
|
+
}
|
|
555
|
+
return result;
|
|
556
|
+
},
|
|
557
|
+
summarize(calculated) {
|
|
558
|
+
return {
|
|
559
|
+
sales: calculated[101] || 0,
|
|
560
|
+
taxCollected: calculated[105] || 0,
|
|
561
|
+
itcs: calculated[108] || 0,
|
|
562
|
+
netTax: calculated[109] || 0,
|
|
563
|
+
balance: calculated["113C"] || 0,
|
|
564
|
+
refund: calculated[114] || 0,
|
|
565
|
+
owing: calculated[115] || 0,
|
|
566
|
+
isRefund: (calculated["113C"] || 0) < 0
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
function nonNegInt(value, field) {
|
|
571
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new TypeError(`${field} must be a non-negative integer (cents), got ${String(value)}`);
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
/** The GST34 as a tax-core TaxEngine. Negative `totalOwing` = refund position (line 114). */
|
|
575
|
+
const gst34Engine = {
|
|
576
|
+
validate(input) {
|
|
577
|
+
const raw = input;
|
|
578
|
+
if (!raw || typeof raw !== "object") throw new TypeError("GST34 input must be an object");
|
|
579
|
+
const period = raw.period;
|
|
580
|
+
if (!period || !(period.start instanceof Date) || !(period.end instanceof Date) || typeof period.label !== "string" || period.start.getTime() > period.end.getTime()) throw new TypeError("period must be { start: Date, end: Date, label } with start <= end");
|
|
581
|
+
return {
|
|
582
|
+
period,
|
|
583
|
+
salesMinor: nonNegInt(raw.salesMinor, "salesMinor"),
|
|
584
|
+
taxCollectedMinor: nonNegInt(raw.taxCollectedMinor, "taxCollectedMinor"),
|
|
585
|
+
adjustmentsIncreaseMinor: nonNegInt(raw.adjustmentsIncreaseMinor ?? 0, "adjustmentsIncreaseMinor"),
|
|
586
|
+
itcsMinor: nonNegInt(raw.itcsMinor, "itcsMinor"),
|
|
587
|
+
adjustmentsDecreaseMinor: nonNegInt(raw.adjustmentsDecreaseMinor ?? 0, "adjustmentsDecreaseMinor"),
|
|
588
|
+
instalmentsMinor: nonNegInt(raw.instalmentsMinor ?? 0, "instalmentsMinor"),
|
|
589
|
+
rebatesMinor: nonNegInt(raw.rebatesMinor ?? 0, "rebatesMinor"),
|
|
590
|
+
selfAssessedRealPropertyMinor: nonNegInt(raw.selfAssessedRealPropertyMinor ?? 0, "selfAssessedRealPropertyMinor"),
|
|
591
|
+
selfAssessedOtherMinor: nonNegInt(raw.selfAssessedOtherMinor ?? 0, "selfAssessedOtherMinor")
|
|
592
|
+
};
|
|
593
|
+
},
|
|
594
|
+
compute(input) {
|
|
595
|
+
const lines = gst34Template.calculate({
|
|
596
|
+
101: input.salesMinor,
|
|
597
|
+
103: input.taxCollectedMinor,
|
|
598
|
+
106: input.itcsMinor
|
|
599
|
+
}, {
|
|
600
|
+
104: input.adjustmentsIncreaseMinor ?? 0,
|
|
601
|
+
107: input.adjustmentsDecreaseMinor ?? 0,
|
|
602
|
+
110: input.instalmentsMinor ?? 0,
|
|
603
|
+
111: input.rebatesMinor ?? 0,
|
|
604
|
+
205: input.selfAssessedRealPropertyMinor ?? 0,
|
|
605
|
+
405: input.selfAssessedOtherMinor ?? 0
|
|
606
|
+
});
|
|
607
|
+
return {
|
|
608
|
+
jurisdiction: "CA",
|
|
609
|
+
category: "gst-hst",
|
|
610
|
+
period: input.period,
|
|
611
|
+
totalOwing: lines["113C"] ?? 0,
|
|
612
|
+
currency: "CAD",
|
|
613
|
+
breakdown: {
|
|
614
|
+
lines,
|
|
615
|
+
netTaxMinor: lines[109] ?? 0,
|
|
616
|
+
refundMinor: lines[114] ?? 0,
|
|
617
|
+
owingMinor: lines[115] ?? 0
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
render(obligation) {
|
|
622
|
+
return JSON.stringify({
|
|
623
|
+
form: "GST34",
|
|
624
|
+
...obligation
|
|
625
|
+
}, null, 2);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Settlement adapter. Owing: clear the collected-liability, recognize the
|
|
630
|
+
* ITC receivable consumption, pay cash. Refund: net tax is negative — a
|
|
631
|
+
* receivable from CRA. Roles: `gst-hst-payable`, `gst-hst-itc-receivable`,
|
|
632
|
+
* `cash`, `cra-receivable`.
|
|
633
|
+
*/
|
|
634
|
+
const gst34LedgerAdapter = { toJournalEntries(obligation, options) {
|
|
635
|
+
const b = obligation.breakdown;
|
|
636
|
+
const date = options.postingDate ?? obligation.period.end;
|
|
637
|
+
const collected = b.lines[105] ?? 0;
|
|
638
|
+
const itcs = b.lines[108] ?? 0;
|
|
639
|
+
const items = [];
|
|
640
|
+
if (collected > 0) items.push({
|
|
641
|
+
account: options.resolveAccount("gst-hst-payable"),
|
|
642
|
+
debit: collected,
|
|
643
|
+
credit: 0
|
|
644
|
+
});
|
|
645
|
+
if (itcs > 0) items.push({
|
|
646
|
+
account: options.resolveAccount("gst-hst-itc-receivable"),
|
|
647
|
+
debit: 0,
|
|
648
|
+
credit: itcs
|
|
649
|
+
});
|
|
650
|
+
if (obligation.totalOwing > 0) items.push({
|
|
651
|
+
account: options.resolveAccount("cash"),
|
|
652
|
+
debit: 0,
|
|
653
|
+
credit: obligation.totalOwing
|
|
654
|
+
});
|
|
655
|
+
else if (obligation.totalOwing < 0) items.push({
|
|
656
|
+
account: options.resolveAccount("cra-receivable"),
|
|
657
|
+
debit: -obligation.totalOwing,
|
|
658
|
+
credit: 0
|
|
659
|
+
});
|
|
660
|
+
const diff = items.reduce((s, i) => s + i.debit, 0) - items.reduce((s, i) => s + i.credit, 0);
|
|
661
|
+
if (diff !== 0) items.push({
|
|
662
|
+
account: options.resolveAccount(diff > 0 ? "cra-receivable" : "gst-hst-payable"),
|
|
663
|
+
debit: diff < 0 ? -diff : 0,
|
|
664
|
+
credit: diff > 0 ? diff : 0
|
|
665
|
+
});
|
|
666
|
+
return [{
|
|
667
|
+
journalType: "TAX",
|
|
668
|
+
date,
|
|
669
|
+
label: `GST/HST return (GST34) — ${obligation.period.label}`,
|
|
670
|
+
...options.partnerId !== void 0 ? { partner: options.partnerId } : {},
|
|
671
|
+
journalItems: items
|
|
672
|
+
}];
|
|
673
|
+
} };
|
|
674
|
+
//#endregion
|
|
675
|
+
export { getCombinedRate as _, SIMPLIFIED_DIGITAL_THRESHOLD_MINOR as a, resetCaTaxRuleExtensions as b, calculateCaSalesTax as c, placeOfSupply as d, CA_SUPPLY_CATEGORIES as f, PROVINCIAL_REGIMES as g, GST_RATE as h, PSB_SMALL_SUPPLIER_THRESHOLD_MINOR as i, claimableItc as l, pstApplies as m, gst34LedgerAdapter as n, SMALL_SUPPLIER_THRESHOLD_MINOR as o, classifySupply as p, gst34Template as r, VEHICLE_ITC_CEILINGS as s, gst34Engine as t, isSmallSupplier as u, getProvincialRegime as v, extendCaTaxRules as y };
|