@classytic/ca-tax 0.0.1 → 0.0.5

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/dist/t2.mjs CHANGED
@@ -262,7 +262,8 @@ const AT1_SCHEDULES_WITH_BUILDERS = Object.freeze([
262
262
  "018",
263
263
  "020",
264
264
  "021",
265
- "029"
265
+ "029",
266
+ "4970"
266
267
  ]);
267
268
  /**
268
269
  * Schedules the engine computes but cannot yet file.
@@ -431,21 +432,182 @@ function schedule18Values(result) {
431
432
  values
432
433
  };
433
434
  }
434
- function schedule29Values(result) {
435
+ /**
436
+ * Field ids read off the live, TRA-certified form
437
+ * (`research/sources/tra-forms/pdf/AT1SCH29-*.pdf`, Rev. 2026-06):
438
+ *
439
+ * Page 1 — Eligible Expenditures, filed only when the caller supplies
440
+ * `eligible` (from `computeIegEligibleExpenditures` in
441
+ * `schedule29-eligible-expenditures.ts`):
442
+ * 003 federal amount of qualified/current SR&ED expenditures (T661 line
443
+ * 559 or 557 — see `iegT661SourceLine`)
444
+ * 005/007/009/011/025 the five adjustments; 031 = 005 − 007 + 009 + 011 + 025
445
+ * 040 primary field of science or technology (1–4), when the caller
446
+ * supplies `primaryFieldCode`
447
+ *
448
+ * Page 1/2 — the grant itself:
449
+ * 110 8% of the capped expenditures
450
+ * 112 12% of the increment above base — the non-associated path. Filed
451
+ * INSTEAD of 125, never alongside it.
452
+ * 125 12% of the associated path's allocated allowed amount — filed
453
+ * instead of 112 when `result.enhancedRateBasis === 'associated'`.
454
+ * 126 taxable capital the line-128 factor was computed from
455
+ * 128 the taxable-capital reduction factor, 0 to 1
456
+ * 130 (110 + 112-or-125) × 128 — the credit before recapture
457
+ * 132 recapture, only where the caller supplied one
458
+ * 134 NET IEG — the figure AT1 page 2, line 129 carries forward
459
+ *
460
+ * Page 3 — the Agreement Among Associated Corporations, filed only when the
461
+ * group agreed one (`agreement`, from `computeIegAgreement` in
462
+ * `schedule29-ieg-group.ts`); omitted entirely for a non-associated claim:
463
+ * 200/202/204/206/208 the longest-year member's CAN, that member's own
464
+ * tax year begin/end, days in that year, and the resulting $4M-pool
465
+ * maximum for the group
466
+ * Per member, occurrence incrementing in the order given (claimant first):
467
+ * 220 Federal Business Number, 230 Alberta CAN, 235 current taxation
468
+ * year end, 240 allocated expenditure limit, 245 current-year
469
+ * expenditures, 250 prior year 1, 260 prior year 2, 265 taxable capital
470
+ * (prior year), 267 individual maximum allowed amount, 268 allocated
471
+ * allowed amount. The form has NO line for the member's NAME — 220 is
472
+ * the Federal Business Number, confirmed by re-rendering page 3 of the
473
+ * live form (a prior version of this function filed `name` at 220,
474
+ * which put free text in a box the form defines as a 9-10 digit account
475
+ * number; `name` is not transmitted at all, matching the form).
476
+ * 270/275/280/290/300 Σ 240/245/250/260/265 across every member
477
+ * 310 the group Allowed Amount — 275 − (280 + 290) / 2
478
+ * 320 Σ 268
479
+ * 325 the claiming corporation's own 268 — this is what feeds line 125
480
+ * above (`associatedAllowedAmount` in `computeIeg`'s input)
481
+ *
482
+ * 128 is a RATIO, not a dollar figure — formatted to 6 decimal places, the
483
+ * same precision `schedule2.ts`'s Reg 402 allocation factor uses. That
484
+ * precision is this codebase's own convention, not something confirmed
485
+ * against TRA's certification validator for THIS field; verify before relying
486
+ * on it for a corporation whose taxable capital sits inside the $10M–$50M
487
+ * band. See `schedule29-ieg.ts` for why 128 exists and the one unresolved
488
+ * discrepancy in how 130 is meant to be computed.
489
+ */
490
+ function schedule29Values(result, agreement, eligible, primaryFieldCode) {
435
491
  const values = [];
436
- const put = (fieldId, value) => values.push({
437
- lineItemId: at1LineItemId("029", fieldId),
492
+ const put = (fieldId, value, occurrence = 1) => values.push({
493
+ lineItemId: at1LineItemId("029", fieldId, occurrence),
438
494
  value
439
495
  });
496
+ if (eligible) {
497
+ put("003", eligible.federalAmount);
498
+ put("005", eligible.albertaPortion);
499
+ if (eligible.federalProxyAmount > 0) put("007", eligible.federalProxyAmount);
500
+ if (eligible.albertaProxyAmount > 0) put("009", eligible.albertaProxyAmount);
501
+ if (eligible.iegReducingFederalExpenditure > 0) put("011", eligible.iegReducingFederalExpenditure);
502
+ if (eligible.repaymentOrContractPayment > 0) put("025", eligible.repaymentOrContractPayment);
503
+ put("031", eligible.totalEligibleExpenditures);
504
+ }
505
+ if (primaryFieldCode !== void 0) put("040", primaryFieldCode);
440
506
  put("110", result.creditAtBaseRate);
441
- put("112", result.creditAtEnhancedRate);
442
- put("128", result.incrementalExpenditures);
507
+ put(result.enhancedRateBasis === "associated" ? "125" : "112", result.creditAtEnhancedRate);
508
+ put("126", result.taxableCapital);
509
+ put("128", result.reductionFactor.toFixed(6));
510
+ put("130", result.grossIeg);
511
+ if (result.recapture > 0) put("132", result.recapture);
512
+ put("134", result.ieg);
513
+ if (agreement) {
514
+ put("200", agreement.longestYearCan);
515
+ put("202", agreement.longestYearBegin);
516
+ put("204", agreement.longestYearEnd);
517
+ put("206", agreement.daysInLongestYear);
518
+ put("208", agreement.maximumExpenditureLimit);
519
+ agreement.members.forEach((m, i) => {
520
+ const n = i + 1;
521
+ if (m.federalBusinessNumber !== void 0) put("220", m.federalBusinessNumber, n);
522
+ if (m.albertaCan !== void 0) put("230", m.albertaCan, n);
523
+ if (m.currentTaxationYearEnd !== void 0) put("235", m.currentTaxationYearEnd, n);
524
+ put("240", m.allocatedExpenditureLimit, n);
525
+ put("245", m.currentYearExpenditures, n);
526
+ put("250", m.priorYear1, n);
527
+ put("260", m.priorYear2, n);
528
+ put("265", m.taxableCapitalPriorYear, n);
529
+ put("267", m.individualMaximumAllowedAmount, n);
530
+ put("268", m.allocatedAllowedAmount, n);
531
+ });
532
+ put("270", agreement.totalAllocatedLimit);
533
+ put("275", agreement.totalCurrentYearExpenditures);
534
+ put("280", agreement.totalPriorYear1);
535
+ put("290", agreement.totalPriorYear2);
536
+ put("300", agreement.totalTaxableCapitalPriorYear);
537
+ put("310", agreement.groupAllowedAmount);
538
+ put("320", agreement.totalAllocatedAllowedAmount);
539
+ put("325", agreement.claimantAllocatedAllowedAmount);
540
+ }
443
541
  return {
444
542
  scheduleId: "029",
445
543
  values
446
544
  };
447
545
  }
448
546
  /**
547
+ * A separate attachment from Schedule 29 itself, one occurrence per Alberta
548
+ * SR&ED project (101/103/105/107/109/111/113), a TOTAL row (repeats the same
549
+ * field ids at a fixed trailing occurrence — see below), and the jurisdiction
550
+ * table (135–170, occurrence 1, no per-jurisdiction row structure since each
551
+ * jurisdiction has its own field id already).
552
+ *
553
+ * ⚠ The schedule id (`'4970'`) and line-item id scheme used here are a
554
+ * PLACEHOLDER — TRA's own published Guide confirms the field NUMBERS
555
+ * (verified twice, independently) but not the NetFile wire-format encoding
556
+ * for this attachment. See `at4970-ieg-projects.ts` and `forms/at4970.ts`
557
+ * for the same caveat. Verify before relying on this for live filing.
558
+ *
559
+ * The TOTAL row is filed at occurrence `projects.length + 1` — one past the
560
+ * last project row — so a real project can never collide with it.
561
+ */
562
+ function schedule4970Values(result) {
563
+ const values = [];
564
+ const put = (fieldId, value, occurrence = 1) => values.push({
565
+ lineItemId: `4970${fieldId}${String(occurrence).padStart(3, "0")}`,
566
+ value
567
+ });
568
+ result.projects.forEach((p, i) => {
569
+ const n = i + 1;
570
+ put("101", p.title, n);
571
+ if (p.projectCode !== void 0) put("103", p.projectCode, n);
572
+ put("105", p.albertaPortion, n);
573
+ put("107", p.otherPortion, n);
574
+ put("109", p.salariesAndWages, n);
575
+ if (p.federalProxyAmount > 0) put("111", p.federalProxyAmount, n);
576
+ if (p.albertaProxyAmount > 0) put("113", p.albertaProxyAmount, n);
577
+ });
578
+ const totalRow = result.projects.length + 1;
579
+ put("105", result.totals.albertaPortion, totalRow);
580
+ put("107", result.totals.otherPortion, totalRow);
581
+ put("109", result.totals.salariesAndWages, totalRow);
582
+ put("111", result.totals.federalProxyAmount, totalRow);
583
+ put("113", result.totals.albertaProxyAmount, totalRow);
584
+ const jurisdictionFields = {
585
+ alberta: "135",
586
+ britishColumbia: "137",
587
+ manitoba: "139",
588
+ newBrunswick: "141",
589
+ newfoundlandAndLabrador: "143",
590
+ northwestTerritories: "145",
591
+ novaScotia: "147",
592
+ nunavut: "149",
593
+ ontario: "151",
594
+ princeEdwardIsland: "153",
595
+ quebec: "155",
596
+ saskatchewan: "157",
597
+ yukon: "159",
598
+ other: "161"
599
+ };
600
+ for (const j of result.jurisdictions) {
601
+ const fieldId = jurisdictionFields[j.jurisdiction];
602
+ if (fieldId && j.amountIncurred > 0) put(fieldId, j.amountIncurred);
603
+ }
604
+ if (result.jurisdictionTotal > 0) put("170", result.jurisdictionTotal);
605
+ return {
606
+ scheduleId: "4970",
607
+ values
608
+ };
609
+ }
610
+ /**
449
611
  * The Schedule 21 → Schedule 12 carry-forwards, as the form states them beside
450
612
  * each line:
451
613
  *
@@ -744,6 +906,37 @@ function schedule16Values(result) {
744
906
  };
745
907
  }
746
908
  //#endregion
909
+ //#region src/t2/at1/schedules/at4970-ieg-projects.ts
910
+ const nn$25 = (v) => Math.max(0, Math.round(v ?? 0));
911
+ function computeAt4970(input) {
912
+ const projects = input.projects.map((p) => ({
913
+ title: p.title,
914
+ ...p.projectCode !== void 0 ? { projectCode: p.projectCode } : {},
915
+ albertaPortion: nn$25(p.albertaPortion),
916
+ otherPortion: nn$25(p.otherPortion),
917
+ salariesAndWages: nn$25(p.salariesAndWages),
918
+ federalProxyAmount: nn$25(p.federalProxyAmount),
919
+ albertaProxyAmount: nn$25(p.albertaProxyAmount)
920
+ }));
921
+ const totals = {
922
+ albertaPortion: projects.reduce((s, p) => s + p.albertaPortion, 0),
923
+ otherPortion: projects.reduce((s, p) => s + p.otherPortion, 0),
924
+ salariesAndWages: projects.reduce((s, p) => s + p.salariesAndWages, 0),
925
+ federalProxyAmount: projects.reduce((s, p) => s + p.federalProxyAmount, 0),
926
+ albertaProxyAmount: projects.reduce((s, p) => s + p.albertaProxyAmount, 0)
927
+ };
928
+ const jurisdictions = (input.jurisdictions ?? []).map((j) => ({
929
+ jurisdiction: j.jurisdiction,
930
+ amountIncurred: nn$25(j.amountIncurred)
931
+ }));
932
+ return {
933
+ projects,
934
+ totals,
935
+ jurisdictions,
936
+ jurisdictionTotal: jurisdictions.reduce((s, j) => s + j.amountIncurred, 0)
937
+ };
938
+ }
939
+ //#endregion
747
940
  //#region src/t2/at1/schedules/schedule2.ts
748
941
  /** Round to six decimal places — the AT1 allocation-factor precision. */
749
942
  function round6(n) {
@@ -764,40 +957,182 @@ function computeAllocationFactor(input) {
764
957
  return round6(factor);
765
958
  }
766
959
  //#endregion
960
+ //#region src/t2/at1/schedules/schedule29-eligible-expenditures.ts
961
+ /**
962
+ * AT1 Schedule 29 page 1 — Eligible Expenditures for IEG Purposes (lines
963
+ * 001–040). AT29's own worksheet for deriving the "eligible expenditures"
964
+ * figure (line 031) that everything else on this schedule — the base amount,
965
+ * the increment, the Agreement's per-member 245/250/260 — is built from.
966
+ *
967
+ * ── Why this module exists separately from `schedule29-ieg.ts` ─────────────
968
+ *
969
+ * `computeIeg` took `eligibleExpenditures` as a bare number input. It is not
970
+ * one — it is DERIVED, by a six-line formula off federal T661, and a prior
971
+ * version of this package never modelled that derivation at all: it skipped
972
+ * page 1 of the live form entirely (only pages 2 and 3 were ever rendered),
973
+ * so every caller had to compute line 031 by hand outside the engine, with
974
+ * nothing here to check the arithmetic. Confirmed missing by rendering the
975
+ * live, TRA-certified form (`research/sources/tra-forms/pdf/AT1SCH29-*.pdf`,
976
+ * page 1) and independently re-verified against two of TRA's own published
977
+ * worked examples (`research/sources/tra-guides/tra-guide-claiming-the-
978
+ * innovation-employment-grant.pdf`, Examples 3 and 4) — see
979
+ * `research/knowledge-base/at1-schedule-29-ieg-mechanics.md`.
980
+ *
981
+ * ── The formula ──────────────────────────────────────────────────────────
982
+ *
983
+ * 003 federal amount of qualified/current SR&ED expenditures, T661 line
984
+ * 559 or 557 (see below)
985
+ * 005 portion of 003 carried out in Alberta — normally the sum of every
986
+ * AT4970 project row's own 105 column (`at4970-ieg-projects.ts`)
987
+ * 007 deduct: federal prescribed proxy amount included in the Alberta
988
+ * portion of 003 — normally Σ AT4970's 111 column
989
+ * 009 add: Alberta proxy amount — normally Σ AT4970's 113 column
990
+ * 011 add: IEG that reduced the federal expenditure IN THE TAXATION YEAR
991
+ * — zero for a first-time current-year claim reported on the
992
+ * pre-deduction federal figures (see the Step 1 / Step 2 note below)
993
+ * 025 add: the Alberta portion of a repayment of government assistance
994
+ * (other than an IEG) OR A CONTRACT PAYMENT, relating to amounts in
995
+ * 005 from the current year or ANY PRECEDING taxation year — two
996
+ * independent triggers, not one
997
+ * 031 TOTAL = 005 − 007 + 009 + 011 + 025
998
+ *
999
+ * 031 is what `computeIeg` calls `eligibleExpenditures` and what an
1000
+ * Agreement member's own `currentYearExpenditures` (line 245) is — the same
1001
+ * formula, computed once per corporation per year, whether or not that
1002
+ * corporation is associated.
1003
+ *
1004
+ * ── The federal T661 line 557 vs 559 split (Fall 2026 re-certification) ────
1005
+ *
1006
+ * TRA's own certification correspondence and the live form agree: for a
1007
+ * taxation year ending ON OR BEFORE 2024-12-15, line 003 comes from federal
1008
+ * T661 line 559 (qualified SR&ED expenditures); for a taxation year ending
1009
+ * ON OR AFTER 2024-12-16, it comes from T661 line 557 (CURRENT SR&ED
1010
+ * expenditures) instead — a different federal figure, not a renumbering.
1011
+ * `iegT661SourceLine` below is purely informational (which federal line a
1012
+ * preparer should be told to copy from); it does not change the Alberta
1013
+ * arithmetic itself, which is identical either way.
1014
+ *
1015
+ * ── The "Step 1 / Step 2" columns in TRA's Guide are not two form fields ────
1016
+ *
1017
+ * The Guide illustrates every page-1 line twice — once using the federal
1018
+ * figures BEFORE the current year's own IEG is netted against them as
1019
+ * government assistance (T661 line 513), once AFTER, with line 011 adding
1020
+ * the IEG back. Both worked examples land on the IDENTICAL line 031 either
1021
+ * way (011 exists specifically to undo the 513 netting for ALBERTA's own
1022
+ * base, so it cannot shrink itself). Only ONE set of figures is filed. Every
1023
+ * TRA test case gives the pre-deduction ("Step 1") federal figures — use
1024
+ * those directly, with 011 = 0 for a first-time current-year claim. Do not
1025
+ * build a literal two-pass loop; there is nothing for it to converge on that
1026
+ * isn't already true of the Step 1 figures alone.
1027
+ *
1028
+ * Whole dollars, pure.
1029
+ */
1030
+ /** Which federal T661 line line 003 sources from, given a taxation year end. */
1031
+ function iegT661SourceLine(taxationYearEnd) {
1032
+ return taxationYearEnd >= "2024-12-16" ? "557" : "559";
1033
+ }
1034
+ const nn$24 = (v) => Math.round(v ?? 0);
1035
+ /** Line 031 = 005 − 007 + 009 + 011 + 025. Every term but 005 defaults to nil. */
1036
+ function computeIegEligibleExpenditures(input) {
1037
+ const federalAmount = nn$24(input.federalAmount);
1038
+ const albertaPortion = nn$24(input.albertaPortion);
1039
+ const federalProxyAmount = nn$24(input.federalProxyAmount);
1040
+ const albertaProxyAmount = nn$24(input.albertaProxyAmount);
1041
+ const iegReducingFederalExpenditure = nn$24(input.iegReducingFederalExpenditure);
1042
+ const repaymentOrContractPayment = nn$24(input.repaymentOrContractPayment);
1043
+ return {
1044
+ federalAmount,
1045
+ albertaPortion,
1046
+ federalProxyAmount,
1047
+ albertaProxyAmount,
1048
+ iegReducingFederalExpenditure,
1049
+ repaymentOrContractPayment,
1050
+ totalEligibleExpenditures: albertaPortion - federalProxyAmount + albertaProxyAmount + iegReducingFederalExpenditure + repaymentOrContractPayment
1051
+ };
1052
+ }
1053
+ //#endregion
767
1054
  //#region src/t2/at1/schedules/schedule29-ieg.ts
768
1055
  /**
769
1056
  * AT1 Schedule 29 — Alberta Innovation Employment Grant (IEG). AT1 line 129.
770
1057
  *
771
1058
  * A refundable grant on eligible R&D (SR&ED) carried out in Alberta:
772
- * - 8% base rate on eligible expenditures up to the base level of spending
773
- * - 20% enhanced rate on the INCREMENT above the base level
774
- * (equivalently: 8% on everything + an extra 12% on the increment)
775
- * base level = average of the prior two years' eligible Alberta expenditures
776
- * (associated group aggregated — pass all members' figures)
777
- *
778
- * Eligible expenditures are capped at a $4,000,000 annual limit, reduced on a
779
- * straight-line taxable-capital grind ($10M $50M), shared across an associated
780
- * group. Pure, integer whole dollars.
781
- *
782
- * NOT modelled here (a later associated-group layer): allocation of the shared
783
- * $4M limit across associated corporations (Test Case 3's A/B/C/D grind). This
784
- * computes one corporation's grant given its eligible expenditures, its (group)
785
- * base amount, and its allocated expenditure limit.
1059
+ * - line 110: 8% of the CAPPED eligible expenditures (the full amount, not
1060
+ * just the portion below base)
1061
+ * - line 112 (non-associated): 12% of the increment ABOVE the base level of
1062
+ * spending — base level = average of the prior two years'
1063
+ * eligible Alberta expenditures (associated group aggregated —
1064
+ * pass all members' figures)
1065
+ * - line 125 (associated, via a filed agreement): 12% of the LESSER of the
1066
+ * expenditure limit and the agreement's allocated allowed
1067
+ * amount see `computeIegAgreement` in `schedule29-ieg-group.ts`.
1068
+ * A genuinely different formula, not a variant of 112's — it does
1069
+ * not reference actual spending at all once the agreement exists.
1070
+ *
1071
+ * Eligible expenditures are capped at a $4,000,000 annual limit (shared across an
1072
+ * associated group; NOT reduced by taxable capital — see below). Pure, integer
1073
+ * whole dollars.
1074
+ *
1075
+ * ── The taxable-capital grind applies to the CREDIT, not the limit ──────────
1076
+ *
1077
+ * A prior version of this module reduced the $4,000,000 expenditure limit itself
1078
+ * on a straight-line taxable-capital grind ($10M → $50M) — modelled on the
1079
+ * federal SBD grind's shape, but wrong for this form. Rendering the live,
1080
+ * TRA-certified Schedule 29 (`research/sources/tra-forms/pdf/AT1SCH29-*.pdf`,
1081
+ * Rev. 2026-06 — cross-checked against AuraTax's live rendering of the same
1082
+ * schedule, `research/validation/auratax/2026-08-25-at1-schedule1-royalty/`)
1083
+ * shows the grind lives at **line 128**, a REDUCTION FACTOR from 0 to 1:
1084
+ *
1085
+ * line 128 = (($40,000,000 − (line 126 − $10,000,000)) / $40,000,000)
1086
+ * (enter 1 if taxable capital ≤ $10,000,000)
1087
+ * line 130 = (line 110 + (line 112 or line 125)) × line 128
1088
+ *
1089
+ * i.e. the credit is computed in FULL against the unreduced $4M limit, and ONLY
1090
+ * THEN scaled by the taxable-capital factor — not the limit ground down first
1091
+ * and the credit computed against the smaller number. The two are NOT
1092
+ * equivalent except at the band's endpoints (TC ≤ $10M ⇒ both give the full
1093
+ * credit; TC ≥ $50M ⇒ both give nil).
1094
+ *
1095
+ * ── One thing not yet resolved, and worth raising with TRA directly ─────────
1096
+ *
1097
+ * The AT1 jacket's OWN business-rule table (`research/sources/tra-spec/
1098
+ * AT1-Chapter3-2025.2-full.txt`, version 2025.2 / August 2025 — ten months
1099
+ * OLDER than the Schedule 29 PDF above) states jacket line 129 as
1100
+ * `(029110 + 029112 X 029128) minus 029132` — multiplying ONLY line 112 by the
1101
+ * factor, not the sum of 110 and 112. That contradicts Schedule 29's own
1102
+ * printed caption, which the certified oracle (AuraTax) also renders as
1103
+ * "sum then multiply". This module follows the newer, doubly-corroborated
1104
+ * Schedule 29 form. Confirm with TRA before relying on this for an associated
1105
+ * corporation with taxable capital between $10M and $50M — the disagreement
1106
+ * changes the filed dollar figure.
1107
+ *
1108
+ * ── The formal Agreement (line 125) ──────────────────────────────────────────
1109
+ *
1110
+ * TRA's Test Case 3 (four associated corporations, `research/sources/
1111
+ * tra-test-cases/Alberta Test Case 3 - Fall 2025.txt`) is the associated path:
1112
+ * pass `associatedAllowedAmount` (line 325 of `computeIegAgreement`'s result)
1113
+ * and this function uses line 125's formula instead of line 112's. Day-of-year
1114
+ * proration of the $4M pool for a short or long taxation year (lines 104/108/
1115
+ * 206/208) is modelled in `computeIegAgreement`, not here — this function
1116
+ * takes the already-allocated `expenditureLimit` as an input.
786
1117
  */
787
1118
  const IEG_2024 = Object.freeze({
788
1119
  BASE_RATE: .08,
789
- ENHANCED_RATE: .2,
1120
+ ENHANCED_RATE: .12,
790
1121
  MAX_EXPENDITURE: 4e6,
791
1122
  TC_GRIND_LOWER: 1e7,
792
1123
  TC_GRIND_UPPER: 5e7,
793
1124
  PRIOR_YEARS: 2
794
1125
  });
795
- /** The eligible-expenditure limit ($4M) reduced by the taxable-capital grind. */
796
- function computeIegExpenditureLimit(taxableCapital, sharedLimit = IEG_2024.MAX_EXPENDITURE) {
797
- const { TC_GRIND_LOWER, TC_GRIND_UPPER } = IEG_2024;
798
- const tcInBand = Math.max(0, Math.min(Math.max(0, taxableCapital), TC_GRIND_UPPER) - TC_GRIND_LOWER);
799
- const reduction = Math.round(sharedLimit * tcInBand / (TC_GRIND_UPPER - TC_GRIND_LOWER));
800
- return Math.max(0, sharedLimit - reduction);
1126
+ /**
1127
+ * The line-128 reduction factor: 1 at or below $10M taxable capital, straight-
1128
+ * line down to 0 at $50M. Multiplies the COMBINED base + enhanced credit at
1129
+ * line 130 it does not touch the $4,000,000 expenditure limit.
1130
+ */
1131
+ function computeIegReductionFactor(taxableCapital, lower = IEG_2024.TC_GRIND_LOWER, upper = IEG_2024.TC_GRIND_UPPER) {
1132
+ const tc = Math.max(0, taxableCapital);
1133
+ if (tc <= lower) return 1;
1134
+ const band = upper - lower;
1135
+ return Math.max(0, Math.min(1, (band - (tc - lower)) / band));
801
1136
  }
802
1137
  /**
803
1138
  * Base level of spending = average of the prior years' eligible Alberta
@@ -813,16 +1148,26 @@ function computeIeg(input) {
813
1148
  const limit = input.expenditureLimit ?? IEG_2024.MAX_EXPENDITURE;
814
1149
  const cappedExpenditures = Math.max(0, Math.min(input.eligibleExpenditures, limit));
815
1150
  const incrementalExpenditures = Math.max(0, cappedExpenditures - input.baseAmount);
816
- const baseRatePortion = cappedExpenditures - incrementalExpenditures;
817
- const creditAtBaseRate = Math.round(IEG_2024.BASE_RATE * baseRatePortion);
818
- const creditAtEnhancedRate = Math.round(IEG_2024.ENHANCED_RATE * incrementalExpenditures);
1151
+ const creditAtBaseRate = Math.round(IEG_2024.BASE_RATE * cappedExpenditures);
1152
+ const enhancedRateBasis = input.associatedAllowedAmount !== void 0 ? "associated" : "non-associated";
1153
+ const creditAtEnhancedRate = enhancedRateBasis === "associated" ? Math.round(IEG_2024.ENHANCED_RATE * Math.min(limit, Math.max(0, input.associatedAllowedAmount ?? 0))) : Math.round(IEG_2024.ENHANCED_RATE * incrementalExpenditures);
1154
+ const taxableCapital = Math.max(0, input.taxableCapital ?? 0);
1155
+ const reductionFactor = computeIegReductionFactor(taxableCapital);
1156
+ const grossIeg = Math.round((creditAtBaseRate + creditAtEnhancedRate) * reductionFactor);
1157
+ const recapture = Math.max(0, Math.round(input.recapture ?? 0));
1158
+ const ieg = Math.max(0, grossIeg - recapture);
819
1159
  return {
820
1160
  cappedExpenditures,
821
1161
  baseAmount: input.baseAmount,
822
1162
  incrementalExpenditures,
823
1163
  creditAtBaseRate,
824
1164
  creditAtEnhancedRate,
825
- ieg: creditAtBaseRate + creditAtEnhancedRate
1165
+ enhancedRateBasis,
1166
+ taxableCapital,
1167
+ reductionFactor,
1168
+ grossIeg,
1169
+ recapture,
1170
+ ieg
826
1171
  };
827
1172
  }
828
1173
  //#endregion
@@ -831,20 +1176,27 @@ function computeIeg(input) {
831
1176
  * AT1 Schedule 29 — the ASSOCIATED-GROUP layer for the Innovation Employment
832
1177
  * Grant.
833
1178
  *
834
- * `computeIeg` grants one corporation its IEG given three figures: its own
835
- * eligible Alberta expenditures, its base level of spending, and its allocated
836
- * expenditure limit. Two of those three are **group** figures, and this module
837
- * derives them.
1179
+ * `computeIeg` grants one corporation its IEG given its own eligible Alberta
1180
+ * expenditures, its base level of spending, its allocated expenditure limit, and
1181
+ * the group's taxable capital. The base level of spending is a **group** figure,
1182
+ * and this module derives it (along with the group's total taxable capital, for
1183
+ * the line-128 reduction factor `computeIeg` applies).
838
1184
  *
839
1185
  * ── Why a group layer is not optional ───────────────────────────────────────
840
1186
  *
841
- * A single corporation with $10M of taxable capital sees no grind at all and gets
842
- * the whole $4,000,000 limit. The same corporation in a group whose members total
843
- * $20M sees the limit ground to $3,000,000. Computing a member in isolation
844
- * therefore **overstates the limit**, and a group that also has prior-year
845
- * spending elsewhere gets an **understated base**, which overstates the 20%
846
- * increment. Both errors run in the taxpayer's favour, which is the direction that
847
- * fails an audit.
1187
+ * A group that has prior-year spending elsewhere gets an **understated base**
1188
+ * when a member is computed alone, which overstates the 12% increment. And the
1189
+ * taxable-capital reduction factor (line 128, see `schedule29-ieg.ts`) is driven
1190
+ * by the WHOLE group's taxable capital, not one member's — a member with $10M of
1191
+ * its own taxable capital sees no grind in isolation, but the group's combined
1192
+ * capital may put it well into the $10M–$50M band. Both errors run in the
1193
+ * taxpayer's favour, which is the direction that fails an audit.
1194
+ *
1195
+ * The $4,000,000 expenditure limit itself is NOT reduced here — see
1196
+ * `schedule29-ieg.ts` for why the taxable-capital grind lives at line 128, not
1197
+ * the limit. `groupExpenditureLimit` below is the flat $4M shared pool, prorated
1198
+ * for a short or long taxation year by the caller if needed (day-of-year
1199
+ * proration is not modelled).
848
1200
  *
849
1201
  * ── Which year's figures ────────────────────────────────────────────────────
850
1202
  *
@@ -871,6 +1223,37 @@ function computeIeg(input) {
871
1223
  * contributes its taxable capital to the grind. Both cases appear in the Alberta
872
1224
  * certification material and both are modelled.
873
1225
  *
1226
+ * ── ⚠ This module's "group aggregated base amount" has no line on the real form ──
1227
+ *
1228
+ * Rendering Schedule 29 page 2 in full (a prior version of this package only
1229
+ * ever rendered pages 2 and 3's CREDIT section, never page 1) shows the base
1230
+ * amount (lines 114/116/118) captioned "*If the corporation is NOT
1231
+ * associated with one or more corporations in the taxation year, the Base
1232
+ * Amount is the amount that is the average of the eligible expenditures OF
1233
+ * THE CORPORATION for the two immediately preceding taxation years" — the
1234
+ * CLAIMANT's own figures alone, no group aggregation, and used ONLY for the
1235
+ * non-associated path (line 112). Confirmed blank in both of TRA's own
1236
+ * published worked examples whenever the corporation IS associated — the
1237
+ * associated path (line 125) uses no base amount at all, replacing the
1238
+ * whole mechanism with the Agreement's Allowed Amount instead (see
1239
+ * `computeIegAgreement` below). The live form also states an associated
1240
+ * corporation MUST complete the Agreement ("If 'Yes', complete page 3") —
1241
+ * so there is no scenario on the real form where a group is associated but
1242
+ * has not filed one. `computeIegGroupFigures`'s "aggregate every member's
1243
+ * prior-two-years spending into one shared base amount", used regardless of
1244
+ * associated/non-associated status by `computeAlbertaReturn`, therefore
1245
+ * does not correspond to anything a preparer could actually file — it is
1246
+ * harmless ONLY because `computeIeg`'s associated branch ignores
1247
+ * `baseAmount` entirely. Left in place rather than removed: not exercised by
1248
+ * any of TRA's Fall 2026 certification test cases (all either non-associated
1249
+ * with a genuine single-corporation base, or associated with a filed
1250
+ * Agreement whose own figures are what actually reach the credit), so a full
1251
+ * rearchitecture is deferred rather than rushed. `taxableCapital` for the
1252
+ * associated path is already sourced from the Agreement, not from here — see
1253
+ * `alberta-return.ts`. See `research/knowledge-base/
1254
+ * at1-schedule-29-ieg-mechanics.md` for the full comparison against the live
1255
+ * form and both of TRA's published worked examples.
1256
+ *
874
1257
  * Whole dollars, pure.
875
1258
  */
876
1259
  const nn$23 = (v) => Math.max(0, Math.round(v ?? 0));
@@ -897,7 +1280,7 @@ function computeIegGroupFigures(members, priorYears = IEG_2024.PRIOR_YEARS) {
897
1280
  const totalPriorYearExpenditures = detail.reduce((s, m) => s + m.priorYearTotal, 0);
898
1281
  return {
899
1282
  groupTaxableCapital,
900
- groupExpenditureLimit: computeIegExpenditureLimit(groupTaxableCapital),
1283
+ groupExpenditureLimit: IEG_2024.MAX_EXPENDITURE,
901
1284
  totalPriorYearExpenditures,
902
1285
  groupBaseAmount: priorYears > 0 ? Math.round(totalPriorYearExpenditures / priorYears) : 0,
903
1286
  members: detail,
@@ -952,6 +1335,74 @@ function allocateIegEvenly(groupExpenditureLimit, names) {
952
1335
  allocated: i === 0 ? each + remainder : each
953
1336
  })));
954
1337
  }
1338
+ function computeAgreementMember(m, index, issues) {
1339
+ const allocatedExpenditureLimit = nn$23(m.allocatedExpenditureLimit);
1340
+ const currentYearExpenditures = nn$23(m.currentYearExpenditures);
1341
+ const priorYear1 = nn$23(m.priorYear1);
1342
+ const priorYear2 = nn$23(m.priorYear2);
1343
+ const base = Math.round((priorYear1 + priorYear2) / 2);
1344
+ const individualMaximumAllowedAmount = currentYearExpenditures - base;
1345
+ const days = Math.max(0, Math.min(m.daysInTaxYear ?? 365, 366));
1346
+ const dayProratedLimit = Math.round(4e6 * (days / 365));
1347
+ const dayProratedComponent = Math.min(dayProratedLimit, currentYearExpenditures) - base;
1348
+ const isClaimant = index === 0;
1349
+ if (m.hasAlbertaPermanentEstablishment === void 0 && !isClaimant) issues.push(`Alberta Schedule 29 Agreement: ${m.name} did not state whether it has a permanent establishment in Alberta. Treated as no permanent establishment (line 268 = nil) — a member without one is not eligible for the IEG even when its own formula would allow an amount. Confirm and re-enter if this member does have an Alberta PE.`);
1350
+ const hasAlbertaPermanentEstablishment = m.hasAlbertaPermanentEstablishment ?? isClaimant;
1351
+ const allocatedAllowedAmount = hasAlbertaPermanentEstablishment ? Math.max(0, Math.min(allocatedExpenditureLimit, individualMaximumAllowedAmount, dayProratedComponent)) : 0;
1352
+ return {
1353
+ name: m.name,
1354
+ ...m.federalBusinessNumber !== void 0 ? { federalBusinessNumber: m.federalBusinessNumber } : {},
1355
+ ...m.albertaCan !== void 0 ? { albertaCan: m.albertaCan } : {},
1356
+ ...m.currentTaxationYearEnd !== void 0 ? { currentTaxationYearEnd: m.currentTaxationYearEnd } : {},
1357
+ allocatedExpenditureLimit,
1358
+ currentYearExpenditures,
1359
+ priorYear1,
1360
+ priorYear2,
1361
+ taxableCapitalPriorYear: nn$23(m.taxableCapitalPriorYear),
1362
+ hasAlbertaPermanentEstablishment,
1363
+ individualMaximumAllowedAmount,
1364
+ allocatedAllowedAmount
1365
+ };
1366
+ }
1367
+ /**
1368
+ * The formal Agreement Among Associated Corporations. The FIRST entry in
1369
+ * `input.members` must be the claiming corporation — its own line 268 becomes
1370
+ * line 325, which is what actually reaches `computeIeg`.
1371
+ */
1372
+ function computeIegAgreement(input) {
1373
+ const issues = [];
1374
+ if (input.members.length === 0) issues.push("Alberta Schedule 29 Agreement: no members were supplied. The claiming corporation must appear as the first row even in a two-corporation group.");
1375
+ const days = Math.max(0, Math.min(input.daysInLongestYear, 366));
1376
+ const maximumExpenditureLimit = Math.round(4e6 * (days / 365));
1377
+ const members = input.members.map((m, i) => computeAgreementMember(m, i, issues));
1378
+ const sum = (get) => members.reduce((s, m) => s + get(m), 0);
1379
+ const totalAllocatedLimit = sum((m) => m.allocatedExpenditureLimit);
1380
+ const totalCurrentYearExpenditures = sum((m) => m.currentYearExpenditures);
1381
+ const totalPriorYear1 = sum((m) => m.priorYear1);
1382
+ const totalPriorYear2 = sum((m) => m.priorYear2);
1383
+ const totalTaxableCapitalPriorYear = sum((m) => m.taxableCapitalPriorYear);
1384
+ const totalAllocatedAllowedAmount = sum((m) => m.allocatedAllowedAmount);
1385
+ const groupAllowedAmount = Math.round(totalCurrentYearExpenditures - (totalPriorYear1 + totalPriorYear2) / 2);
1386
+ if (totalAllocatedLimit > maximumExpenditureLimit) issues.push(`Alberta Schedule 29 Agreement: the group allocated ${totalAllocatedLimit} of a maximum expenditure limit of ${maximumExpenditureLimit} (line 270 exceeds line 208). The group must agree a valid split.`);
1387
+ if (totalAllocatedAllowedAmount > groupAllowedAmount) issues.push(`Alberta Schedule 29 Agreement: the group's allocated allowed amounts total ${totalAllocatedAllowedAmount}, exceeding the group Allowed Amount of ${groupAllowedAmount} (line 320 exceeds line 310). The group must agree a valid split.`);
1388
+ return {
1389
+ longestYearCan: input.longestYearCan,
1390
+ longestYearBegin: input.longestYearBegin,
1391
+ longestYearEnd: input.longestYearEnd,
1392
+ daysInLongestYear: days,
1393
+ maximumExpenditureLimit,
1394
+ members,
1395
+ totalAllocatedLimit,
1396
+ totalCurrentYearExpenditures,
1397
+ totalPriorYear1,
1398
+ totalPriorYear2,
1399
+ totalTaxableCapitalPriorYear,
1400
+ groupAllowedAmount,
1401
+ totalAllocatedAllowedAmount,
1402
+ claimantAllocatedAllowedAmount: members[0]?.allocatedAllowedAmount ?? 0,
1403
+ issues
1404
+ };
1405
+ }
955
1406
  //#endregion
956
1407
  //#region src/t2/at1/engine/alberta-return.ts
957
1408
  /**
@@ -982,21 +1433,49 @@ function computeAlbertaReturn(input) {
982
1433
  }, rates);
983
1434
  const issues = [];
984
1435
  let iegGroup;
1436
+ let iegAgreement;
1437
+ let iegEligibleExpenditures;
985
1438
  let ieg;
986
- if (input.ieg) if (input.ieg.group.length === 0) issues.push("AT1 Schedule 29: no associated-group members were supplied, so no grant was claimed. Pass the claimant itself even when it has no associated corporations — an empty group would otherwise imply no taxable-capital grind and a nil base, producing the maximum possible grant on absent data.");
987
- else {
988
- iegGroup = computeIegGroupFigures(input.ieg.group);
989
- issues.push(...iegGroup.issues);
990
- const allocation = allocateIegExpenditureLimit(iegGroup.groupExpenditureLimit, [{
991
- name: "claimant",
992
- allocated: input.ieg.allocatedLimit ?? iegGroup.groupExpenditureLimit
993
- }]);
994
- issues.push(...allocation.issues);
995
- ieg = computeIeg({
996
- eligibleExpenditures: input.ieg.eligibleExpenditures,
997
- baseAmount: iegGroup.groupBaseAmount,
998
- expenditureLimit: allocation.allocations[0]?.allocated ?? 0
999
- });
1439
+ let at4970;
1440
+ if (input.ieg) {
1441
+ if (input.ieg.at4970) at4970 = computeAt4970(input.ieg.at4970);
1442
+ let resolvedEligibleExpenditures = input.ieg.eligibleExpenditures ?? 0;
1443
+ if (input.ieg.eligible) {
1444
+ const e = input.ieg.eligible;
1445
+ const federalProxyAmount = e.federalProxyAmount ?? at4970?.totals.federalProxyAmount;
1446
+ const albertaProxyAmount = e.albertaProxyAmount ?? at4970?.totals.albertaProxyAmount;
1447
+ iegEligibleExpenditures = computeIegEligibleExpenditures({
1448
+ federalAmount: e.federalAmount,
1449
+ albertaPortion: e.albertaPortion ?? at4970?.totals.albertaPortion ?? 0,
1450
+ ...federalProxyAmount !== void 0 ? { federalProxyAmount } : {},
1451
+ ...albertaProxyAmount !== void 0 ? { albertaProxyAmount } : {},
1452
+ ...e.iegReducingFederalExpenditure !== void 0 ? { iegReducingFederalExpenditure: e.iegReducingFederalExpenditure } : {},
1453
+ ...e.repaymentOrContractPayment !== void 0 ? { repaymentOrContractPayment: e.repaymentOrContractPayment } : {}
1454
+ });
1455
+ resolvedEligibleExpenditures = iegEligibleExpenditures.totalEligibleExpenditures;
1456
+ }
1457
+ if (input.ieg.group.length === 0) issues.push("AT1 Schedule 29: no associated-group members were supplied, so no grant was claimed. Pass the claimant itself even when it has no associated corporations — an empty group would otherwise imply no taxable-capital grind and a nil base, producing the maximum possible grant on absent data.");
1458
+ else {
1459
+ iegGroup = computeIegGroupFigures(input.ieg.group);
1460
+ issues.push(...iegGroup.issues);
1461
+ const allocation = allocateIegExpenditureLimit(iegGroup.groupExpenditureLimit, [{
1462
+ name: "claimant",
1463
+ allocated: input.ieg.allocatedLimit ?? iegGroup.groupExpenditureLimit
1464
+ }]);
1465
+ issues.push(...allocation.issues);
1466
+ if (input.ieg.agreement) {
1467
+ iegAgreement = computeIegAgreement(input.ieg.agreement);
1468
+ issues.push(...iegAgreement.issues);
1469
+ }
1470
+ ieg = computeIeg({
1471
+ eligibleExpenditures: resolvedEligibleExpenditures,
1472
+ baseAmount: iegGroup.groupBaseAmount,
1473
+ expenditureLimit: allocation.allocations[0]?.allocated ?? 0,
1474
+ taxableCapital: iegAgreement ? iegAgreement.totalTaxableCapitalPriorYear : iegGroup.groupTaxableCapital,
1475
+ ...input.ieg.recapture !== void 0 ? { recapture: input.ieg.recapture } : {},
1476
+ ...iegAgreement ? { associatedAllowedAmount: iegAgreement.claimantAllocatedAllowedAmount } : {}
1477
+ });
1478
+ }
1000
1479
  }
1001
1480
  const schedulePayloads = [];
1002
1481
  const sched = input.schedules;
@@ -1010,13 +1489,16 @@ function computeAlbertaReturn(input) {
1010
1489
  if (sched?.dispositions) schedulePayloads.push(schedule18Values(sched.dispositions));
1011
1490
  if (sched?.donations) schedulePayloads.push(schedule20Values(sched.donations));
1012
1491
  if (sched?.losses) schedulePayloads.push(schedule21Values(sched.losses));
1013
- if (ieg) schedulePayloads.push(schedule29Values(ieg));
1492
+ if (ieg) schedulePayloads.push(schedule29Values(ieg, iegAgreement, iegEligibleExpenditures, input.ieg?.primaryFieldCode));
1493
+ if (at4970) schedulePayloads.push(schedule4970Values(at4970));
1014
1494
  return {
1015
1495
  allocationFactor,
1016
1496
  albertaTax,
1017
1497
  albertaTaxPayable: albertaTax.albertaTaxPayable,
1018
1498
  schedulePayloads,
1019
1499
  ...iegGroup ? { iegGroup } : {},
1500
+ ...iegAgreement ? { iegAgreement } : {},
1501
+ ...iegEligibleExpenditures ? { iegEligibleExpenditures } : {},
1020
1502
  ...ieg ? { ieg } : {},
1021
1503
  innovationEmploymentGrant: ieg?.ieg ?? 0,
1022
1504
  issues
@@ -1166,6 +1648,7 @@ const AT1_MANDATORY_WITHOUT_DEFAULT = Object.freeze([
1166
1648
  function assertAt1MandatoryComplete(d) {
1167
1649
  const blank = (v) => v === void 0 || v === null || String(v).trim() === "";
1168
1650
  const missing = AT1_MANDATORY_WITHOUT_DEFAULT.filter(([key]) => blank(d[key])).map(([, label]) => label);
1651
+ if (d.transmitter?.isAmended && blank(d.transmitter.amendmentDescription)) missing.push("EDI073 description of changes (required when the amended return indicator is set)");
1169
1652
  if (missing.length > 0) throw new At1MandatoryFieldMissingError(missing);
1170
1653
  }
1171
1654
  /** Refuse to render when any critical mandatory field is absent. */
@@ -1525,6 +2008,16 @@ const AT1_EDI_LINE_ITEMS = [
1525
2008
  id: "EDI041001",
1526
2009
  get: (t) => t.contact.email,
1527
2010
  fmt: "text"
2011
+ },
2012
+ {
2013
+ id: "EDI071001",
2014
+ get: (t) => t.isAmended ? "1" : void 0,
2015
+ fmt: "text"
2016
+ },
2017
+ {
2018
+ id: "EDI073001",
2019
+ get: (t) => t.isAmended ? t.amendmentDescription : void 0,
2020
+ fmt: "text"
1528
2021
  }
1529
2022
  ];
1530
2023
  //#endregion
@@ -2016,8 +2509,9 @@ function albertaCurrentYearLoss(result) {
2016
2509
  *
2017
2510
  * The STRAIGHT-LINE classes (13 leasehold interests, 14 limited-life intangibles)
2018
2511
  * are deliberately NOT here — they use their own arithmetic in Reg 1100(1)(b) and
2019
- * 1100(1)(c), so `computeCcaClass` throws for them rather than silently applying
2020
- * a declining-balance rate. See `schedules/cca-straight-line.ts`.
2512
+ * 1100(1)(c), so `computeCcaClass` never looks them up in this table. It handles
2513
+ * an EXISTING opening-UCC pool for them directly (see `schedule8.ts`); a NEW
2514
+ * addition needs the full layer/property mechanics in `schedules/cca-straight-line.ts`.
2021
2515
  *
2022
2516
  * Class 14.1 IS a declining-balance class (5%) and belongs here. Its pre-2027
2023
2517
  * transitional additional allowance — which keeps pre-2017 expenditures moving at
@@ -2303,19 +2797,79 @@ function computeClass141AdditionalAllowance(input) {
2303
2797
  * Terminal loss: if the class is EMPTIED (no assets left) with positive UCC, the
2304
2798
  * remaining UCC is deducted as a terminal loss, no CCA, closing UCC nil.
2305
2799
  *
2306
- * NOT modelled (caller / other schedules): class 13/14/14.1 straight-line, the
2307
- * shared $1.5M immediate-expensing limit across an associated group (caller passes
2308
- * the already-capped eligible amount), and the 2024+ AIIP phase-out factor.
2800
+ * Class 13/14 (straight-line, Reg 1100(1)(b)/(c)): this function accepts them
2801
+ * too, but ONLY as an existing opening-UCC pool being drawn down no rate, no
2802
+ * half-year rule, no AIIP; the claim is capped at the UCC before CCA and
2803
+ * nothing else. A current-year ADDITION on one of these classes is refused
2804
+ * (see `UnsupportedCcaClassError`) rather than silently claimed without the
2805
+ * 5-year floor / remaining-life proration Schedule III and Reg 1100(1)(c)
2806
+ * require for a NEW layer or property — that full mechanic is
2807
+ * `computeClass13` / `computeClass14` in `cca-straight-line.ts`, which this
2808
+ * entry point does not have layer/remaining-life data to drive.
2809
+ *
2810
+ * NOT modelled (caller / other schedules): a NEW class 13/14 addition (see
2811
+ * above), class 14.1's transitional additional allowance, the shared $1.5M
2812
+ * immediate-expensing limit across an associated group (caller passes the
2813
+ * already-capped eligible amount), and the 2024+ AIIP phase-out factor.
2309
2814
  */
2310
2815
  var UnsupportedCcaClassError = class extends Error {
2311
2816
  ccaClass;
2312
- constructor(ccaClass) {
2313
- super(`CCA class ${ccaClass} is not a declining-balance class this engine computes yet (straight-line / special classes like 13, 14, 14.1 use different mechanics).`);
2817
+ constructor(ccaClass, reason) {
2818
+ super(reason ?? `CCA class ${ccaClass} is not a declining-balance class this engine computes yet (straight-line / special classes like 13, 14, 14.1 use different mechanics).`);
2314
2819
  this.name = "UnsupportedCcaClassError";
2315
2820
  this.ccaClass = ccaClass;
2316
2821
  }
2317
2822
  };
2823
+ /** Class 13/14 accept only an opening-UCC drawdown here — see the file docstring. */
2824
+ const STRAIGHT_LINE_OPENING_BALANCE_ONLY = /* @__PURE__ */ new Set(["13", "14"]);
2825
+ /**
2826
+ * Class 13/14, opening-balance drawdown only: no rate, no half-year rule, no
2827
+ * AIIP, no immediate expensing — none of those concepts apply to a
2828
+ * straight-line class. The claim is simply capped at the UCC before CCA.
2829
+ * Recapture and terminal loss reuse the same general rules as every other
2830
+ * class (s.13(1) / s.20(16) are not declining-balance-specific).
2831
+ */
2832
+ function computeStraightLineOpeningBalanceClass(input) {
2833
+ if (Math.max(0, input.additions ?? 0) > 0) throw new UnsupportedCcaClassError(input.ccaClass, `CCA class ${input.ccaClass}: a current-year addition needs the detailed leasehold/limited-life schedule (computeClass13 / computeClass14 in cca-straight-line.ts), which this entry point has no lease-term / remaining-life data to drive. Drawing down an EXISTING opening balance (no addition this year) is supported directly.`);
2834
+ const dispositions = Math.max(0, input.dispositions ?? 0);
2835
+ const netAdjustments = input.netAdjustments ?? 0;
2836
+ const uccBeforeCca = input.openingUCC + netAdjustments - dispositions;
2837
+ const base = {
2838
+ ccaClass: input.ccaClass,
2839
+ rate: 0,
2840
+ uccBeforeCca,
2841
+ immediateExpensingClaim: 0,
2842
+ halfYearAdjustment: 0,
2843
+ aiipEnhancement: 0,
2844
+ ccaBase: 0,
2845
+ maxCca: 0,
2846
+ ccaClaimed: 0,
2847
+ closingUCC: 0,
2848
+ recapture: 0,
2849
+ terminalLoss: 0
2850
+ };
2851
+ if (uccBeforeCca < 0) return {
2852
+ ...base,
2853
+ closingUCC: 0,
2854
+ recapture: -uccBeforeCca
2855
+ };
2856
+ if (input.classEmptied && uccBeforeCca > 0) return {
2857
+ ...base,
2858
+ closingUCC: 0,
2859
+ terminalLoss: uccBeforeCca
2860
+ };
2861
+ const maxCca = uccBeforeCca;
2862
+ const ccaClaimed = input.claim != null ? Math.min(Math.max(0, input.claim), maxCca) : maxCca;
2863
+ return {
2864
+ ...base,
2865
+ ccaBase: uccBeforeCca,
2866
+ maxCca,
2867
+ ccaClaimed,
2868
+ closingUCC: uccBeforeCca - ccaClaimed
2869
+ };
2870
+ }
2318
2871
  function computeCcaClass(input, rates = CCA_DECLINING_BALANCE_RATES_2024, prorationFactor = 1) {
2872
+ if (STRAIGHT_LINE_OPENING_BALANCE_ONLY.has(input.ccaClass)) return computeStraightLineOpeningBalanceClass(input);
2319
2873
  const rate = rates[input.ccaClass];
2320
2874
  if (rate === void 0) throw new UnsupportedCcaClassError(input.ccaClass);
2321
2875
  const additions = Math.max(0, input.additions ?? 0);
@@ -6173,4 +6727,4 @@ function computeMpDeduction(input, rates = MP_RATES_2024) {
6173
6727
  };
6174
6728
  }
6175
6729
  //#endregion
6176
- export { computeProvincialAllocation as $, at1TaxPayableDeductions as $n, CEC_DEDUCTION_RATE as $t, runConformance as A, reconcileAlbertaNetIncome as An, AB_GENERAL_RATE_BANDS as Ar, computeCcpcActiveBusinessTax as At, computeGrip as B, formatRsiText as Bn, AT1_DONATION_GAIN_RATE as Bt, computeQuebecTax as C, albertaCcaScheduleAdjustments as Cn, schedule1Values as Cr, blendProvinceRateTable as Ct, T2_LINE_META as D, albertaReserveDifference as Dn, schedule2Values as Dr, dividendsDeductibleS112 as Dt, resolveQuebecTaxRates as E, albertaRecaptureDifference as En, schedule29Values as Er, computeTaxableIncome as Et, normalizeSchedule88 as F, RSI_NEGATIVE_PREFIX as Fn, earliestRateYear as Fr, CORP_TAX_RATE_BOOK as Ft, ITC_RECAPTURE_PERIOD_YEARS as G, AT1_CRITICAL_MANDATORY_FIELDS as Gn, SECTION_34_2_GROSS_UP as Gt, LARGE_CORPORATION_THRESHOLD as H, renderRsiHeader as Hn, computeDonationMaximum as Ht, computeSchedule55 as I, RSI_WORD_GAP as In, extendRateBook as Ir, resolveCorpTaxRates as It, allocateEvenly as J, At1TaxPayableMismatchError as Jn, AT1_RESERVE_LINES as Jt, computeItcRecapture as K, At1CriticalFieldMissingError as Kn, computeAlbertaSchedule18 as Kt, LRIP_INVESTMENT_CORPORATION_MULTIPLE as L, RsiLineItemError as Ln, hasExactRateYear as Lr, T2_CERTIFICATION_FIXTURES as Lt, computeFederalT2 as M, computeLossCarryback as Mn, AB_TAX_2024 as Mr, computeBusinessLimit as Mt, computeSchedule101 as N, RSI_COLUMN_GAP as Nn, AB_TAX_RATE_BOOK as Nr, computeSBD as Nt, foldT2Lines as O, albertaTerminalLossDifference as On, computeAlbertaTax as Or, netCapitalLossApplied as Ot, SCHEDULE_88_MAX_URLS as P, RSI_DELIMITER as Pn, resolveAlbertaTaxRates as Pr, CORP_TAX_2024 as Pt, computeSchedule6 as Q, assertCriticalFields as Qn, computeAlbertaSchedule16 as Qt, LRIP_INVESTMENT_INCOME_FACTOR as R, formatRsiAmount as Rn, latestRateYear as Rr, computeLossSchedule as Rt, computeQuebecAllocationFactor as S, albertaCcaDifference as Sn, schedule18Values as Sr, resolveProvinceRates as St, QC_TAX_RATE_BOOK as T, albertaDispositionAdjustments as Tn, schedule21Values as Tr, charitableDonationsDeduction as Tt, computeTaxableCapital as U, renderRsiLineItem as Un, computeSchedule20 as Ut, computeSchedule43 as V, renderAt1Rsi as Vn, AT1_DONATION_INCOME_RATE as Vt, computeSchedule31 as W, renderAt1NetFile as Wn, AT1_DISPOSITION_CATEGORIES as Wt, computeSchedule21 as X, assertAt1MandatoryComplete as Xn, computeAlbertaSchedule17 as Xt, computeBusinessLimitAllocation as Y, albertaBalanceUnpaid as Yn, AT1_RESERVE_TOTAL_LINES as Yt, computeSchedule13 as Z, assertAt1TaxPayableReconciles as Zn, assistanceFrom as Zt, renderT2DraftReturn as _, CCA_RATE_BOOK as _n, schedule12LossDeductions as _r, EIFEL_EFFECTIVE_FROM as _t, PART_VI_1_DEDUCTION_BANDS as a, computeCcaClass as an, allocateIegExpenditureLimit as ar, computeSchedule2 as at, co17Engine as b, albertaAbilDifference as bn, schedule16Values as br, PROVINCE_RATE_BOOK as bt, EIFEL_FIRST_YEAR_START as c, CLASS_14_1_MINIMUM_DEDUCTION as cn, computeIeg as cr, assertSchedule1Fileable as ct, EIFEL_TRANSITIONAL_RATIO as d, MIN_LEASEHOLD_PERIODS as dn, SINGLE_JURISDICTION_ALBERTA_FACTOR as dr, deferredIncomeTaxProvisionAddBack as dt, CEC_INCLUSION_RATE as en, at1YesNo as er, computeSchedule5 as et, computeEifelLimitation as f, computeClass13 as fn, computeAllocationFactor as fr, findSchedule1LineDefects as ft, computeT2Settlement as g, CCA_DECLINING_BALANCE_RATES_2024 as gn, schedule10Values as gr, terminalLossDeduction as gt, computeAdjustedTaxableIncome as h, leaseholdPeriods as hn, at1LineItemId as hr, recaptureAddBack as ht, computeMpDeduction as i, UnsupportedCcaClassError as in, allocateIegEvenly as ir, computePart4Rdtoh as it, runConformanceSuite as j, LossCarrybackError as jn, computeDayWeightedGeneralTax as jr, computePartITax as jt, formatConformanceReport as k, computeSchedule12 as kn, computeAlbertaSbd as kr, nonCapitalLossApplied as kt, EIFEL_STANDARD_RATIO as l, CLASS_14_1_TRANSITIONAL_RATE as ln, computeIegBaseAmount as lr, ccaDeduction as lt, FOREIGN_TAX_CREDIT_GROSS_UP as m, computeClass141AdditionalAllowance as mn, AT1_SCHEDULES_WITH_BUILDERS as mr, mealsAndEntertainmentAddBack as mt, MP_GROSS_REVENUE_THRESHOLD as n, computeAlbertaSchedule14 as nn, at1Engine as nr, PART_IV_RATE as nt, computePartVI1Deduction as o, computeCcaSchedule as on, computeIegGroupFigures as or, Schedule1NotFileableError as ot, ratioOfPermissibleExpenses as p, computeClass14 as pn, AT1_SCHEDULES_WITHOUT_BUILDERS as pr, incomeTaxProvisionAddBack as pt, computeZetm as q, At1MandatoryFieldMissingError as qn, AT1_RESERVE_KINDS as qt, MP_RATES_2024 as r, computeAlbertaSchedule13 as rn, computeAlbertaReturn as rr, REFUNDABLE_PART_I_RATE as rt, partVI1DeductionMultiple as s, computeSchedule8 as sn, IEG_2024 as sr, amortizationAddBack as st, MP_EXCLUDED_ACTIVITIES as t, cecScheduleAppliesToTaxYear as tn, xmlEscape as tr, computeSchedule4Losses as tt, EIFEL_STANDARD_RATIO_FROM as u, MAX_LEASEHOLD_PERIODS as un, computeIegExpenditureLimit as ur, computeSchedule1 as ut, t2Engine as v, isDecliningBalanceClass as vn, schedule12Values as vr, assessEifel as vt, QC_TAX_2024 as w, albertaCurrentYearLoss as wn, schedule20Values as wr, dayWeightedRate as wt, computeQuebecReturn as x, albertaCapitalGainDifference as xn, schedule17Values as xr, isSchedule5Province as xt, renderCo17DraftReturn as y, resolveCcaRates as yn, schedule13Values as yr, PROVINCE_RATES_2024 as yt, computeSchedule54 as z, formatRsiDate as zn, resolveRates as zr, computeLossContinuity as zt };
6730
+ export { computeProvincialAllocation as $, at1TaxPayableDeductions as $n, CEC_DEDUCTION_RATE as $t, runConformance as A, reconcileAlbertaNetIncome as An, schedule29Values as Ar, computeCcpcActiveBusinessTax as At, computeGrip as B, formatRsiText as Bn, earliestRateYear as Br, AT1_DONATION_GAIN_RATE as Bt, computeQuebecTax as C, albertaCcaScheduleAdjustments as Cn, schedule13Values as Cr, blendProvinceRateTable as Ct, T2_LINE_META as D, albertaReserveDifference as Dn, schedule1Values as Dr, dividendsDeductibleS112 as Dt, resolveQuebecTaxRates as E, albertaRecaptureDifference as En, schedule18Values as Er, computeTaxableIncome as Et, normalizeSchedule88 as F, RSI_NEGATIVE_PREFIX as Fn, AB_GENERAL_RATE_BANDS as Fr, CORP_TAX_RATE_BOOK as Ft, ITC_RECAPTURE_PERIOD_YEARS as G, AT1_CRITICAL_MANDATORY_FIELDS as Gn, SECTION_34_2_GROSS_UP as Gt, LARGE_CORPORATION_THRESHOLD as H, renderRsiHeader as Hn, hasExactRateYear as Hr, computeDonationMaximum as Ht, computeSchedule55 as I, RSI_WORD_GAP as In, computeDayWeightedGeneralTax as Ir, resolveCorpTaxRates as It, allocateEvenly as J, At1TaxPayableMismatchError as Jn, AT1_RESERVE_LINES as Jt, computeItcRecapture as K, At1CriticalFieldMissingError as Kn, computeAlbertaSchedule18 as Kt, LRIP_INVESTMENT_CORPORATION_MULTIPLE as L, RsiLineItemError as Ln, AB_TAX_2024 as Lr, T2_CERTIFICATION_FIXTURES as Lt, computeFederalT2 as M, computeLossCarryback as Mn, schedule4970Values as Mr, computeBusinessLimit as Mt, computeSchedule101 as N, RSI_COLUMN_GAP as Nn, computeAlbertaTax as Nr, computeSBD as Nt, foldT2Lines as O, albertaTerminalLossDifference as On, schedule20Values as Or, netCapitalLossApplied as Ot, SCHEDULE_88_MAX_URLS as P, RSI_DELIMITER as Pn, computeAlbertaSbd as Pr, CORP_TAX_2024 as Pt, computeSchedule6 as Q, assertCriticalFields as Qn, computeAlbertaSchedule16 as Qt, LRIP_INVESTMENT_INCOME_FACTOR as R, formatRsiAmount as Rn, AB_TAX_RATE_BOOK as Rr, computeLossSchedule as Rt, computeQuebecAllocationFactor as S, albertaCcaDifference as Sn, schedule12Values as Sr, resolveProvinceRates as St, QC_TAX_RATE_BOOK as T, albertaDispositionAdjustments as Tn, schedule17Values as Tr, charitableDonationsDeduction as Tt, computeTaxableCapital as U, renderRsiLineItem as Un, latestRateYear as Ur, computeSchedule20 as Ut, computeSchedule43 as V, renderAt1Rsi as Vn, extendRateBook as Vr, AT1_DONATION_INCOME_RATE as Vt, computeSchedule31 as W, renderAt1NetFile as Wn, resolveRates as Wr, AT1_DISPOSITION_CATEGORIES as Wt, computeSchedule21 as X, assertAt1MandatoryComplete as Xn, computeAlbertaSchedule17 as Xt, computeBusinessLimitAllocation as Y, albertaBalanceUnpaid as Yn, AT1_RESERVE_TOTAL_LINES as Yt, computeSchedule13 as Z, assertAt1TaxPayableReconciles as Zn, assistanceFrom as Zt, renderT2DraftReturn as _, CCA_RATE_BOOK as _n, AT1_SCHEDULES_WITHOUT_BUILDERS as _r, EIFEL_EFFECTIVE_FROM as _t, PART_VI_1_DEDUCTION_BANDS as a, computeCcaClass as an, allocateIegExpenditureLimit as ar, computeSchedule2 as at, co17Engine as b, albertaAbilDifference as bn, schedule10Values as br, PROVINCE_RATE_BOOK as bt, EIFEL_FIRST_YEAR_START as c, CLASS_14_1_MINIMUM_DEDUCTION as cn, IEG_2024 as cr, assertSchedule1Fileable as ct, EIFEL_TRANSITIONAL_RATIO as d, MIN_LEASEHOLD_PERIODS as dn, computeIegReductionFactor as dr, deferredIncomeTaxProvisionAddBack as dt, CEC_INCLUSION_RATE as en, at1YesNo as er, computeSchedule5 as et, computeEifelLimitation as f, computeClass13 as fn, computeIegEligibleExpenditures as fr, findSchedule1LineDefects as ft, computeT2Settlement as g, CCA_DECLINING_BALANCE_RATES_2024 as gn, computeAt4970 as gr, terminalLossDeduction as gt, computeAdjustedTaxableIncome as h, leaseholdPeriods as hn, computeAllocationFactor as hr, recaptureAddBack as ht, computeMpDeduction as i, UnsupportedCcaClassError as in, allocateIegEvenly as ir, computePart4Rdtoh as it, runConformanceSuite as j, LossCarrybackError as jn, schedule2Values as jr, computePartITax as jt, formatConformanceReport as k, computeSchedule12 as kn, schedule21Values as kr, nonCapitalLossApplied as kt, EIFEL_STANDARD_RATIO as l, CLASS_14_1_TRANSITIONAL_RATE as ln, computeIeg as lr, ccaDeduction as lt, FOREIGN_TAX_CREDIT_GROSS_UP as m, computeClass141AdditionalAllowance as mn, SINGLE_JURISDICTION_ALBERTA_FACTOR as mr, mealsAndEntertainmentAddBack as mt, MP_GROSS_REVENUE_THRESHOLD as n, computeAlbertaSchedule14 as nn, at1Engine as nr, PART_IV_RATE as nt, computePartVI1Deduction as o, computeCcaSchedule as on, computeIegAgreement as or, Schedule1NotFileableError as ot, ratioOfPermissibleExpenses as p, computeClass14 as pn, iegT661SourceLine as pr, incomeTaxProvisionAddBack as pt, computeZetm as q, At1MandatoryFieldMissingError as qn, AT1_RESERVE_KINDS as qt, MP_RATES_2024 as r, computeAlbertaSchedule13 as rn, computeAlbertaReturn as rr, REFUNDABLE_PART_I_RATE as rt, partVI1DeductionMultiple as s, computeSchedule8 as sn, computeIegGroupFigures as sr, amortizationAddBack as st, MP_EXCLUDED_ACTIVITIES as t, cecScheduleAppliesToTaxYear as tn, xmlEscape as tr, computeSchedule4Losses as tt, EIFEL_STANDARD_RATIO_FROM as u, MAX_LEASEHOLD_PERIODS as un, computeIegBaseAmount as ur, computeSchedule1 as ut, t2Engine as v, isDecliningBalanceClass as vn, AT1_SCHEDULES_WITH_BUILDERS as vr, assessEifel as vt, QC_TAX_2024 as w, albertaCurrentYearLoss as wn, schedule16Values as wr, dayWeightedRate as wt, computeQuebecReturn as x, albertaCapitalGainDifference as xn, schedule12LossDeductions as xr, isSchedule5Province as xt, renderCo17DraftReturn as y, resolveCcaRates as yn, at1LineItemId as yr, PROVINCE_RATES_2024 as yt, computeSchedule54 as z, formatRsiDate as zn, resolveAlbertaTaxRates as zr, computeLossContinuity as zt };