toga-ai 1.0.797 → 1.0.799
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/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +21 -1
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +57 -1
- package/knowledge/1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md +29 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/cloud-s3-helpers.md +13 -2
- package/knowledge/2.0/apps/_underscore/features/item-classification-itemclasses.md +127 -0
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-app.md +35 -2
- package/knowledge/2.0/apps/toga-blox/workflows/dynamic-publish-pipeline.md +57 -2
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +50 -2
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/nychh/profile.md +11 -1
- package/knowledge/clients/prudential/INDEX.md +1 -0
- package/knowledge/clients/prudential/features/itemclasses-device-tier-data.md +108 -0
- package/knowledge/clients/prudential/features/transmit-ordershipped-email.md +19 -9
- package/knowledge/clients/prudential/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-11
|
|
10
10
|
owners: [jcardinal, mhammontree, bala]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -79,6 +79,17 @@ belong to the NetSuite importer — documented in the per-client-sync doc, not h
|
|
|
79
79
|
- Returns the decoded response object; callers read `->data->{resource}`, `->meta->nextPage`,
|
|
80
80
|
`->isSuccess`, `->status`, `->messages[].code`.
|
|
81
81
|
|
|
82
|
+
> **The "Invalid API Response" exception now LEADS with the api2 message codes (2026-09-11).**
|
|
83
|
+
> `send()`'s failure exception used to `print_r` the whole response envelope, whose header prints
|
|
84
|
+
> first — so on a real rejection the useful api2 code (e.g. `EV-10: ...`) fell **past** the
|
|
85
|
+
> `Logs.Event.errorMessage` `varchar(255)` limit and was **truncated away**, making the failure look
|
|
86
|
+
> "quiet" (this is exactly what hid the NYCHH duplicate-`Units` EV-10 — see the
|
|
87
|
+
> [per-client sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md)). The exception
|
|
88
|
+
> message now puts the `->messages[].code: text` list **first**, via a new private helper
|
|
89
|
+
> `describeApiMessages()` (~L726), then the `print_r` dump. The helper **caps each message's text at
|
|
90
|
+
> 160 chars** so an echoed customer value cannot land in full in the log table (cso point). Net: the
|
|
91
|
+
> real api2 cause self-reveals in `Logs.Event` on the next run instead of being cut off.
|
|
92
|
+
|
|
82
93
|
> **`send()` ALWAYS returns something carrying `isSuccess` — so `empty($response->isSuccess)` is a
|
|
83
94
|
> safe failure check on anything it returns, GETs included** (verified 2026-08-12, ~L432–450). It
|
|
84
95
|
> returns early **only** when `isSuccess` is true; when `isSuccess` is false and
|
|
@@ -413,6 +424,15 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
413
424
|
|
|
414
425
|
## Change history
|
|
415
426
|
|
|
427
|
+
- 2026-09-11 — **Observability: `send()`'s "Invalid API Response" exception now leads with the api2
|
|
428
|
+
message codes.** Added a private `describeApiMessages()` helper (~L726) that renders the
|
|
429
|
+
`->messages[].code: text` list (each text capped at 160 chars, cso point) and prepended it to the
|
|
430
|
+
exception message ahead of the `print_r` dump (~L636). Reason: `Logs.Event.errorMessage` is
|
|
431
|
+
`varchar(255)` and the old dump printed the envelope header first, pushing the real api2 code (e.g.
|
|
432
|
+
`EV-10`) past 255 chars where it was truncated — so failures looked "quiet". This made the NYCHH
|
|
433
|
+
duplicate-`Units` EV-10 self-reveal; the underlying case-mismatch fix and the three diagnosis traps
|
|
434
|
+
are on the [per-client sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md). Library
|
|
435
|
+
only, `php -l` clean; **written, not committed/deployed**. (jcardinal)
|
|
416
436
|
- 2026-09-08 - Added `updateTransferOrderStageToClosed(&$nsOrder, array &$clientConfiguration): bool`
|
|
417
437
|
and the constant `TRANSFER_ORDER_STATUS__CLOSED = 'closed'` (also now used by the `case 'Closed'` in
|
|
418
438
|
the transfer-order status switch). It is a **stage-only** writer: it never touches line items, so it
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-11
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -296,6 +296,49 @@ hit **every section and every client**, not just the by-reference ones.
|
|
|
296
296
|
GET's UTC. `strtotime()` and `App_Date::convertToSQLDate()` both use the script tz (Chicago) and are
|
|
297
297
|
the trap. Any manual cursor rewind value you set by hand must also be **Eastern**.
|
|
298
298
|
|
|
299
|
+
### ⚠ ITEM_RECEIPTS froze at RUNNING on a case-mismatched Units serial → duplicate-unit INSERT (EV-10 / 1062) (found + fixed 2026-09-11, NYCHH prod)
|
|
300
|
+
|
|
301
|
+
`syncItemReceiptFromNetsuite()` (`library/app/api/toga2.php` ~L4022) builds a PHP array of the
|
|
302
|
+
client's existing `Units` keyed by **serial number** to decide PUT (update) vs POST (create). The
|
|
303
|
+
DB `GET /units` (`Units.serialNumber IN (...)`) is **case-insensitive** and returns the **stored**
|
|
304
|
+
casing (e.g. `'na100'`), but the code keyed the PHP lookup by that **raw stored** value while reading
|
|
305
|
+
it with the **uppercased** serial (`strtoupper` at ~L3905). **PHP array keys are case-sensitive** → the
|
|
306
|
+
lookup missed the existing row → took the **create** path → `POST /units` collided on the
|
|
307
|
+
case-insensitive `Units.itemId_serialNumber` unique key → MySQL **1062** / api2 **EV-10** → the
|
|
308
|
+
ITEM_RECEIPTS section **threw before its cursor PUT**, freezing at RUNNING (and ITEM_FULFILLMENTS,
|
|
309
|
+
which runs behind it, froze with it).
|
|
310
|
+
|
|
311
|
+
- **Proven on NYCHH:** `Client_Nychh.Units` 23400-23409 store serial `'na100'..'na109'` (lowercase),
|
|
312
|
+
`c_netsuiteInternalInventoryAssignmentId` NULL; the sync sent `'NA100'`.
|
|
313
|
+
- **Fix (library only, `php -l` clean):** `strtoupper()` on **both** the array key **and** the reads,
|
|
314
|
+
so the PHP lookup is case-insensitive like the DB. A **second, half-fixed copy** of the same lookup
|
|
315
|
+
lower in the file (~L6326) already uppercased the **key** but not its two **reads** — made them
|
|
316
|
+
match too. php-reviewer + cso both cleared it (SAFE TO SHIP). **Written, NOT committed/deployed.**
|
|
317
|
+
- **Lesson:** whenever a PHP lookup mirrors a case-insensitive DB column, normalize case on **every**
|
|
318
|
+
side — the key write and every read. One un-normalized read reintroduces the duplicate-create path.
|
|
319
|
+
|
|
320
|
+
#### Diagnosis: three traps that hid this EV-10 (2026-09-11)
|
|
321
|
+
|
|
322
|
+
The section looked "quiet" (RUNNING, no loud crash-loop under the usual filter) because of three
|
|
323
|
+
things worth knowing for any 1.0-sync freeze:
|
|
324
|
+
|
|
325
|
+
- **areaPath is NOT only `crons/toga2/netsuite`.** 1.0 sync NetSuite/api2 errors also log under
|
|
326
|
+
`context.REFERENCE.areaPath` = `library/app/api` and `library/app/api/netsuite` — filtering on the
|
|
327
|
+
cron path alone **misses** them. (The "Filter on areaPath = `crons/toga2/netsuite`" note near the top
|
|
328
|
+
of this doc is therefore an *incomplete* filter — widen it to these two areas as well.)
|
|
329
|
+
- **EV-10 is api2's GENERIC "save failed" wrapper — the real DB error is NOT in base `Logs`.** The
|
|
330
|
+
underlying `1062 Duplicate entry ... for key ...` lives in `Logs_<Client>.Api`: filter
|
|
331
|
+
`responsePayload LIKE '%EV-10%'` (or join by `transactionId`) for route + `requestPayload` + the full
|
|
332
|
+
`responsePayload` carrying the real MySQL error. (Same `Logs_<Client>.Api` chain as the other rejection
|
|
333
|
+
gotchas in this doc.)
|
|
334
|
+
- **Some crashes never record at all.** The worker's error-log write to the logs DB can time out
|
|
335
|
+
(`mysqli_select_db ... errno=110 Connection timed out`, e.g. `Logs.Issue` 588/589), so a section can
|
|
336
|
+
freeze with **no** `Logs.Event`/`Issue` row — read `Client_<X>.Parameters` for the stuck RUNNING
|
|
337
|
+
section as the primary signal, not the log tables.
|
|
338
|
+
- Cross-link: the `send()` "Invalid API Response" exception was also changed 2026-09-11 to **lead with
|
|
339
|
+
the api2 message codes** so this EV-10 self-reveals in `Logs.Event` next time — see
|
|
340
|
+
[App_Api_Toga2](../../library/features/toga2-api-client-and-bridge.md).
|
|
341
|
+
|
|
299
342
|
### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
|
|
300
343
|
|
|
301
344
|
Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
|
|
@@ -1439,6 +1482,19 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
1439
1482
|
|
|
1440
1483
|
## Change history
|
|
1441
1484
|
|
|
1485
|
+
- 2026-09-11 — **ITEM_RECEIPTS froze at RUNNING on NYCHH: a case-mismatched `Units` serial lookup
|
|
1486
|
+
(library, fix written not deployed).** `syncItemReceiptFromNetsuite` (`library/app/api/toga2.php`
|
|
1487
|
+
~L4022) keyed a PHP existing-`Units` array on the DB's stored serial casing (`'na100'`) but read it
|
|
1488
|
+
with the uppercased serial (`'NA100'`, `strtoupper` ~L3905); PHP array keys are case-sensitive, so the
|
|
1489
|
+
lookup missed the row, took the create path, and hit the case-insensitive `Units.itemId_serialNumber`
|
|
1490
|
+
unique key → MySQL **1062** / api2 **EV-10** → the section threw before its cursor PUT and froze,
|
|
1491
|
+
dragging ITEM_FULFILLMENTS behind it. Fixed by `strtoupper()`-ing **both** the array key and the reads
|
|
1492
|
+
(and the half-fixed second copy ~L6326, which had the key but not its two reads). Proven on
|
|
1493
|
+
`Client_Nychh.Units` 23400-23409 (serial `'na100'..'na109'`). php-reviewer + cso cleared it. Also
|
|
1494
|
+
recorded the three diagnosis traps that hid it: sync errors also log under areaPath `library/app/api`
|
|
1495
|
+
and `library/app/api/netsuite` (not only `crons/toga2/netsuite`); EV-10 is a generic wrapper whose real
|
|
1496
|
+
1062 lives in `Logs_<Client>.Api` (`responsePayload LIKE '%EV-10%'`); and the worker's logs-DB write can
|
|
1497
|
+
time out (errno 110, `Logs.Issue` 588/589) so some crashes never record. (jcardinal)
|
|
1442
1498
|
- 2026-09-09 — **The resume cursor was READ on the wrong clock — a silent +1h window skip dropped
|
|
1443
1499
|
records across ALL 6 sections and ALL clients (worker, deployed + verified).** `startModeIteration`
|
|
1444
1500
|
(`common_sync_togasupply.php` ~L622) parsed the Eastern-stored cursor with `strtotime()` (script tz
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
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-
|
|
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-
|
|
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-
|
|
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,13 +6,15 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
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)_ —
|
|
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)
|
|
@@ -18,7 +18,7 @@ project: _Underscore
|
|
|
18
18
|
client: nychh
|
|
19
19
|
type: profile
|
|
20
20
|
status: active
|
|
21
|
-
updated: 2026-09-
|
|
21
|
+
updated: 2026-09-11
|
|
22
22
|
owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
|
|
23
23
|
files:
|
|
24
24
|
- dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
|
|
@@ -115,6 +115,16 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
115
115
|
written. Proven on IF `6138135` (Eastern 2025-09-04 17:26:58). Fixes and the standing "cursor is
|
|
116
116
|
always Eastern" rule live in the
|
|
117
117
|
[per-client sync feature doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
118
|
+
- **ITEM_RECEIPTS froze on a case-mismatched serial → duplicate-`Units` EV-10/1062, root-caused + fixed
|
|
119
|
+
(2026-09-11, fix NOT deployed).** The receipts feed threw before its cursor PUT (freezing it and
|
|
120
|
+
ITEM_FULFILLMENTS behind it) because `syncItemReceiptFromNetsuite` keyed its existing-`Units` PHP
|
|
121
|
+
lookup on the DB's stored serial casing (`'na100'`) but read it with the uppercased serial (`'NA100'`);
|
|
122
|
+
the missed row took the create path and collided on the case-insensitive `Units.itemId_serialNumber`
|
|
123
|
+
unique key → MySQL 1062 / api2 EV-10. Proven on `Client_Nychh.Units` 23400-23409
|
|
124
|
+
(serials `'na100'..'na109'`, `c_netsuiteInternalInventoryAssignmentId` NULL). Library-only fix
|
|
125
|
+
(`strtoupper()` on both the key and the reads). This is a **different** duplicate from the open
|
|
126
|
+
sales-order `Items` duplicate below (658-BFZH). Mechanism + the EV-10 → `Logs_Nychh.Api` diagnosis
|
|
127
|
+
traps: [per-client sync doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
118
128
|
- **NYCHH sync scope + data-gap scale + backfill approach (2026-09-09).**
|
|
119
129
|
- **Scope = 22 NetSuite customers:** parent **28908** "NYC Health + Hospitals" + 21 hospital child
|
|
120
130
|
customers (`24145, 29273, 29276, 31584, 31910-31916, 31918-31925, 32229, 33674`).
|
|
@@ -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-
|
|
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`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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**.
|
|
101
|
-
|
|
102
|
-
|
|
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-
|
|
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