toga-ai 1.0.796 → 1.0.798

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.
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-04
10
- owners: ["bala", "kyalamarthi"]
9
+ updated: 2026-09-11
10
+ owners: ["bala", "kyalamarthi", "rgirish"]
11
11
  files:
12
12
  - worker/ebs/cron.worker.php
13
13
  - worker/schedules/cron.worker.sync.json
@@ -23,6 +23,7 @@ related:
23
23
  - ../../library/features/cron-execution-monitoring.md
24
24
  - ../../library/features/email-test-mode-and-write-free-sends.md
25
25
  - ../../../1.0/standards/backend-php.md
26
+ - ../../../clients/prudential/features/itemclasses-device-tier-data.md
26
27
  ---
27
28
 
28
29
  ## Summary
@@ -47,6 +48,27 @@ frequency makes you read a normal gap as an outage.
47
48
  Also check whether the job is registered in the env you are testing in at all: this one is **not**
48
49
  in `cron.beta.json`, so beta never runs it on a schedule. Nothing on beta is evidence about it.
49
50
 
51
+ ### Is the job switched OFF? — read the `active` flag carefully
52
+
53
+ Each schedule entry is `{active, name, schedule, cron}`, and `ebs/cron.worker.php:39` gates it:
54
+
55
+ ```php
56
+ if (!property_exists($cron, 'active') || $cron->active) {
57
+ ```
58
+
59
+ - **`"active": 0` genuinely stops the job** — it is never written into the crontab, so it leaves
60
+ no `CronJobExecutions` row and no trace anywhere. Indistinguishable from "never fired" unless
61
+ you read the JSON.
62
+ - **⚠ A MISSING `active` property means the job RUNS.** The default is on, not off. Do not read
63
+ the absence of `active` as "disabled".
64
+
65
+ When a job is off, `git log` the schedule file to find out who turned it off and why — the commit
66
+ message usually carries the reason. Worked example (2026-09-11): "Transmissions to Netsuite"
67
+ (`cron.worker.sync.json:441`) is `"active": 0`, disabled 2026-07-17 in commit `14570def`
68
+ ("Add careOf & skip Apple; disable Netsuite cron") on `_production`. That single flag is what made
69
+ Prudential's `ItemClasses` data dead — see
70
+ [Prudential ItemClasses device-tier data](../../../clients/prudential/features/itemclasses-device-tier-data.md).
71
+
50
72
  ### Which of the 7 boxes runs it — and in which timezone
51
73
 
52
74
  Two facts you need before you go looking for evidence, both easy to get wrong:
@@ -188,6 +210,11 @@ real script **never executes**, on every tick, with no error anywhere. It looks
188
210
  contain anything; only the `cron` path is shell-parsed.
189
211
 
190
212
  ## Change history
213
+ - 2026-09-11 — Added **"is the job switched OFF?"** to Step 1: `ebs/cron.worker.php:39` gates on
214
+ `!property_exists($cron, 'active') || $cron->active`, so `"active": 0` genuinely stops the job
215
+ and leaves no trace, but a **missing `active` property means it RUNS** (default is on). Also:
216
+ `git log` the schedule file to find who disabled it and why. Found while proving Prudential's
217
+ `ItemClasses` data was dead. (rgirish)
191
218
  - 2026-09-04 — Added the **`&`-in-a-cron-filename trap**: `ebs/cron.worker.php:41` concatenates the
192
219
  path into the crontab line unquoted, so a shell metacharacter splits the command and the script
193
220
  never runs while leaving no trace. Found via the renamed Compass Canada
