toga-ai 1.0.528 → 1.0.530
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 +14 -2
- package/knowledge/1.0/apps/togaview/architecture.md +9 -3
- package/knowledge/1.0/apps/togaview/features/login-flows.md +61 -14
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +36 -1
- package/knowledge/2.0/apps/_underscore/features/netsuite-rest-client.md +21 -2
- package/knowledge/2.0/apps/_underscore/features/tracking-number-bridges.md +13 -2
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/nychh-asset-tag-backfill.md +99 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +11 -2
- package/knowledge/clients/rumcsi/profile.md +16 -7
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [jcardinal, mhammontree]
|
|
9
|
+
updated: 2026-08-05
|
|
10
|
+
owners: [jcardinal, mhammontree, bala]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
13
|
- worker/crons/toga2/aig/sync_togasupply_aig.php
|
|
@@ -275,6 +275,14 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
275
275
|
|
|
276
276
|
## Gotchas / known issues
|
|
277
277
|
|
|
278
|
+
- **A 2.0 GET silently TRUNCATES a nested child collection — never trust a single response for a
|
|
279
|
+
count.** When you read a parent with a nested `hasMany` collection in one GET, api2 can return the
|
|
280
|
+
collection **partial** (observed: a fulfillment's nested `itemFulfillmentItemUnits` came back **45
|
|
281
|
+
of 70**) with no error and no obvious signal — so any verification that counts off the nested array
|
|
282
|
+
is silently wrong. **Read the collection flat and fully paginated** (query the child resource
|
|
283
|
+
directly with the universal `meta->nextPage` loop) and take counts from **`meta.totalRecordCount`**,
|
|
284
|
+
not from a single nested response. Surfaced building the NYCHH fulfillment asset-tag verifier
|
|
285
|
+
(`worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php`).
|
|
278
286
|
- **🔐 Hardcoded credentials.** Every client's `CLIENT_UUID_*`, `API_UUID_*`, and `API_SECRET_*`
|
|
279
287
|
(plus the "True Workers" master keys) are committed as plaintext class constants at the top of
|
|
280
288
|
`toga2.php`. Treat them as **compromised-if-leaked**; do not reproduce the values in the KB,
|
|
@@ -353,6 +361,10 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
353
361
|
|
|
354
362
|
## Change history
|
|
355
363
|
|
|
364
|
+
- 2026-08-05 — Recorded that a 2.0 GET **silently truncates a nested child collection** (observed
|
|
365
|
+
`itemFulfillmentItemUnits` 45 of 70) with no error — so verification/counts must read the child
|
|
366
|
+
resource **flat + fully paginated** and use `meta.totalRecordCount`, never a single nested
|
|
367
|
+
response. Surfaced building the NYCHH fulfillment asset-tag verifier. (bala)
|
|
356
368
|
- 2026-08-04 (**uncommitted/undeployed** at time of writing) — `authenticate()` now records the
|
|
357
369
|
ambient client uuid for error reporting (`App_Error_Capture::setCurrentClientUuid()`, store-only)
|
|
358
370
|
because it is the one place every 1.0→2.0 cron passes through holding its `UUID_CLIENT` — this is
|
|
@@ -108,9 +108,15 @@ divergent shapes (read `emailAddress` with `email` fallback).
|
|
|
108
108
|
|
|
109
109
|
**SAML** (rumcsi, towfoundation, newcenturyholdingsllc) is handled in `framework.php`
|
|
110
110
|
(`meta`/`acs`/`sls`); newcenturyholdingsllc and towfoundation **auto-create `people` rows** for
|
|
111
|
-
unknown SAML users. rumcsi additionally accepts a
|
|
112
|
-
account-takeover path that
|
|
113
|
-
|
|
111
|
+
unknown SAML users. rumcsi additionally accepts a `?hash=<base64 email>` auto-login — **an unauthenticated
|
|
112
|
+
account-takeover path that is also, per the routing evidence, RUMC's live production SSO login**:
|
|
113
|
+
the block holds the only rumcsi route to `/enterprise_dashboard` (where a real production SSO
|
|
114
|
+
login lands), and togaview has no `?saml=` handling, so the 2.0 SAML gateway is not involved.
|
|
115
|
+
Only rumcsi's `acs` handler fails to populate the session (newcenturyholdingsllc `:819` and
|
|
116
|
+
towfoundation `:1180` do), which is why only rumcsi has a magic link. **Deleting it breaks RUMC
|
|
117
|
+
login into an IdP redirect loop** — sign the token or complete the `acs` handler instead, in
|
|
118
|
+
coordination with the external link generator. A cold-login capture to confirm the inbound
|
|
119
|
+
`?hash=` hop is still outstanding (see [login flows](features/login-flows.md)).
|
|
114
120
|
|
|
115
121
|
## Dashboards
|
|
116
122
|
Five dashboards, one per audience/business model, each delegating to a client override or the
|
|
@@ -72,26 +72,65 @@ lookup silently found nothing. Removed, and `email LIKE '…'` changed to `=` in
|
|
|
72
72
|
|
|
73
73
|
## Gotchas / known issues
|
|
74
74
|
|
|
75
|
-
### OPEN SECURITY ITEM — rumcsi `?hash=`
|
|
75
|
+
### OPEN SECURITY ITEM — rumcsi `?hash=` is the LIVE production SSO login (do not delete it)
|
|
76
76
|
`_/app/framework.php` authenticates a rumcsi user on nothing but
|
|
77
77
|
`base64_decode(urldecode($_GET['hash']))` matching an active `people` row for the rumcsi
|
|
78
78
|
clientid — **no password, signature, nonce, or expiry** — then populates ~18 session keys and
|
|
79
79
|
redirects to `/enterprise_dashboard`. Anyone who can guess a RUMC user's email can log in as
|
|
80
80
|
them.
|
|
81
81
|
|
|
82
|
-
It was removed during TRUE-80671 and then **deliberately reverted
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
It was removed during TRUE-80671 and then **deliberately reverted**. Routing evidence gathered
|
|
83
|
+
afterwards shows this is not a dormant legacy backdoor — it is **how RUMC actually logs in**:
|
|
84
|
+
|
|
85
|
+
- Jonathan performed a real production SSO login and was returned to
|
|
86
|
+
`https://rumcsi.togaview.agilantsolutions.com/enterprise_dashboard`.
|
|
87
|
+
- Inside the rumcsi block, **`framework.php:278` is the ONLY path to `/enterprise_dashboard`** —
|
|
88
|
+
and it sits **inside the hash block**, behind `if ($rumcsiUser)`. The SAML `acs` handler instead
|
|
89
|
+
does `App_MVC::routeTo('/')`. The only other RUMC route to that page is
|
|
90
|
+
`mvc/login/post.php:511`, which is the *password* login. **Landing on `/enterprise_dashboard`
|
|
91
|
+
after an SSO login is the hash block's signature.**
|
|
92
|
+
- The absence of `?hash=` in the final URL is **expected, not exculpatory**: the block consumes
|
|
93
|
+
the parameter and issues a fresh redirect, so the query string is gone from the address bar.
|
|
94
|
+
- **The modern alternative is ruled out.** The 2.0 SAML gateway (repo `saml`,
|
|
95
|
+
`Controller/Index.php:456-460`) hands off with `?saml=<base64 JSON whose client and user UUIDs
|
|
96
|
+
are encrypted via `_String::encryptWithKey`>` — the correct mechanism, which `tools` uses.
|
|
97
|
+
togaview has **zero** handling for `?saml=` (no `$_GET['saml']` anywhere). RUMC does not come
|
|
98
|
+
through that gateway, and there is no hidden session-population path.
|
|
99
|
+
- **Had the deletion shipped, RUMC SSO would have broken outright:** authenticate at the IdP →
|
|
100
|
+
`acs` → `/` → no `contactId` → bounced back to the IdP. A redirect loop, not a login. The
|
|
101
|
+
revert was correct.
|
|
102
|
+
|
|
103
|
+
**Still open — who generates the link.** Nothing in togaview/togadesk/library builds a `hash=`
|
|
104
|
+
URL, so the generator is external (likely RUMC's IdP-initiated flow or their own portal).
|
|
105
|
+
**Confirming test:** a **cold** SSO login in a private window with DevTools → Network →
|
|
106
|
+
*Preserve log*, capturing the full redirect chain and checking whether the hop **into** togaview
|
|
107
|
+
carries `?hash=`. Jonathan's test may not have exercised a cold login at all — with a live session
|
|
108
|
+
cookie the `!array_key_exists('contactId', $_SESSION)` gate is skipped entirely and
|
|
109
|
+
`/enterprise_dashboard` is simply where he already was.
|
|
110
|
+
|
|
111
|
+
**Fix shape — a coordinated change, never a deletion.** Two options, both requiring coordination
|
|
112
|
+
with whoever generates the link:
|
|
113
|
+
- **(a) Keep the handoff but sign it.** Replace the unsigned base64 email with an HMAC-signed
|
|
114
|
+
token via `App_String::encrypt`/`decrypt` (`library/app/string.php`). Smallest change; kills
|
|
115
|
+
guessability.
|
|
116
|
+
- **(b) Complete the rumcsi `acs` handler** so it builds the session itself, then retire the
|
|
117
|
+
handoff.
|
|
118
|
+
|
|
119
|
+
⚠ **Do NOT delete the block first, under either option.**
|
|
120
|
+
|
|
121
|
+
### Why rumcsi is the only one of the three SAML clients with a magic-link login
|
|
122
|
+
The real lesson is the contrast between the three `acs` handlers in `_/app/framework.php`:
|
|
123
|
+
|
|
124
|
+
| Client | `acs` handler | Sets `contactId` / session? |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| newcenturyholdingsllc | `:819` | **Yes** — completes the login |
|
|
127
|
+
| towfoundation | `:1180` | **Yes** — completes the login |
|
|
128
|
+
| rumcsi | `:399-417` | **No** — only `routeTo('/')` (since 9e7d306, "phase one … make sure redirect works") |
|
|
129
|
+
|
|
130
|
+
rumcsi's handler was never finished, and the `?hash=` block is what filled the gap. When adding a
|
|
131
|
+
SAML client, the `acs` handler **must** populate the session — an unfinished one invites exactly
|
|
132
|
+
this kind of unsigned side door.
|
|
133
|
+
|
|
95
134
|
- Note the surviving SAML branch is gated on `!inTestMode()`, so **test mode leaves rumcsi with
|
|
96
135
|
no login path at all**.
|
|
97
136
|
|
|
@@ -106,6 +145,14 @@ generated outside our codebase.
|
|
|
106
145
|
- The flows are order-dependent: a contact matching an earlier flow never reaches later ones.
|
|
107
146
|
|
|
108
147
|
## Change history
|
|
148
|
+
- 2026-08-05 (later, TRUE-80671) — **Upgraded the rumcsi `?hash=` finding from "may be" to
|
|
149
|
+
evidenced:** `framework.php:278` inside the hash block is the only rumcsi route to
|
|
150
|
+
`/enterprise_dashboard`, where a real production SSO login landed; togaview has no `?saml=`
|
|
151
|
+
handling at all, so the 2.0 SAML gateway is ruled out; deleting the block would have produced an
|
|
152
|
+
IdP redirect loop, not a login. Recorded the three-way `acs` handler contrast (newcentury :819 /
|
|
153
|
+
towfoundation :1180 populate the session, rumcsi :399-417 never did), both fix shapes (sign the
|
|
154
|
+
token with `App_String::encrypt`, or finish `acs`), and the outstanding cold-login DevTools test.
|
|
155
|
+
(mhammontree)
|
|
109
156
|
- 2026-08-05 (TRUE-80671, uncommitted) — Fixed the unguarded `$user_name[1]` session-name build
|
|
110
157
|
that **locked mononym / shared-mailbox users out entirely** (4 sites); removed
|
|
111
158
|
`sqlProtect(sqlEscape())` and switched email lookups to `=`; recorded the rumcsi `?hash=`
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-05
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -91,6 +91,32 @@ places or the sync runs but imports nothing (or 403s). All five steps are requir
|
|
|
91
91
|
The most commonly-missing one is `c_netsuiteInternalCustomerId` on the **Customers** record
|
|
92
92
|
(`recordId 113`, `isIdentifier=1`).
|
|
93
93
|
|
|
94
|
+
### Item-fulfillment section is a FULL reconcile (like `syncSalesOrderFromNetsuite`)
|
|
95
|
+
|
|
96
|
+
`App_Api_Toga2::syncItemFulfillmentFromNetsuite` (`library/app/api/toga2.php`) is now a **full
|
|
97
|
+
reconcile**, not an append: on every re-sync it makes TOGa **match NetSuite**. It updates item
|
|
98
|
+
fulfillment items and units, sets serial / `assetTag` / `macAddress` (**never blanking** a value
|
|
99
|
+
that is already populated), and maintains **all three tracking bridges** —
|
|
100
|
+
`ItemFulfillments_TrackingNumbers` (record 317, header), `ItemFulfillmentItems_TrackingNumbers`
|
|
101
|
+
(318, item), `ItemFulfillmentItemUnits_TrackingNumbers` (319, unit). It **prunes** units/items that
|
|
102
|
+
NetSuite has dropped, **deleting their tracking-bridge links FIRST** because those bridge FKs are
|
|
103
|
+
`RESTRICT` (no cascade) — delete the bridge rows before the parent unit/item or the delete errors.
|
|
104
|
+
See the [tracking-number bridge doc](../../../2.0/apps/_underscore/features/tracking-number-bridges.md).
|
|
105
|
+
|
|
106
|
+
- **Record 318 is fetched in its own isolated GET.** 318 is deliberately excluded from the main
|
|
107
|
+
fulfillment GET: requesting a field a role cannot read **403s the whole GET**, so 318 is
|
|
108
|
+
reconciled in a separate try/catch GET that can fail without taking the rest down.
|
|
109
|
+
- **Re-sync trigger is the ItemShip `lastmodifieddate`.** A tag added later only to the
|
|
110
|
+
serial/inventory-number record does **not** bump the fulfillment, so late tags are **not** caught
|
|
111
|
+
here — they are handled by the separate per-client asset-tag backfill/cron (e.g. NYCHH's, see
|
|
112
|
+
[nychh-asset-tag-backfill](../../../2.0/apps/worker2/features/nychh-asset-tag-backfill.md)).
|
|
113
|
+
- **Upstream-mirror interaction is a cheap no-op for non-Compass clients.** Any
|
|
114
|
+
`/item-fulfillments`, `/item-fulfillment-items`, `/item-fulfillment-item-units` POST/PUT fires
|
|
115
|
+
`_underscore`'s `_Model_Client_ItemFulfillment::ensureUpstreamItemFulfillment` (the Compass
|
|
116
|
+
multi-tier SO→PO→SO mirror), but it is a ~2-query no-op for NYCHH / Quad / Prudential (verified:
|
|
117
|
+
0 of their fulfillments resolve an upstream sales order). `/units` writes and all three
|
|
118
|
+
tracking-bridge models have **no** interceptor, and **DELETE fires none**.
|
|
119
|
+
|
|
94
120
|
### Custom field contract (hardcoded in `common_sync_togasupply.php`)
|
|
95
121
|
|
|
96
122
|
The engine requests these `c_` fields by literal name per route — every synced client must have
|
|
@@ -211,6 +237,15 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
211
237
|
|
|
212
238
|
## Change history
|
|
213
239
|
|
|
240
|
+
- 2026-08-05 — Documented that `syncItemFulfillmentFromNetsuite` is now a **full reconcile** like
|
|
241
|
+
`syncSalesOrderFromNetsuite`: updates IF items/units + serial/`assetTag`/`macAddress`
|
|
242
|
+
(never blanking), maintains **all three** tracking bridges (317/318/319), and **prunes**
|
|
243
|
+
dropped units/items **deleting their bridge links first** (bridge FKs are `RESTRICT`). Record
|
|
244
|
+
318 is reconciled in its own isolated try/catch GET (a forbidden field 403s the whole GET);
|
|
245
|
+
re-sync trigger is the ItemShip `lastmodifieddate` (serial-only tag edits are handled by the
|
|
246
|
+
separate asset-tag backfill). The upstream-mirror interceptor
|
|
247
|
+
(`ensureUpstreamItemFulfillment`) is a ~2-query no-op for NYCHH/Quad/Prudential (0 upstream SOs);
|
|
248
|
+
`/units` + the bridge models have no interceptor and DELETE fires none. (bala)
|
|
214
249
|
- 2026-07-22 — Recorded the **dual-catalog duplicate OD PO-line bug** in
|
|
215
250
|
`syncSalesOrderFromNetsuite` (uuid-only PO-line reconciliation → shadow-VendorItem duplicate
|
|
216
251
|
lines) and its SKU-fallback fix. Compass cleanup + sequencing captured in the
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["dfranks", "jcardinal"]
|
|
9
|
+
updated: 2026-08-05
|
|
10
|
+
owners: ["dfranks", "jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
13
13
|
related:
|
|
@@ -131,6 +131,20 @@ would corrupt values like `O'Brien`. Use `str_replace("'", "''", $value)` to esc
|
|
|
131
131
|
string literal. (worker2's `SalesOrder.php` carries this as a private `escapeSuiteQl()` helper;
|
|
132
132
|
any new SuiteQL caller must do the same.)
|
|
133
133
|
|
|
134
|
+
## SuiteQL custom columns are SELECT-only — you cannot filter (WHERE) on them
|
|
135
|
+
|
|
136
|
+
You **can `SELECT`** a NetSuite custom column in SuiteQL, but you **cannot put it in a `WHERE`**.
|
|
137
|
+
On the `inventorynumber` table, `SELECT custitemnumber_asset_tag, custitemnumber_mac_address` returns
|
|
138
|
+
the values fine — but `WHERE custitemnumber_asset_tag IS NOT NULL` (or any predicate on a custom
|
|
139
|
+
column) returns **0 rows with NO error**. It fails silently, so it looks like "no matching data"
|
|
140
|
+
rather than a bad query. Confirmed live 2026-07-28.
|
|
141
|
+
|
|
142
|
+
**Drive the query off a native column instead**, then filter in PHP: query
|
|
143
|
+
`WHERE inventorynumber IN ('serial1','serial2',…)` (chunk over the serials/keys you already have —
|
|
144
|
+
500 at a time works), `SELECT` the custom columns alongside, and drop blanks in PHP after the fetch.
|
|
145
|
+
Surfaced building the NYCHH asset-tag backfill (see
|
|
146
|
+
[nychh-asset-tag-backfill](../../worker2/features/nychh-asset-tag-backfill.md)).
|
|
147
|
+
|
|
134
148
|
## Logging
|
|
135
149
|
|
|
136
150
|
`send()` calls `setLogging(false)`, so **NetSuite REST request/response bodies are NOT written to
|
|
@@ -155,6 +169,11 @@ doc.)
|
|
|
155
169
|
|
|
156
170
|
## Change history
|
|
157
171
|
|
|
172
|
+
- 2026-08-05 — Recorded the **SuiteQL custom-column filter trap**: a NetSuite custom column
|
|
173
|
+
(e.g. `inventorynumber.custitemnumber_asset_tag`/`_mac_address`) can be `SELECT`ed but **not**
|
|
174
|
+
filtered on — `WHERE custitemnumber_asset_tag IS NOT NULL` returns 0 rows with no error. Drive
|
|
175
|
+
the query off a native column (`WHERE inventorynumber IN (...)`, chunked) and filter blanks in
|
|
176
|
+
PHP. Surfaced building the NYCHH asset-tag backfill. (bala)
|
|
158
177
|
- 2026-07-21 — **Documented the auth mechanism, config-key locations, `send()` signature, and the
|
|
159
178
|
`RECORD_*`/query/metadata constant catalog** (folded in from a retired project-local
|
|
160
179
|
`worker2/netsuite/CLAUDE.md`). Auth = OAuth 2.0 certificate-signed JWT client assertion
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal", "mhammontree", "dfranks", "apeterson"]
|
|
9
|
+
updated: 2026-08-05
|
|
10
|
+
owners: ["jcardinal", "mhammontree", "dfranks", "apeterson", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php
|
|
@@ -121,6 +121,13 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
121
121
|
|
|
122
122
|
- **Tracking is now an inherent child, not a scalar.** Any producer that sent a flat `trackingNumber`
|
|
123
123
|
on a unit must nest it under the unit's `*TrackingNumbers` child array, or it is silently dropped.
|
|
124
|
+
- **Pruning a unit/item means deleting its bridge rows FIRST — the bridge FKs are `RESTRICT`, not
|
|
125
|
+
cascade.** The IF bridge FKs (`ItemFulfillments/Items/ItemUnits_TrackingNumbers` → parent) are
|
|
126
|
+
`ON DELETE RESTRICT`, so deleting a parent unit/item that NetSuite dropped errors unless you delete
|
|
127
|
+
its 317/318/319 bridge links first. The 1.0 `App_Api_Toga2::syncItemFulfillmentFromNetsuite` full
|
|
128
|
+
reconcile does exactly this (delete bridges, then parent). Any code that prunes IF units/items must
|
|
129
|
+
do the same ordering. See the
|
|
130
|
+
[per-client sync doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
124
131
|
- **Migration FK pitfalls (all handled in the SQL):** deleting `RecordFields` hits `DefaultRecordFieldSettings`
|
|
125
132
|
FKs (repoint first); deleting `AclRecordPermissions` hits the `AclLogicGroups`→`AclLogicGroupExpressions`
|
|
126
133
|
chain (record-41 teardown wraps it in `SET FOREIGN_KEY_CHECKS=0`); deleting `TableViewFields` hits
|
|
@@ -173,6 +180,10 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
173
180
|
- **Porting the item-fulfillment TableView fix to another client is two independent decisions, not a copy-paste.** The **re-root** (Units 31 → ItemFulfillmentItems 29 + OUTER unit chain) is universally portable and fixes the "0 records" bug everywhere. The **tracking source is client-specific**: Compass writes one tracking number per item fulfillment at the **item level (318)**, so its views read 318. Other clients populate different bridge levels — verify with `SELECT COUNT(*)` per bridge before choosing. As of 2026-06-19, **record 318 is empty in `Client_Nychh` and `Client_Quad`**; their tracking lives at the **IF/shipment level (317)** (covers IFIs: NYCHH 7955/10049 ≈ 79%, Quad 4878/4967 ≈ 98%) and the **unit level (319)** (≈ 7–9% — serialized only). Copying Compass's 318 join into a client that doesn't write 318 yields a structurally-correct view with permanently blank tracking until 318 is backfilled. (NYCHH's pre-fix view pointed at the now-deleted record 41 = old IF-level/package bridge, i.e. it originally intended IF-level 317.)
|
|
174
181
|
|
|
175
182
|
## Change history
|
|
183
|
+
- 2026-08-05 — Recorded that the 1.0 `App_Api_Toga2::syncItemFulfillmentFromNetsuite` full reconcile
|
|
184
|
+
now maintains **all three** IF bridges (317/318/319) and, when pruning units/items NetSuite dropped,
|
|
185
|
+
**deletes their bridge links first** because the bridge parent FKs are `RESTRICT` (no cascade) —
|
|
186
|
+
added the delete-order gotcha. (bala)
|
|
176
187
|
- 2026-07-14 — **GroWrk** hit the bridge ACL gap (TRUE-79191): 403 EZ-1 on `item-fulfillment-tracking-numbers` made TOGa Supply's Pending Shipments list come back `isSuccess:false` (frontend showed the empty "Create your first shipment" state). GroWrk never got the fix that Prudential (2026-06-15) and Quad (2026-06-18a) already had. Added `Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql` (all 6 bridges 317-322, idempotent NOT-EXISTS guards, `slug='all'`/`sqlExpression='1'`, root logic group operator AND). Decision: kept Growrk-scoped for now (only client on Fulfill & Ship); shared `Client/` migration remains the long-term fix. (mhammontree)
|
|
177
188
|
- 2026-07-09 — Noted that `Client_CompassCanada` is missing these bridge tables on **beta**, so a local
|
|
178
189
|
reset from beta reintroduces the 17-table drift (1146 on SalesOrders load); fix locally with
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
28
28
|
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
29
29
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
30
|
+
| [NYCHH Asset-Tag Backfill (worker2)](features/nychh-asset-tag-backfill.md) | Keeps NYC Health & Hospitals (`Client_Nychh`) unit asset tags and MAC addresses synced from NetSuite. | worker2/Worker/Client/Nychh.php, worker2/Worker/Client/Nychh/AssetTagBackfill.php, worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php, worker/crons/toga2/netsuite/backfill_all_asset_tags_from_netsuite_nychh.php |
|
|
30
31
|
| [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php, worker2/Worker/Monitor/Operations.php |
|
|
31
32
|
| [Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-client-data- | worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
32
33
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NYCHH Asset-Tag Backfill (worker2)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: nychh
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-05
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Client/Nychh.php
|
|
13
|
+
- worker2/Worker/Client/Nychh/AssetTagBackfill.php
|
|
14
|
+
- worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php
|
|
15
|
+
- worker/crons/toga2/netsuite/backfill_all_asset_tags_from_netsuite_nychh.php
|
|
16
|
+
related:
|
|
17
|
+
- ../../_underscore/features/netsuite-rest-client.md
|
|
18
|
+
- ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
19
|
+
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
20
|
+
- creating-worker-actions.md
|
|
21
|
+
- ../../../clients/nychh/profile.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
Keeps NYC Health & Hospitals (`Client_Nychh`) unit asset tags and MAC addresses synced from
|
|
27
|
+
NetSuite. In NetSuite the asset tag / MAC live on the **inventory-number (serial) record**, not
|
|
28
|
+
the item-fulfillment transaction — so the regular per-client fulfillment sync does **not** pick up
|
|
29
|
+
a tag that is added to the serial after the ItemShip is created (the sync's trigger is the
|
|
30
|
+
fulfillment's `lastmodifieddate`, and a serial-only edit does not bump it). This backfill closes
|
|
31
|
+
that gap: it reads every NYCHH unit's serial, asks NetSuite for the current tag/MAC, and writes
|
|
32
|
+
them back.
|
|
33
|
+
|
|
34
|
+
This is the **first NYCHH presence in worker2**. The write path is deliberately **direct-DB**
|
|
35
|
+
(no api2), mirroring `_Worker_Client_Prudential_AssetImports`, to avoid the known beta
|
|
36
|
+
worker2→api2 hard-crash (PHP 8.x, 500 with no trace).
|
|
37
|
+
|
|
38
|
+
## Key files / entry points
|
|
39
|
+
|
|
40
|
+
- **`worker2/Worker/Client/Nychh.php`** — the NYCHH client worker base. Its inherited
|
|
41
|
+
`initialize()` registers the `Client_Nychh` DB so any `_Model_Client_*` load resolves (see the
|
|
42
|
+
`DB_CLIENT` auto-registration gotcha in [creating-worker-actions](creating-worker-actions.md)).
|
|
43
|
+
- **`worker2/Worker/Client/Nychh/AssetTagBackfill.php`** — the action.
|
|
44
|
+
Method: `Backfill(bool $dryRun = true, ?string $serialNumber = null): string`.
|
|
45
|
+
- Action path: `Client/Nychh/AssetTagBackfill/Backfill` → class
|
|
46
|
+
`_Worker_Client_Nychh_AssetTagBackfill`, method `Backfill`.
|
|
47
|
+
- Live cron: **`Core.CronJobs` id 44**, action `Client/Nychh/AssetTagBackfill/Backfill`.
|
|
48
|
+
- **NetSuite access** — `_Component_Api_Netsuite` (2.0 `_underscore` REST client) SuiteQL. Creds
|
|
49
|
+
come from the worker2 `[netsuite]` ini section (`worker2/Config/<env>.ini`) — **location only,
|
|
50
|
+
never the values**; see [netsuite-rest-client](../../_underscore/features/netsuite-rest-client.md).
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
1. Load all `Client_Nychh.Units` keyed by serial number.
|
|
55
|
+
2. Ask NetSuite (SuiteQL over the `inventorynumber` table) for the asset tag + MAC of those
|
|
56
|
+
serials, **chunked 500 at a time**, filtering on the **native `inventorynumber` column**:
|
|
57
|
+
`WHERE inventorynumber IN ('serial1','serial2',…)`. Then `SELECT` the custom columns
|
|
58
|
+
`custitemnumber_asset_tag` / `custitemnumber_mac_address` and filter blanks in PHP.
|
|
59
|
+
> **Do not filter on the custom columns.** SuiteQL will `SELECT` them but a
|
|
60
|
+
> `WHERE custitemnumber_asset_tag IS NOT NULL` returns **0 rows with no error** — see the
|
|
61
|
+
> SuiteQL custom-field gotcha in
|
|
62
|
+
> [netsuite-rest-client](../../_underscore/features/netsuite-rest-client.md).
|
|
63
|
+
3. **Never-blank** batch `UPDATE` of `Units.assetTag` / `Units.macAddress`: a value that is empty
|
|
64
|
+
in NetSuite never overwrites a populated TOGa value (NYCHH tags are sparse-then-growing, so a
|
|
65
|
+
blank NetSuite side is "not assigned yet", not "clear it").
|
|
66
|
+
|
|
67
|
+
### Running it
|
|
68
|
+
|
|
69
|
+
- `parameters` is spread as **named arguments** into `Backfill` (worker2 dispatch contract — see
|
|
70
|
+
[creating-worker-actions](creating-worker-actions.md)). It runs **dry by default**.
|
|
71
|
+
- **Write:** pass `{"dryRun": false}`.
|
|
72
|
+
- **Single-serial verification:** pass `serialNumber` to run one serial and print a
|
|
73
|
+
NetSuite-vs-TOGa comparison (no batch, useful for spot-checking a specific unit).
|
|
74
|
+
|
|
75
|
+
### Verification & full-history backfill (1.0 companions)
|
|
76
|
+
|
|
77
|
+
Two 1.0 `worker` scripts (run via PuTTY) back this up:
|
|
78
|
+
|
|
79
|
+
- **`worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php`** — read-only,
|
|
80
|
+
compares NetSuite vs TOGa per fulfillment to confirm the sync grabbed tags correctly.
|
|
81
|
+
- **`worker/crons/toga2/netsuite/backfill_all_asset_tags_from_netsuite_nychh.php`** —
|
|
82
|
+
full-history SOAP per-serial backfill; `DRY_RUN` default, never-blank, resumable via a ledger
|
|
83
|
+
file.
|
|
84
|
+
|
|
85
|
+
> **Reading the TOGa side must be flat + fully paginated.** api2 GET silently **truncates nested
|
|
86
|
+
> collections** in a single response (a fulfillment's nested `itemFulfillmentItemUnits` came back
|
|
87
|
+
> as 45 of 70), producing false verification results. Read flat, fully-paginated GETs and take
|
|
88
|
+
> counts from `meta.totalRecordCount` — never from a single nested response. See the gotcha in
|
|
89
|
+
> [App_Api_Toga2](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md).
|
|
90
|
+
|
|
91
|
+
## Change history
|
|
92
|
+
- 2026-08-05 — BUILT the worker2 NYCHH asset-tag backfill (action
|
|
93
|
+
`Client/Nychh/AssetTagBackfill/Backfill`, cron `Core.CronJobs` id 44) — first NYCHH worker2
|
|
94
|
+
presence. Reads serials, queries NetSuite `inventorynumber` in 500-chunks by native serial
|
|
95
|
+
column (custom tag/MAC columns are SELECT-only, un-filterable), never-blank direct-DB UPDATEs of
|
|
96
|
+
`Units.assetTag`/`macAddress` (no api2, avoids the beta worker2→api2 crash; pattern from
|
|
97
|
+
`_Worker_Client_Prudential_AssetImports`). Added the 1.0 verification + full-history backfill
|
|
98
|
+
companion scripts. (bala)
|
|
99
|
+
</content>
|
package/knowledge/INDEX.md
CHANGED
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 46 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
|
-
- **worker2** (Worker) —
|
|
22
|
+
- **worker2** (Worker) — 40 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 22 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 5 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 5 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -7,12 +7,15 @@ apps:
|
|
|
7
7
|
- toga2-supply
|
|
8
8
|
- toga25-supply
|
|
9
9
|
- dbchanges2
|
|
10
|
+
- worker2
|
|
11
|
+
- worker
|
|
12
|
+
- library
|
|
10
13
|
project: _Underscore
|
|
11
14
|
client: nychh
|
|
12
15
|
type: profile
|
|
13
16
|
status: active
|
|
14
|
-
updated: 2026-
|
|
15
|
-
owners: ["jcardinal", "apeterson"]
|
|
17
|
+
updated: 2026-08-05
|
|
18
|
+
owners: ["jcardinal", "apeterson", "bala"]
|
|
16
19
|
files: []
|
|
17
20
|
related:
|
|
18
21
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
@@ -26,6 +29,8 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
26
29
|
## Platforms & data
|
|
27
30
|
- **2.0:** `_underscore` backend, prod schema `Client_Nychh`. Item fulfillments, units, and
|
|
28
31
|
tracking numbers follow the shared tracking-number bridge model.
|
|
32
|
+
- **Worker tiers:** `worker2` (2.0) runs the NYCHH asset-tag backfill; `worker`/`library` (1.0)
|
|
33
|
+
run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
|
|
29
34
|
|
|
30
35
|
## Key features (this client)
|
|
31
36
|
- Item-fulfillment table views (`item-fulfillments-for-sales-orders`,
|
|
@@ -33,6 +38,10 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
33
38
|
re-rooted at `ItemFulfillmentItems` with item-level (record 318) tracking. See
|
|
34
39
|
[Tracking-Number Bridge Migration](../../2.0/apps/_underscore/features/tracking-number-bridges.md)
|
|
35
40
|
(Client variations + the TableView portability gotcha).
|
|
41
|
+
- **Asset-tag backfill** (worker2, first NYCHH worker2 presence) — keeps `Units.assetTag`/
|
|
42
|
+
`macAddress` synced from the NetSuite serial (inventory-number) record, closing the gap where a
|
|
43
|
+
tag added after the ItemShip never bumps the fulfillment sync. See
|
|
44
|
+
[NYCHH Asset-Tag Backfill](../../2.0/apps/worker2/features/nychh-asset-tag-backfill.md).
|
|
36
45
|
|
|
37
46
|
## Notes
|
|
38
47
|
- Tracking data: record 318 (item-level) is currently empty for this client; their tracking
|
|
@@ -35,13 +35,19 @@ Richmond University Medical Center (**RUMCSI**) is a framework **1.0** client se
|
|
|
35
35
|
- **TOGa View SAML.** `togaview/_/app/framework.php` has a RUMCSI-specific host branch that selects
|
|
36
36
|
branding (`$_SESSION['stylePath']`) and the SAML login path — an unregistered togaview hostname
|
|
37
37
|
falls through to default branding and skips SAML.
|
|
38
|
-
- **⚠ Portal login
|
|
39
|
-
handler never populates `$_SESSION
|
|
40
|
-
`_/app/framework.php` block that authenticates on nothing but a base64-encoded email in
|
|
41
|
-
`?hash
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
- **⚠ Portal login IS an unauthenticated `?hash=` magic link — and it is load-bearing.** The
|
|
39
|
+
rumcsi SAML `acs` handler never populates `$_SESSION`; the only working rumcsi session builder
|
|
40
|
+
is a `_/app/framework.php` block that authenticates on nothing but a base64-encoded email in
|
|
41
|
+
`?hash=` (no signature, nonce, or expiry). The routing evidence is firm: `framework.php:278`,
|
|
42
|
+
inside that block, is the **only** rumcsi route to `/enterprise_dashboard`, which is exactly
|
|
43
|
+
where a real production SSO login landed
|
|
44
|
+
(`rumcsi.togaview.agilantsolutions.com/enterprise_dashboard`), and togaview has **no** `?saml=`
|
|
45
|
+
handling, so the 2.0 SAML gateway is not involved. **Deleting the block breaks RUMC SSO into an
|
|
46
|
+
IdP redirect loop** — it must be signed or replaced by a completed `acs` handler, in
|
|
47
|
+
coordination with whoever generates the link (the generator is external to our repos and still
|
|
48
|
+
unidentified). Full analysis, both fix shapes, and the outstanding cold-login confirmation test:
|
|
49
|
+
[TOGa View login
|
|
50
|
+
flows](../../1.0/apps/togaview/features/login-flows.md#open-security-item--rumcsi-hash-is-the-live-production-sso-login-do-not-delete-it).
|
|
45
51
|
Note also that the SAML branch is gated on `!inTestMode()`, so **test mode leaves rumcsi with
|
|
46
52
|
no login path**.
|
|
47
53
|
- **Hardcoded client-ID branching.** RUMCSI appears in TOGa Desk's hardcoded client-ID branching
|
|
@@ -49,6 +55,9 @@ Richmond University Medical Center (**RUMCSI**) is a framework **1.0** client se
|
|
|
49
55
|
|
|
50
56
|
## Change history
|
|
51
57
|
|
|
58
|
+
- 2026-08-05 (later) — routing evidence confirms the `?hash=` path is the live SSO login (only
|
|
59
|
+
route to `/enterprise_dashboard`; no `?saml=` handling in togaview); deleting it would break
|
|
60
|
+
RUMC login into a redirect loop. (mhammontree)
|
|
52
61
|
- 2026-08-05 — recorded the `?hash=` portal-login path as an open takeover risk left in place
|
|
53
62
|
pending SSO confirmation, and the test-mode no-login-path gap (TRUE-80671). (mhammontree)
|
|
54
63
|
- 2026-08-04 — Client profile created during the TRUE-80139 read-only review of the RUMCSI branded
|
package/package.json
CHANGED