@@ -27,6 +27,7 @@
27
27
  | [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` is the 2.0 field type for blob-backed columns (S3 or local folder). |
28
28
  | [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). |
29
29
  | [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. |
30
+ | [ItemClasses / _Model_Client_ItemClass — who actually uses it (and why it is not free)](features/item-classification-itemclasses.md) | `ItemClasses` is an empty-looking, almost-unused lookup table that ships in **every** 2.0 client database. |
30
31
  | [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m |
31
32
  | [DB-free unit testing for _underscore model interceptors](features/model-interceptor-unit-testing.md) | `_underscore` shipped with **no** PHPUnit setup (no `composer.json`/`phpunit`; only vendored PhpOffice tests existed). |
32
33
  | [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. |
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-14
10
- owners: ["jcardinal", "bala"]
9
+ updated: 2026-09-10
10
+ owners: ["jcardinal", "bala", "rgirish"]
11
11
  files:
12
12
  - _underscore/Cloud.php
13
13
  related:
@@ -59,6 +59,13 @@ None — shared core helper.
59
59
  do not add your own `ContinuationToken` loop on top.
60
60
  - A bucket outside the config-default us-east-1 (e.g. `agilant-as2` in us-west-2) throws
61
61
  `AuthorizationHeaderMalformed` unless you pass `$awsRegion`.
62
+ - **⚠ `copyFileToS3` is a single `putObject` — it CANNOT upload a file larger than 5 GB.**
63
+ Verified 2026-09-10 at `_underscore/Cloud.php:49`: the method issues one `putObject` call, and
64
+ S3 caps a single PUT at **5 GB**. There is **no `MultipartUploader` anywhere in `_underscore`
65
+ or `worker2`**. Anything that could produce a large artifact — a database dump above all — will
66
+ fail on this helper once it crosses the limit. If you need large-object upload, add
67
+ `MultipartUploader` explicitly; do not assume this helper scales.
68
+
62
69
  - **`getFileFromS3` (the GET method) is single-region.** It builds the `S3Client` with region
63
70
  `_Config::cloud('aws_region')` (the worker's own region) and only swallows HTTP 404. Reading
64
71
  an object from a bucket in a **different** region raises a `301 PermanentRedirect`, which it
@@ -67,6 +74,10 @@ None — shared core helper.
67
74
  has no region-override parameter.)
68
75
 
69
76
  ## Change history
77
+ - 2026-09-10 — Recorded that **`copyFileToS3` (`Cloud.php:49`) is a single `putObject`**, so it
78
+ cannot upload an object larger than S3's 5 GB single-PUT cap, and that **no `MultipartUploader`
79
+ exists anywhere in `_underscore` or `worker2`**. Matters for any large-artifact upload, database
80
+ dumps first. (rgirish)
70
81
  - 2026-07-15 — Documented that `getFileFromS3` is single-region (client built with the worker's
71
82
  own `aws_region`, only swallows 404, re-throws `301 PermanentRedirect` for cross-region reads);
72
83
  use a direct HTTPS download for cross-region/public objects. (bala)
@@ -0,0 +1,127 @@
1
+ ---
2
+ title: "ItemClasses / _Model_Client_ItemClass — who actually uses it (and why it is not free)"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-11
10
+ owners: ["rgirish"]
11
+ files:
12
+ - _underscore/Model/Client/ItemClass.php
13
+ - _underscore/Model/Client/Item.php
14
+ - worker/crons/toga2/prudential/transmissions_to_netsuite.php
15
+ - toga2-supply/src/pages/Inventory/api/InventoryApi.tsx
16
+ related:
17
+ - ./netsuite-rest-client.md
18
+ - ./record-change-audit-log.md
19
+ - ../../../../clients/prudential/features/itemclasses-device-tier-data.md
20
+ - ../../dbchanges2/workflows/client-schema-drift-audit.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `ItemClasses` is an empty-looking, almost-unused lookup table that ships in **every** 2.0 client
26
+ database. It looks like free real estate for a new item-classification feature. It is not:
27
+ **2 of 36 production clients already have rows in it, and they use it for two different things,
28
+ neither of which is NetSuite's `class` field.**
29
+
30
+ Read this before you plan to reuse, extend, or repurpose `ItemClasses` for any client.
31
+
32
+ Audit done 2026-09-11 across all TOGA codebases and all 36 production client schemas.
33
+
34
+ ## Key files / entry points
35
+
36
+ - **`_underscore/Model/Client/ItemClass.php`** — `_Model_Client_ItemClass`,
37
+ `DATABASE = _underscore::DB_CLIENT`, `TABLE = 'ItemClasses'`. Four fields only:
38
+ `id`, `uuid`, `code`, `name`.
39
+ - **`_underscore/Model/Client/Item.php:23`** — the only foreign key:
40
+ `public $itemClassId = [self::FIELD_FOREIGNKEY, ... => '\_Model_Client_ItemClass'];`
41
+ - **`worker/crons/toga2/prudential/transmissions_to_netsuite.php:171-178`** — the **only** code
42
+ anywhere that reads it (see below).
43
+
44
+ ## How it works
45
+
46
+ ### The table ships blank to everyone
47
+
48
+ `ItemClasses` is part of the blank client DB, so every 2.0 tenant has the table. Having the
49
+ table says nothing about whether the client uses it. **Check row counts, not schema.**
50
+
51
+ ### Who has data (prod, verified 2026-09-11)
52
+
53
+ | Client | Rows in `ItemClasses` | Items tagged | What the rows actually mean |
54
+ |---|---|---|---|
55
+ | **Northwell** | 27 | 245 of 247 | ServiceNow **service categories** — "Print", "Computer", "Service Desk" |
56
+ | **Prudential** | 7 | 9 of 765 | Dell/Apple **device tiers** — "LATITUDE 73XX", "MACBOOK PRO" |
57
+ | all other 34 clients | 0 | 0 | unused |
58
+
59
+ Two clients, two unrelated meanings. Neither is an "item class" in the NetSuite sense.
60
+
61
+ **Canon is a false positive.** `dbchanges2/Client_Canon/2024-01-01.sql` contains `ItemClasses`
62
+ schema changes, but production `Client_Canon` has **0 rows**. Schema history is not usage.
63
+
64
+ ### There is exactly one reader in the whole platform
65
+
66
+ `worker/crons/toga2/prudential/transmissions_to_netsuite.php:171-178` pulls
67
+ `ItemClasses.name` as `deviceTier` for the line-1 item of a sales order and sends it to NetSuite:
68
+
69
+ ```sql
70
+ SELECT ItemClasses.name
71
+ FROM SalesOrderItems
72
+ INNER JOIN Items ON Items.id = SalesOrderItems.itemId
73
+ INNER JOIN ItemClasses ON ItemClasses.id = Items.itemClassId
74
+ WHERE SalesOrderItems.salesOrderId = SalesOrders.id AND SalesOrderItems.lineNumber = 1
75
+ LIMIT 1
76
+ ```
77
+
78
+ That cron is **disabled** — see the
79
+ [Prudential ItemClasses data doc](../../../../clients/prudential/features/itemclasses-device-tier-data.md).
80
+
81
+ **Nothing writes `itemClassId`.** No writer exists in `api2`, `worker2`, or `_underscore`.
82
+
83
+ ### Frontend: one real reference, and a grep trap
84
+
85
+ `grep itemClass` in the frontend is misleading — most hits in `toga2-supply` are CSS
86
+ variables like `menuItemClassNames`, not this table. The one genuine reference is
87
+ `toga2-supply/src/pages/Inventory/api/InventoryApi.tsx:815`, requesting the column
88
+ `item.itemClass.name`.
89
+
90
+ ## NetSuite `classification` — the shape you would have to map from
91
+
92
+ Queried live via SuiteQL (2026-09-11): **311 classifications**, columns
93
+ `id`, `name`, `fullname`, `isinactive`, `parent`.
94
+
95
+ - **It is a tree, not a flat list.** Each row has a `parent` and a colon-separated `fullname`,
96
+ e.g. name `Laptop` / fullname `Technology Sales : Hardware : End User Devices : Laptop` /
97
+ parent `405`.
98
+ - **⚠ `name` is NOT globally unique** — only unique within its parent. Repeats seen:
99
+ "Managed Services" 3x (ids 75, 395, 379), "Software" 2x (ids 3, 419), "AI & Data Sciences" 2x,
100
+ "Data Services" 2x.
101
+
102
+ ## Gotchas
103
+
104
+ - **⚠ `ItemClasses.name` has a UNIQUE index (index literally named `name`).** NetSuite
105
+ classification names repeat, so **any attempt to load NetSuite classes into this table fails
106
+ until that UNIQUE is dropped.** This is the first blocker on any NetSuite-classification work.
107
+ - **A hierarchy path column must be `TEXT`, not `varchar(512)`.** NetSuite `fullname` values are
108
+ long, and the 2.0 standard caps varchar at 255.
109
+ - **The table is already `utf8mb4_0900_ai_ci`**, so new columns need no explicit `COLLATE`.
110
+ - **`c_` custom fields are NOT declared as properties on the 2.0 Model class.** The framework
111
+ reads them from `CustomRecordFields` (`ItemClasses` is `Core.Records` id **105**). Confirmed:
112
+ no `public $c_netsuite*` property exists on any `Model/Client` class. Copy the pattern in
113
+ `dbchanges2/_modules/netsuite/2026-08-18 - TransferOrdersNetsuiteStatus.sql`.
114
+ - **Check whether the client has an `ItemClassCustomFields` table before adding `c_` columns.**
115
+ `Client_Prudential` does not, so its `c_` columns would go directly on `ItemClasses`.
116
+ - **"Every client has the table" ≠ "the table is free."** Repurposing `ItemClasses` org-wide
117
+ would break Northwell's 245 tagged items, which hold ServiceNow service categories.
118
+
119
+ ## Change history
120
+ - 2026-09-11 — Documented from a full usage audit of `ItemClasses` across every TOGA codebase and
121
+ all 36 prod client schemas: the table ships blank to all clients but **only Northwell (27 rows,
122
+ ServiceNow service categories) and Prudential (7 rows, Dell device tiers) have data**, for two
123
+ different meanings; there is exactly **one reader** (`transmissions_to_netsuite.php:171-178`,
124
+ currently disabled) and **zero writers**; Canon has schema but no rows. Recorded the live
125
+ NetSuite `classification` shape (311 rows, a tree with repeating `name`) and the resulting
126
+ blocker: `ItemClasses.name` is UNIQUE and must be dropped before NetSuite classes can be
127
+ mapped in. (rgirish)
@@ -6,8 +6,8 @@ project: AI-BDR
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-26
10
- owners: [tcox]
9
+ updated: 2026-09-10
10
+ owners: [tcox, rgirish]
11
11
  files:
12
12
  - bdr/PLAN.md
13
13
  - bdr/next.config.ts
@@ -32,6 +32,9 @@ files:
32
32
  - bdr/test/fastDial.test.ts
33
33
  - bdr/eslint.config.mjs
34
34
  - bdr/amplify.yml
35
+ - bdr/package.json
36
+ - bdr/.npmrc
37
+ - bdr/src/components/TalosDrawer.tsx
35
38
  - bdr/src/app
36
39
  - bdr/src/app/layout.tsx
37
40
  - bdr/src/app/page.tsx
@@ -619,6 +622,28 @@ would be cached by browsers and crawlers and become painful to undo.
619
622
 
620
623
  ## Gotchas
621
624
 
625
+ - **⚠ An `import` added without the matching `package.json` dependency only fails in Amplify —
626
+ never locally.** Root cause of the 2026-09-10 build break: PR #5 (`blox-talos-bdr-update`,
627
+ TRUE-81447) added
628
+ `import { TalosPanel } from "@agilant/toga-blox/dist/components/Talos/index.js"` to
629
+ `src/components/TalosDrawer.tsx` but **never added `@agilant/toga-blox` to `package.json`**.
630
+ The author's `node_modules` already had it (linked or left over from an earlier install), so
631
+ local `next build` was green; Amplify installs from the lockfile into a clean tree and failed
632
+ with `Module not found`. **Before merging any PR that adds a new bare import, grep the new
633
+ import specifiers against `package.json`** — a green local build proves nothing about a clean
634
+ install. The `local-link` workflow states the same trap from the blox side: "a link masks
635
+ missing dependency + peer declarations."
636
+ - **BDR's blox dependency set (as fixed 2026-09-10):** `@agilant/toga-blox@1.1.2-production.144`
637
+ (pinned exactly, from the `production` dist-tag), plus
638
+ `@fortawesome/fontawesome-svg-core@^7.3.1` and `@fortawesome/react-fontawesome@^3.5.0`, plus a
639
+ new `bdr/.npmrc`. Verified: clean install exit 0, `next build` compiled, **385/385 tests pass**.
640
+ The `.npmrc` needs the **FontAwesome Pro** registry line and `legacy-peer-deps=true` — blox is a
641
+ **public** package needing no token, and blox peers still cap React at `^18` while BDR runs
642
+ React 19. Full detail:
643
+ [dynamic-publish-pipeline.md](../../toga-blox/workflows/dynamic-publish-pipeline.md#consuming-blox-which-registry-actually-needs-a-token).
644
+ **Never commit the token value** — a live one was pushed in `bdr` commit `88809c3`; treat it as
645
+ compromised and rotate.
646
+
622
647
  - **"The funnel is gone / a CRM link doesn't prefill" — check the redirect FIRST.** Since
623
648
  2026-08-24 `/` 307s to `/landing`, so every symptom that used to mean "missing env" or
624
649
  "rate-limit soft-degrade" can now simply mean the request never reached `page.tsx`. Probe
@@ -754,6 +779,14 @@ would be cached by browsers and crawlers and become painful to undo.
754
779
  harness; pull it with `npx toga-ai`.
755
780
 
756
781
  ## Change history
782
+ - 2026-09-10 — **Fixed the Amplify `Module not found` build break** and recorded its durable
783
+ lesson: PR #5 added a `@agilant/toga-blox` import to `TalosDrawer.tsx` without adding the
784
+ package to `package.json`, so it built locally (stale `node_modules`) and failed only on
785
+ Amplify's clean install. Added `@agilant/toga-blox@1.1.2-production.144` +
786
+ `@fortawesome/fontawesome-svg-core@^7.3.1` + `@fortawesome/react-fontawesome@^3.5.0` and a
787
+ `bdr/.npmrc` (FontAwesome Pro registry + `legacy-peer-deps=true` for the React 19 host; blox
788
+ itself is public and needs no token). Verified clean install, compiled build, 385/385 tests.
789
+ (rgirish)
757
790
  - 2026-08-26 — Env-var accounting only: recorded an **optional 8th deploy var**,
758
791
  `TOGA_SECURITY_CAMPAIGN_UUID`, declared in `.env.example` and deliberately **left unset**.
759
792
  It gates `/landing`'s new real callback route (`POST /api/landing-call`), which 503s and
@@ -6,12 +6,14 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-02
10
- owners: [jcardinal, apeterson, tcox]
9
+ updated: 2026-09-10
10
+ owners: [jcardinal, apeterson, tcox, rgirish]
11
11
  files:
12
12
  - toga-blox/.github/workflows/publish.yml
13
13
  - toga-blox/package.json
14
14
  - toga-blox/src/utils/getFontAwesomeIcon.tsx
15
+ - bdr/.npmrc
16
+ - toga25-supply/.npmrc
15
17
  related:
16
18
  - ../architecture.md
17
19
  - ./landing-a-large-feature-branch.md
@@ -83,6 +85,50 @@ blox channel. A channel exists **only** when this repo (`toga-blox-npm`) publish
83
85
  This publish-before-build ordering is the ongoing cost of the strict 1:1 model: every
84
86
  environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
85
87
 
88
+ ## Consuming blox: which registry actually needs a token
89
+
90
+ **`@agilant/toga-blox` is a PUBLIC npm package. It needs NO auth.** Verified 2026-09-10 by an
91
+ unauthenticated request:
92
+
93
+ ```
94
+ curl -s -o /dev/null -w '%{http_code}' https://registry.npmjs.org/@agilant%2Ftoga-blox
95
+ # 200
96
+ ```
97
+
98
+ The team had been copying an `.npmrc` carrying **two** tokens into each consumer, believing the
99
+ `@agilant` scope was private. It is not. Only **one** credential is real:
100
+
101
+ | Registry | Auth needed? | Why |
102
+ |---|---|---|
103
+ | `registry.npmjs.org` (`@agilant/toga-blox`) | **No** | Public package, HTTP 200 unauthenticated |
104
+ | `npm.fontawesome.com` (`@fortawesome/pro-*`) | **Yes** | Paid/private packages |
105
+
106
+ **FontAwesome Pro is the only thing the token is for.** blox's `src/utils/getFontAwesomeIcon.tsx`
107
+ hard-imports `@fortawesome/pro-solid-svg-icons`, `pro-regular-svg-icons` and
108
+ `pro-light-svg-icons`, so any consumer of blox pulls the Pro registry transitively. Proven by
109
+ isolating `HOME` to an empty dir and installing: the failure is **E401 on `npm.fontawesome.com`
110
+ tarballs only** — never on the `@agilant` scope.
111
+
112
+ Practical consequence: a consumer's `.npmrc` needs the FontAwesome registry line + token and
113
+ `legacy-peer-deps=true`. It does **not** need an `@agilant:registry` line or an npmjs token.
114
+
115
+ ### `legacy-peer-deps=true` is required on a React 19 host
116
+
117
+ blox `peerDependencies` still declare React `^18`. A React 19 consumer (e.g. the `bdr` Next.js
118
+ app) cannot resolve the install without `legacy-peer-deps=true` in `.npmrc`. The peer range is a
119
+ declaration, not a runtime wall — blox runs fine on React 19 (see
120
+ [local-link-into-a-consumer-app.md](./local-link-into-a-consumer-app.md) §2).
121
+
122
+ ### ⚠ Never commit a token VALUE in `.npmrc`
123
+
124
+ Live FontAwesome tokens have been found **committed** in `toga25-supply/.npmrc` and
125
+ `info/.npmrc`, and one was pushed in `bdr` commit `88809c3`. Treat every token found this way as
126
+ compromised and rotate it. The correct shape is an env-var placeholder resolved at build time —
127
+ which is exactly what blox's own `publish.yml` already does (see the "Secret hygiene done right"
128
+ gotcha below). This is the same violation already recorded in
129
+ [`../../../standards/frontend.md`](../../../standards/frontend.md); what is **new here is that
130
+ only the FontAwesome token was ever needed** — the npmjs token in those files bought nothing.
131
+
86
132
  ## Gotchas
87
133
 
88
134
  - **⚠ There is a STALE UNDERSCORED dist-tag on the registry — `_sandbox-client` (with the
@@ -212,6 +258,15 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
212
258
  you push to a release branch.
213
259
 
214
260
  ## Change history
261
+ - 2026-09-10 — **Corrected a long-standing team assumption: `@agilant/toga-blox` is a PUBLIC npm
262
+ package and needs no auth** (verified by unauthenticated `curl` → HTTP 200). The only registry
263
+ that requires a token is **FontAwesome Pro** (`npm.fontawesome.com`), pulled in transitively
264
+ because `getFontAwesomeIcon.tsx` hard-imports the `@fortawesome/pro-*` packages — proven by
265
+ isolating `HOME` and observing E401 on FontAwesome tarballs only. Consumers had been copying an
266
+ `.npmrc` with both tokens; the npmjs one was never needed. Also recorded that
267
+ `legacy-peer-deps=true` is required on a React 19 host (blox peers still cap at `^18`), and that
268
+ live tokens were found committed in `toga25-supply/.npmrc`, `info/.npmrc` and pushed in `bdr`
269
+ `88809c3` — rotate and replace with a build-time env-var placeholder. (rgirish)
215
270
  - 2026-09-02 — Released `1.1.2-sandbox-client.142` (base `1.1.0` → `1.1.1` on `_sandbox-client`,
216
271
  commit `543a7186`) and rolled it into `toga25-supply` (pin `1.1.1-sandbox-client.140` →
217
272
  `1.1.2-sandbox-client.142`, clean `npm install`, `tsc --noEmit` zero errors). The documented
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
10
- owners: [rgirish]
9
+ updated: 2026-09-10
10
+ owners: [rgirish, jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Clickup/Fluffer.php
13
13
  - worker2/Worker/Clickup.php
@@ -121,6 +121,14 @@ never by value.
121
121
 
122
122
  ## Gotchas / known issues
123
123
 
124
+ - **⚠ OPEN (found 2026-09-10, NOT fixed): the Talos agent call is failing in prod with HTTP 403.**
125
+ `Clickup/Fluffer/Process` failed **168 times over 7 days** with `Talos agent returned HTTP 403` — an
126
+ auth/permission failure on the `POST /api/ai/agent` call (`callTalosAgent()`,
127
+ `worker2/Worker/Clickup/Fluffer.php` ~L230, from `Process()` ~L94). 403 points at a bad/expired
128
+ `[talos] agent_api_key` (the `dev-core`-scoped key sent as `X-API-Key`) or the agent bridge revoking
129
+ it — **not** a ClickUp problem. Left unfixed by the developer for now; recorded so the next triage
130
+ starts from "check the `agent_api_key` / agent-bridge auth", not from the code. Surfaced by the
131
+ `Core.WorkerJobs` action+outcome census (group by action, isSuccess over 7 days).
124
132
  - **Watchdog race.** The fluffer is an **ACTION** job; its watchdog is the Lambda env
125
133
  `MAX_EXECUTION_TIME_WORKER_ACTION` (default 300s). The Talos timeout is set to **280** (just
126
134
  under 300) so the job doesn't lose the race against its own watchdog out of the box.
@@ -144,6 +152,11 @@ never by value.
144
152
 
145
153
  ## Change history
146
154
 
155
+ - 2026-09-10 — **Discovered (NOT fixed): the fluffer is down in prod on a Talos-agent HTTP 403.**
156
+ `Clickup/Fluffer/Process` failed 168x over 7 days with `Talos agent returned HTTP 403` — an
157
+ auth/permission failure on the `/api/ai/agent` call, most likely an expired/wrong `[talos]
158
+ agent_api_key`. Deferred by the developer; logged as an open known issue for the next triage. Found
159
+ via the `Core.WorkerJobs` action+outcome census (toga-db MCP, no ClickUp/Talos access). (jcardinal)
147
160
  - 2026-07-20 — Built the ClickUp task description fluffer: new `_Worker_Clickup_Fluffer`
148
161
  (action `Clickup/Fluffer/Process`, `taskCreated` webhook auto-trigger, `force` regenerate)
149
162
  calling the Talos `dev-core` KB-investigation agent at `/api/ai/agent` via `_ApiRequest`
@@ -6,13 +6,15 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-02
10
- owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean]
9
+ updated: 2026-09-10
10
+ owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
11
11
  files:
12
12
  - worker2/Worker/
13
13
  - worker2/Controller/Index.php
14
14
  - worker2/LambdaFunctions/JobScheduler/lambda_function.py
15
15
  - _underscore/Worker.php
16
+ - worker2/Worker/Infrastructure/Database.php
17
+ - worker2/.platform/hooks/prebuild/020-install-mysql-client.sh
16
18
  related:
17
19
  - ../architecture.md
18
20
  - ./netsuite-opportunity-client-labels.md
@@ -190,6 +192,40 @@ The EB worker dispatcher (`Controller/Index.php::worker()`) wraps your action ca
190
192
  > It now captures **both** success results and failure text. See
191
193
  > [architecture.md](../architecture.md) for the multi-producer rename gotcha.
192
194
 
195
+ ### ⚠ A `null` return is logged as SUCCESS — "it ran green" is not evidence it did anything
196
+
197
+ `Controller/Index.php:378` records `isSuccess = 1` for **any** normal return, including `null`.
198
+ The framework has no notion of "the action did no work": an action that silently does nothing —
199
+ or is an unfinished stub — produces a clean `WorkerJobs` row indistinguishable from a real run.
200
+
201
+ Live example: **`_Worker_Infrastructure_Database::Backup()`
202
+ (`worker2/Worker/Infrastructure/Database.php:5-15`) is a stub.** It sends a test email to a
203
+ hardcoded address and returns `null`. Every invocation is logged as a **successful database
204
+ backup**. Nothing is backed up.
205
+
206
+ Two rules follow:
207
+
208
+ 1. **When you write an action, return something that proves the work happened** — a count, an
209
+ id list, a summary string. `return null;` on a work-performing action throws away the only
210
+ signal the dispatcher records.
211
+ 2. **When you audit an action, never accept a green `WorkerJobs` row as proof.** Read the
212
+ `output` column, and read the method. This applies to anything safety-critical (backups,
213
+ reconciliations, syncs) where "it's been green for months" is the expected — and worthless —
214
+ observation.
215
+
216
+ Two related defects found in the same audit, both worth knowing before you build on this
217
+ infrastructure:
218
+
219
+ - **`_underscore/Cloud.php:49` `copyFileToS3` is a single `putObject`**, so it cannot upload
220
+ anything over S3's **5 GB** single-PUT cap, and there is no `MultipartUploader` anywhere in
221
+ `_underscore` or `worker2` — see
222
+ [_Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md).
223
+ - **`worker2/.platform/hooks/prebuild/020-install-mysql-client.sh` installs the WRONG client for
224
+ Aurora MySQL 3.** It installs **`mariadb105`**, whose `mysqldump` has no `--set-gtid-purged`
225
+ flag and emits MariaDB-versioned `/*!100xxx*/` conditional comments — comments that MySQL
226
+ silently **ignores on reload**, so parts of the dump vanish without any error. Any dump-based
227
+ work on this host needs the **MySQL** client (`mysql-community-client`), not the MariaDB one.
228
+
193
229
  Crucially, the worker **always returns HTTP 200, even on a throw**, so SQS deletes the message
194
230
  immediately. **There is no DLQ and no automatic retry.** A throw makes the failure *visible* (a
195
231
  failed `WorkerJobs` row to alert on) — it does **not** re-run the job.
@@ -464,6 +500,18 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
464
500
  commit-before-SQS transaction pattern that the worker relies on.
465
501
 
466
502
  ## Change history
503
+ - 2026-09-10 — Recorded that **a `null` return is stamped `isSuccess = 1`**
504
+ (`Controller/Index.php:378`), so an action that does nothing is logged as a successful run —
505
+ with the live proof that `_Worker_Infrastructure_Database::Backup()`
506
+ (`Worker/Infrastructure/Database.php:5-15`) is a stub that emails a hardcoded address, returns
507
+ `null`, and has therefore been reporting successful database backups while backing up nothing.
508
+ Added the two rules: return a proof-of-work value from every action, and never treat a green
509
+ `WorkerJobs` row as evidence for a safety-critical job. Also recorded two neighbouring defects
510
+ found in the same audit: `_Cloud::copyFileToS3` is a single `putObject` (5 GB ceiling, no
511
+ `MultipartUploader` in the codebase), and the EB prebuild hook
512
+ `020-install-mysql-client.sh` installs **`mariadb105`**, whose `mysqldump` lacks
513
+ `--set-gtid-purged` and writes `/*!100xxx*/` comments that MySQL silently drops on reload —
514
+ the wrong client for Aurora MySQL 3. (rgirish)
467
515
  - 2026-09-02 — Added the corollary that an action must **never sleep-wait for a third-party API
468
516
  rate budget** (it burns the 300s cap and the failure is unrecoverable — no retry, no DLQ); bulk
469
517
  work yields and re-enqueues, event-driven work proceeds over budget counted and alarmed. (ajean)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-04
10
- owners: ["dfranks", "kyalamarthi", "ajean"]
9
+ updated: 2026-09-10
10
+ owners: ["dfranks", "kyalamarthi", "ajean", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite.php
13
13
  - worker2/Worker/Netsuite/Opportunity.php
@@ -387,6 +387,22 @@ the legacy cron's OPPORTUNITIES section:
387
387
  (`hasEndCustomerColumn()`), since the migration is applied by hand and an unknown column would fail
388
388
  the whole query and break every opportunity webhook. See
389
389
  [the labels doc](./netsuite-opportunity-client-labels.md).
390
+ - **⚠ `hasEndCustomerColumn()` guards the DB COLUMN, NOT the ORM MODEL field — and that gap took the
391
+ whole opportunity sync down for 8 days (2026-09-02 → 2026-09-10).** The guard does a `SHOW COLUMNS`
392
+ on `Forecast.Opportunities`, so once the migration was applied it passed and the handler ran
393
+ `$opportunity->endCustomerName = …`. But the 2.0 model `_Model_Forecast_Opportunity`
394
+ (`_underscore/Model/Forecast/Opportunity.php`) never **declared** the field, so `_Model.__set`
395
+ (`_underscore/Model.php`) threw *"There is no field called 'endCustomerName' in the
396
+ '_Model_Forecast_Opportunity' model."* on **every** `Netsuite/Opportunity/post|put` — 100% failure,
397
+ every opportunity import dead. `Core.WorkerJobs` census: Sep 1 = 115 ok / 60 fail (last good day),
398
+ Sep 2 → Sep 10 = 0 ok / 100% fail (61 fails on Sep 10 alone). This was a **half-deploy**: the
399
+ migration + the handler write shipped, the model field declaration did not.
400
+ **Durable rule: a new column that the handler writes must ship THREE things together — the
401
+ `dbchanges2` migration, the handler write, AND the `_Model` field declaration.** A column-existence
402
+ guard (`SHOW COLUMNS`) proves nothing about the ORM; both ORMs enumerate *declared* properties, so an
403
+ undeclared field is a hard `__set` throw, not a silent skip. Fixed 2026-09-10 by declaring
404
+ `public $endCustomerName = self::FIELD_CHAR;` (matching sibling char fields clickupTaskId/title/memo
405
+ over the varchar column); deployed to worker2 `_production`, sync recovered.
390
406
  - Writes use raw `_Query` against `_underscore::DB_FORECAST` and **must**
391
407
  `_Database::transactionCommit(DB_FORECAST)` (lazy-transaction gotcha).
392
408
 
@@ -759,6 +775,19 @@ deprecated** for production opportunity code.
759
775
  blocked on the Aaron stakeholder decision noted above.
760
776
 
761
777
  ## Change history
778
+ - 2026-09-10 — **Fixed an 8-day total opportunity-sync outage caused by a half-deploy: a missing ORM
779
+ model field.** `Forecast.Opportunities.endCustomerName` (column added ~Sep 1) and the worker2 handler
780
+ write shipped, but `_Model_Forecast_Opportunity` (`_underscore/Model/Forecast/Opportunity.php`) never
781
+ **declared** the field. `hasEndCustomerColumn()` guards the DB column (`SHOW COLUMNS`), not the ORM
782
+ model, so the guard passed and `_Model.__set` then threw *"There is no field called 'endCustomerName'
783
+ in the '_Model_Forecast_Opportunity' model."* on **every** `Netsuite/Opportunity/post|put` —
784
+ `Core.WorkerJobs` shows Sep 1 = 115 ok / 60 fail (last good day), Sep 2 → Sep 10 = 0 ok / 100% fail.
785
+ Fix: declared `public $endCustomerName = self::FIELD_CHAR;` (php-reviewer clean); deployed to worker2
786
+ `_production`, sync recovered. Durable lesson added to the `endCustomerName` data-model bullet: a
787
+ handler-written column must ship its migration, its handler write, AND its `_Model` field declaration
788
+ together — a `SHOW COLUMNS` guard does not protect against a missing ORM field. Diagnosed with only the
789
+ `Core.WorkerJobs` action+outcome census (group by action, isSuccess over 7 days) via the toga-db MCP —
790
+ no NetSuite/ClickUp access. (jcardinal)
762
791
  - 2026-09-04 — **Investigation + design session; no code shipped (two prototype fixes were written and
763
792
  deliberately reverted).** Four corrections and four new facts. **Corrected:** the `STAGE_MAP`
764
793
  section described a constant that does not exist — the shipped map is **`SALES_STAGE_MAP`** on
@@ -18,10 +18,10 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
 
19
19
  ## 2.0 framework
20
20
 
21
- - **_underscore** (_Underscore) _(framework core)_ — 82 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **_underscore** (_Underscore) _(framework core)_ — 83 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 68 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
- - **dbchanges2** (Database Changes) _(framework core)_ — 17 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
+ - **dbchanges2** (Database Changes) _(framework core)_ — 18 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 6 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
27
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
@@ -5,6 +5,7 @@
5
5
  | [Prudential: Dell ASN units PRE/POST interceptor (legacy key + flat tracking)](features/dell-asn-units-interceptor.md) | 2.0 | After the tracking-number bridge migration, the ASN unit route was renamed (`advance-shipping-notice-units` → `advance-shipping-notice-item-units`), so the inhe |
6
6
  | [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. |
7
7
  | [Prudential: Device information import + unit→contact linking (import_device_information.php)](features/device-information-import-and-contact-linking.md) | 1.0 | Prudential's **device-sync** cron (`worker/crons/toga2/prudential/import_device_information.php`) pulls device/asset records (from ServiceNow / the Dell CMDB fe |
8
+ | [Prudential: ItemClasses device-tier data is dead (and how that was proved)](features/itemclasses-device-tier-data.md) | 2.0 | `Client_Prudential.ItemClasses` held 7 hand-seeded Dell/Apple **device tiers** ("LATITUDE 73xx", "MACBOOK PRO", …) tagged onto **9 of 765** items. |
8
9
  | [Prudential: OneUptime order-pipeline backlog monitors (Monitor/Prudential/*)](features/oneuptime-order-pipeline-monitors.md) | 2.0 | Three worker2 OneUptime push monitors (TRUE-80587) that watch the Prudential/Dell order pipeline for **stalls**, which were previously invisible — a stuck pipel |
9
10
  | [Prudential: order email PDF attachments (which guide goes on which email)](features/order-email-pdf-attachments.md) | 1.0 | Every customer-facing Prudential order email carries S3-hosted PDF guides, and **which** guide is attached is decided by the service request type. |
10
11
  | [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer |
@@ -0,0 +1,108 @@
1
+ ---
2
+ title: "Prudential: ItemClasses device-tier data is dead (and how that was proved)"
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: prudential
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-09-11
10
+ owners: ["rgirish"]
11
+ files:
12
+ - dbchanges2/Client_Prudential/2024-01-01.sql
13
+ - dbchanges2/Client_Prudential/2026-09-11 - ClearItemClasses.sql
14
+ - worker/crons/toga2/prudential/transmissions_to_netsuite.php
15
+ - worker/schedules/cron.worker.sync.json
16
+ related:
17
+ - ../profile.md
18
+ - ../../../2.0/apps/_underscore/features/item-classification-itemclasses.md
19
+ - ../../../2.0/apps/_underscore/features/record-change-audit-log.md
20
+ - ../../../1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `Client_Prudential.ItemClasses` held 7 hand-seeded Dell/Apple **device tiers**
26
+ ("LATITUDE 73xx", "MACBOOK PRO", …) tagged onto **9 of 765** items. The data was seeded once by
27
+ SQL in 2023, never maintained, is partly wrong, and **nothing reads or writes it today**. It was
28
+ cleared on 2026-09-11 to free the table for real NetSuite item classification later.
29
+
30
+ The second half of this doc is the **4-source method used to prove "this field is dead"** —
31
+ reusable for any field on any client.
32
+
33
+ ## How it works (what the data was)
34
+
35
+ Source is a hardcoded seed block in `dbchanges2/Client_Prudential/2024-01-01.sql:435-449`:
36
+ 7 `INSERT INTO ItemClasses`, then a per-`partNumber` `UPDATE` stamping `Items.itemClassId`.
37
+ **No API, no external feed, no sync.** That is the entire origin.
38
+
39
+ The one consumer was `worker/crons/toga2/prudential/transmissions_to_netsuite.php:171-178`,
40
+ which sent `ItemClasses.name` to NetSuite as `deviceTier`.
41
+
42
+ ## Proving a field is dead — the 4-source method
43
+
44
+ Run all four. Any one alone is weak; together they are conclusive.
45
+
46
+ **1. Grep for readers and writers in every repo.**
47
+ One reader (`transmissions_to_netsuite.php`), zero writers in `api2`, `worker2`, `_underscore`.
48
+ Watch for frontend grep noise — `itemClass` matches CSS variable names like `menuItemClassNames`.
49
+
50
+ **2. Check the field-change audit log — absence is evidence.**
51
+ Resolve the field id first (`Logs_*` and `Core` are on different clusters, so this is two
52
+ queries — see [Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md)):
53
+
54
+ - `Core.RecordFields` id **605** = `Items.itemClassId`; `Core.Records` id **21** = `Items`,
55
+ id **105** = `ItemClasses`.
56
+ - `Logs_Prudential.RecordField` has **zero rows** for field 605, ever. Zero log rows on record 105.
57
+
58
+ **Prove the logging itself works, or "no rows" means nothing.** Item logging is demonstrably
59
+ live: 1,963 log rows across 353 items, current through today, tracking `partNumber`,
60
+ `description`, `isFulfillable`, `catalogId`. So `itemClassId` simply never changes.
61
+
62
+ **3. Scan the inbound API payloads.**
63
+ Zero of **3,808** `/v2/items` calls in September carried an `itemClass` key. Items arrive from a
64
+ NetSuite sync cron — `POST /v2/items` from `34.214.204.95` (`apiId` 5) — with payload shape
65
+ `{partNumber, description, inventoryType, catalog:{name}, c_netsuiteInternalItemId, isFulfillable}`.
66
+ **No `itemClass` field.** That is why new items never get a class, and why only 9 of 765 are tagged.
67
+
68
+ **4. Check the consumer cron's `active` flag, then `git log` it.**
69
+ `worker/schedules/cron.worker.sync.json:441` — `"active": 0` on "Transmissions to Netsuite".
70
+ Disabled **2026-07-17** by Jeff Cardinal, commit `14570def` ("Add careOf & skip Apple; disable
71
+ Netsuite cron"), branch `_production`. See
72
+ [tracing a worker cron run](../../../1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md)
73
+ for the `active`-flag semantics.
74
+
75
+ ## Gotchas
76
+
77
+ - **⚠ The surviving tags were partly wrong.** 2 of the 9: item 3512 (`MRW23XX/A`,
78
+ "MacBook Pro 16 M3") was tagged **MACBOOK AIR**; item 3513 (`920-008219`, a Logitech keyboard)
79
+ was tagged **MACBOOK PRO**. Stale hand-seeded lookup data degrades silently — do not trust it
80
+ because it exists.
81
+ - **Clear the FK before the parent rows.** `Items_itemClassId` is the only FK referencing
82
+ `ItemClasses` (verified via `information_schema.KEY_COLUMN_USAGE` on `Client_Prudential`), so
83
+ the null-out of `Items.itemClassId` must run **before** the parent rows are removed.
84
+ - **Northwell was NOT touched.** Its 27 ServiceNow service categories and 245 tagged items are
85
+ still live. Open question, not yet decided: whether to move Northwell's rows to a separate
86
+ table if `ItemClasses` gets repurposed org-wide.
87
+
88
+ ## The migration
89
+
90
+ `dbchanges2/Client_Prudential/2026-09-11 - ClearItemClasses.sql` — data removal only, re-runnable,
91
+ table and columns untouched. Two statements: null out every non-null `Items.itemClassId`, then
92
+ empty `ItemClasses`. Order matters (see the FK gotcha above).
93
+
94
+ **Deliberately scoped small.** Adding the NetSuite classification columns
95
+ (`c_netsuiteInternalClassificationId`, `c_netsuiteFullName`, `c_netsuiteParentId`), dropping the
96
+ UNIQUE on `name`, and registering the `CustomRecordFields` rows for `recordId` 105 is a
97
+ **separate later task**. The traps already found for it are recorded in
98
+ [ItemClasses / _Model_Client_ItemClass](../../../2.0/apps/_underscore/features/item-classification-itemclasses.md).
99
+
100
+ ## Change history
101
+ - 2026-09-11 — Cleared the 7 stale device-tier rows and the 9 item tags via
102
+ `2026-09-11 - ClearItemClasses.sql` (null the FK first — `Items_itemClassId` is the only FK).
103
+ Documented the 4-source proof that the data was dead: one reader / zero writers,
104
+ zero `Logs_Prudential.RecordField` rows for `Core.RecordFields` id 605 against 1,963 live rows
105
+ on other Item fields, zero of 3,808 September `/v2/items` payloads carrying `itemClass`, and the
106
+ sole consumer cron `"active": 0` since 2026-07-17 (commit `14570def`). Also recorded that 2 of
107
+ the 9 surviving tags were plainly wrong. Column additions deliberately deferred to a later task.
108
+ (rgirish)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: prudential
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-31
9
+ updated: 2026-09-11
10
10
  owners: ["rgirish", "bala"]
11
11
  files:
12
12
  - worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php
@@ -86,20 +86,26 @@ TrackingNumbers WHERE purchaseOrderId = PurchaseOrders.id AND TrackingNumbers.st
86
86
  orders that genuinely qualified for a delivery email. Over 2026-07-01..07-09 this dropped 6 false
87
87
  positives (25 → 19 flagged) while retaining all 19 genuine discrepancies.
88
88
 
89
- ## A second cron sends the same shipped email
89
+ ## A second cron sends the same shipped email — but it is DISABLED (corrected 2026-09-11)
90
90
 
91
- `transmissions_to_netsuite.php` (also `active: 1`) builds and sends the **same** shipped email —
92
- same subjects, same branch shape — and also stamps `c_dtTransmittedOrderShippedUpdateToPrudential`.
93
- It is easy to miss: it is not named like the other `transmit_*` crons. Any change to the shipped
94
- email's content or attachments must be applied to **both** files or the two senders drift.
91
+ `transmissions_to_netsuite.php` builds and sends the **same** shipped email — same subjects, same
92
+ branch shape — and also stamps `c_dtTransmittedOrderShippedUpdateToPrudential`. It is easy to
93
+ miss: it is not named like the other `transmit_*` crons. Any change to the shipped email's content
94
+ or attachments must be applied to **both** files or the two senders drift.
95
+
96
+ > **⚠ It is `"active": 0`, not `active: 1` as this page said until 2026-09-11.**
97
+ > `worker/schedules/cron.worker.sync.json:441` — disabled **2026-07-17** by Jeff Cardinal, commit
98
+ > `14570def` ("Add careOf & skip Apple; disable Netsuite cron") on `_production`. So **it has not
99
+ > been sending anything since then**, and it is not currently a live duplicate sender. Keep the
100
+ > two files in sync anyway — it can be switched back on with one flag.
95
101
 
96
102
  **And it does not stop at the shipped email — it also sends the two *delivered* emails.**
97
103
  `transmissions_to_netsuite.php` carries "Prudential: New hire equipment has been delivered"
98
104
  (~L1765) and "Prudential: Your equipment has been delivered" (~L2149), the **same two subjects**
99
105
  that `transmit_closecomplete_updates_prudential.php` sends — except the `transmissions_to_netsuite`
100
- versions make **no `addAttachment()` calls at all**. Whether both crons fire for the same order was
101
- not established (2026-08-31); if they do, the customer receives a duplicate delivered email, one
102
- copy of which is missing the guides. Establish this before touching either delivered path.
106
+ versions make **no `addAttachment()` calls at all**. The open question from 2026-08-31 whether
107
+ both crons fire for the same order and send a duplicate delivered email — is **moot while this
108
+ cron is off** (see the box above). It becomes live again the moment anyone sets `"active": 1`.
103
109
 
104
110
  It was also the last Prudential cron still calling `file_get_contents()` directly for its PDFs and
105
111
  calling `addAttachment()` unconditionally — so a failed S3 pull threw and killed the batch, and a
@@ -201,6 +207,10 @@ stamp (crash hits the next iteration). With multiple New-type POs, a crash on PO
201
207
  all subsequent POs in the same run.
202
208
 
203
209
  ## Change history
210
+ - 2026-09-11 — **Correction: `transmissions_to_netsuite.php` is `"active": 0`, not `active: 1`.**
211
+ Disabled 2026-07-17 (commit `14570def`) in `cron.worker.sync.json:441`, so the "second shipped
212
+ sender" and the suspected duplicate *delivered* email are dormant, not live. Found while
213
+ auditing that cron as the only reader of `ItemClasses`. (rgirish)
204
214
  - 2026-08-31 — TRUE-81393 / PR #1707 (merged to `_production`): the shipped email's
205
215
  `$attachmentText` paragraph was unconditional while each attach was guarded, so a failed S3 pull
206
216
  produced copy promising guides that were not attached — and it said "guides" even for a single
@@ -15,7 +15,7 @@ project: _Underscore
15
15
  client: prudential
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-09-10
18
+ updated: 2026-09-11
19
19
  owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
20
20
  files: []
21
21
  related:
@@ -31,6 +31,7 @@ related:
31
31
  where each code is logged, and how to tell "we refused it" from "they never sent it".
32
32
  - features/supply-orders-csv-export.md
33
33
  - features/order-email-pdf-attachments.md
34
+ - features/itemclasses-device-tier-data.md
34
35
  ---
35
36
 
36
37
  ## Summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.796",
3
+ "version": "1.0.798",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",