toga-ai 1.0.554 → 1.0.555

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-05
10
- owners: ["dfranks", "bala", "jcardinal"]
9
+ updated: 2026-08-11
10
+ owners: ["dfranks", "bala", "jcardinal", "mhammontree"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
13
13
  - worker/crons/toga2/netsuite/sync_togasupply_canon.php
@@ -149,6 +149,14 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
149
149
 
150
150
  ## Gotchas / known issues
151
151
 
152
+ - **⚠ OPEN, UNTRACKED (found 2026-08-11): the GroWrk item-receipt sync has been failing every 5
153
+ minutes since 2026-08-06.** `Logs.Issue` **124** for `clientId` **33**, *"Invalid API Response"* from
154
+ `App_Api_Toga2::send()` (`library/app/api/toga2.php:3245`) via
155
+ `App_Api_Toga2::syncItemReceiptFromNetsuite`
156
+ (`worker/crons/toga2/netsuite/common_sync_togasupply.php:903`) from
157
+ `worker/crons/toga2/netsuite/sync_togasupply_growrk.php:42` — **288 events/day** on 2026-08-07/08/09.
158
+ This is the **1.0 stack** (`worker` + `library`) and was deliberately out of scope for TRUE-80824.
159
+ **Needs its own ticket.**
152
160
  - **Split history:** all per-client wrappers were carved out of a single `CLIENT_CONFIGURATION`
153
161
  array in `sync_togasupply.php` by commit **778a3f1f** (2026-04-17). The original 15-client
154
162
  array is archived at `worker/test/@dave/netsuite_togasupply_original_clients.php`.
@@ -237,6 +245,10 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
237
245
 
238
246
  ## Change history
239
247
 
248
+ - 2026-08-11 — TRUE-80824 (side finding, no code change): recorded an **open, untracked** failure —
249
+ GroWrk (`clientId` 33) `syncItemReceiptFromNetsuite` has raised "Invalid API Response" from
250
+ `App_Api_Toga2::send()` every 5 minutes since 2026-08-06 (`Logs.Issue` 124, ~288 events/day).
251
+ 1.0 stack, out of scope for that ticket; needs its own. (mhammontree)
240
252
  - 2026-08-05 — Documented that `syncItemFulfillmentFromNetsuite` is now a **full reconcile** like
241
253
  `syncSalesOrderFromNetsuite`: updates IF items/units + serial/`assetTag`/`macAddress`
242
254
  (never blanking), maintains **all three** tracking bridges (317/318/319), and **prunes**
@@ -10,7 +10,7 @@
10
10
  | [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
11
11
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
12
12
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
13
- | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
13
+ | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
14
14
  | [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
15
15
  | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
16
16
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Api/Oneuptime/Oneuptime.php, api2/Component/Api/Netsuite/Netsuite.php, _underscore/Component/Api/Paypal/Paypal.php |
@@ -27,6 +27,7 @@
27
27
  | [_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()`. | _underscore/Model/Core/Model.php, _underscore/Model.php, _underscore/Model/Rate/Subscription.php |
28
28
  | [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php, _underscore/Model/Rate/Subscription.php |
29
29
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
30
+ | [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | `_Trait_Netsuite_SalesOrder` is the **shared** sales-order importer composed into **22 client models** (every client on the dbchanges2 `netsuite` module). | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Model.php, dbchanges2/_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql |
30
31
  | [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>` — still the live path for `toga2-supply` and other pre-Surf | _underscore/Model/Core/Page.php |
31
32
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
32
33
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
@@ -6,9 +6,11 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-03
9
+ updated: 2026-08-11
10
10
  owners: [mhammontree]
11
11
  files:
12
+ - test/@Mark/true-80824-fedex-inflate-test.php
13
+ - dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql
12
14
  - _underscore/Model/Client/ItemFulfillment.php
13
15
  - _underscore/Model/Client/TrackingNumber.php
14
16
  - _underscore/Model/Client/ItemFulfillments/TrackingNumber.php
@@ -229,6 +231,49 @@ same type**.
229
231
  `TN` was chosen over `ON` (Dealer Order Number) precisely **because UPS prints the code's meaning** —
230
232
  "Dealer Order Number" would be misleading next to a sales-order number.
231
233
 
234
+ ## FedEx returns GZIP-ENCODED error bodies — `inflateIfCompressed()` (implemented 2026-08-11, TRUE-80824)
235
+
236
+ FedEx (or an edge/WAF in front of it) returned a **gzip-encoded HTTP 400**. `_ApiRequest` sets **no
237
+ `CURLOPT_ENCODING`**, so curl hands back **compressed bytes**, `json_decode` returns `null`, and
238
+ `describeShipmentError()` fell through to its raw-body fallback — surfacing
239
+ `"FedEx returned HTTP 400 in an unrecognized format: \x1f\x8b\b…"`. Compounding it, the ship call runs
240
+ **`$api->setLogging(false)`** (deliberate — a successful ship response carries the full base64 label,
241
+ megabytes), so **there is NO outbound log row** to recover the body from. **This differs from UPS**,
242
+ whose real errors *are* in the client-logs `Api` table (`direction = OUT`, `hostname LIKE '%ups.com%'`).
243
+
244
+ `Fedex.php` gained a private **`inflateIfCompressed()`**, applied to **BOTH the error and the success
245
+ paths** (a compressed `200` would otherwise become "Invalid shipment response"):
246
+
247
+ 1. **gzip magic (`\x1f\x8b`) → `gzdecode()`**, falling back to `gzinflate(substr($payload, 10))` for a
248
+ truncated stream.
249
+ 2. Otherwise — **and ONLY when the payload is not valid UTF-8** — try `gzuncompress()` then
250
+ `gzinflate()`; **first non-empty result wins**.
251
+
252
+ **Three encodings need three functions:** gzip = `gzdecode`, **raw** deflate = `gzinflate`, **zlib**
253
+ deflate = `gzuncompress`. Both `gzdecode` and `gzinflate` return `false` on zlib-wrapped deflate —
254
+ which is what most servers actually send for `Content-Encoding: deflate`.
255
+
256
+ ### ⚠ THE UTF-8 GUARD IS LOAD-BEARING — DO NOT REMOVE IT
257
+
258
+ zlib and raw deflate **cannot be detected reliably** (zlib's CMF/FLG pair varies with window size; raw
259
+ deflate has **no header at all**), so they can only be *attempted* — and attempting them on a legible
260
+ body is **destructive**. Measured by **fuzzing 100,000 valid-UTF-8 bodies** (JSON error envelopes,
261
+ HTML error pages, short printable ASCII): **`gzinflate()` returned a non-false, non-empty result on
262
+ 257 of them (~1 in 390)**, turning one 64-byte JSON error into **403 bytes of binary junk**. So a
263
+ **"first non-empty result wins" rule does NOT make it safe.** Every such input is valid UTF-8 and
264
+ every garbage output is not, so a single **`preg_match('//u', $payload)`** check removes the entire
265
+ class of false positives. **CodeRabbit asked twice to remove this guard; it was rejected on this
266
+ evidence.**
267
+
268
+ **Regression coverage:** `test/@Mark/true-80824-fedex-inflate-test.php` — 18 cases, drives the private
269
+ method **by reflection against the real class file** (no DB, no network, no bootstrap; see the
270
+ [static no-DB regression harness](../../../1.0/apps/test/features/static-no-db-regression-harness.md)),
271
+ including **4 fuzz-discovered `fp:` cases whose only job is to fail if the UTF-8 guard is ever removed**.
272
+
273
+ **Still open (recommended, not done):** setting **`CURLOPT_ENCODING` in `_ApiRequest`** would fix this
274
+ class for **every** integration, but that is the framework's shared HTTP client and needs its own
275
+ review. **The UPS client has the same latent gap.**
276
+
232
277
  ## `FIELD_STORAGE` mechanics (reference)
233
278
 
234
279
  `Model.php` handles storage fields separately from regular columns (write loop ~line
@@ -301,7 +346,11 @@ still discards the unsaved in-memory value.
301
346
  - UPS `AddressLine` max 35 chars/line; send an array of trimmed lines (FedEx already
302
347
  does), not `line1 . ' ' . line2`.
303
348
  - `ShippingMethods.code` is carrier-scoped: UPS numeric (`03` Ground, `01` Next Day
304
- Air), FedEx strings (`FEDEX_GROUND`). Empty code → UPS error 120500.
349
+ Air), FedEx strings (`FEDEX_GROUND`). Empty code → UPS error 120500. **⚠ In production this column
350
+ is empty for most clients** (2026-08-11: Elite 0/6, Quad 0/12, Rate 0/4, Compass 1/23 — and that one
351
+ is an invalid `'GND'`; GroWrk ids 3/4 were fixed by
352
+ `Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql`). Because the TOGa Supply dropdown
353
+ renders `name`, not `code`, **the UI shows no signal** — check the data before debugging the code.
305
354
  - Serialized-inventory assignment fails ("Invalid issueinventorynumber reference key")
306
355
  when the lookup isn't location-scoped and the SO has no Warehouse Location —
307
356
  recommended: always scope `getInventoryNumberFromSerialNumber` by location.
@@ -367,6 +416,20 @@ still discards the unsaved in-memory value.
367
416
  bill) — expect a hard error rather than a silently-wrong bill in those cases.
368
417
 
369
418
  ## Change history
419
+ - 2026-08-11 — TRUE-80824: **made FedEx carrier rejection reasons readable.** FedEx returned a
420
+ **gzip-encoded HTTP 400** and `_ApiRequest` sets no `CURLOPT_ENCODING`, so `json_decode` saw
421
+ compressed bytes and `describeShipmentError()` printed `\x1f\x8b…`; with `setLogging(false)` on the
422
+ ship call there is no log row to recover the body from (unlike UPS, whose errors are in the
423
+ client-logs `Api` table). Added `inflateIfCompressed()` on **both** the error and success paths
424
+ (gzip magic → `gzdecode`, fallback `gzinflate(substr($payload,10))`; otherwise, **only when the
425
+ payload is not valid UTF-8**, `gzuncompress` then `gzinflate`, first non-empty wins). Documented that
426
+ the **UTF-8 guard is load-bearing**: fuzzing 100k valid-UTF-8 bodies had `gzinflate()` return
427
+ non-false, non-empty output on **257** of them (~1 in 390), so "first non-empty wins" alone is
428
+ unsafe — `preg_match('//u', …)` removes the class (CodeRabbit's twice-requested removal was rejected
429
+ on this evidence). Regression: `test/@Mark/true-80824-fedex-inflate-test.php`, 18 cases by reflection
430
+ incl. 4 fuzz-derived guard cases. Also recorded the production `ShippingMethods.code` data gap
431
+ across clients. **Open:** set `CURLOPT_ENCODING` in `_ApiRequest` for all integrations — the UPS
432
+ client has the same latent gap. (mhammontree)
370
433
  - 2026-08-03 — TRUE-80494: **wired Reference 1 / Reference 2 through to the carrier so they PRINT on
371
434
  the label** — `ShipmentRequest` gained `$referenceString2`, `upsShipmentApi`/`fedexShipmentApi`
372
435
  gained optional `$reference1`/`$reference2` (transient query-string params, **zero dbchanges2
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-11
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - _underscore/Trait/Netsuite/SalesOrder.php
13
+ - _underscore/Model.php
14
+ - dbchanges2/_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql
15
+ related:
16
+ - acl-permission-chain.md
17
+ - address-uniqueness-normalization.md
18
+ - carrier-shipping-labels.md
19
+ - ../../toga2-supply/features/fulfill-and-ship.md
20
+ - ../../api2/features/record-scripts.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `_Trait_Netsuite_SalesOrder` is the **shared** sales-order importer composed into **22 client models**
26
+ (every client on the dbchanges2 `netsuite` module). It writes the sales order, its ship-to
27
+ `Addresses` record, and the linked purchase order. This doc records where each user-visible value
28
+ comes from and the four traps that make a change here risky, because **anything changed in this trait
29
+ changes behaviour for all 22 clients at once**.
30
+
31
+ ## Where the values come from (confirmed against production, 2026-08-11)
32
+
33
+ | Value | Source |
34
+ |---|---|
35
+ | Sales order # (TOGa Supply "Reference 1") | `SalesOrders.number` |
36
+ | Purchase order # ("Reference 2") | NetSuite **`otherRefNum`**, stored as a **`PurchaseOrders` record linked through the `PurchaseOrders_SalesOrders` bridge** |
37
+ | Ship-to phone | the NetSuite **shipping address's `addrPhone`** → `Addresses.phoneNumber` |
38
+
39
+ **⚠ Reference 2 is NOT `SalesOrders.customerPurchaseOrder`.** That column is only ~**59%** populated
40
+ (and was NULL on the order tested), while the **bridge covers 12,643 of 12,658** orders. Resolve the
41
+ PO through the bridge, not the column.
42
+
43
+ ## ⚠ Four traps when writing a field on this shared trait
44
+
45
+ 1. **Normalise a phone with `_String::cleanPhoneNumber()` on the way INTO the payload.**
46
+ `Addresses.phoneNumber` is **`FIELD_CHAR_PHONE`**, and `_Model` runs **every write** through that
47
+ same function (`Model.php` ~584), reformatting a 10-digit number to `"(XXX) XXX-XXXX"`. Sending
48
+ NetSuite's raw string therefore **stores a different value than was sent**, making the
49
+ change-detection comparison **permanently unequal** and re-`PUT`ting the address on **every sync,
50
+ for every client on the trait**. Verified idempotent/stable across raw, dashed, dotted, leading-1,
51
+ extension, short and empty inputs.
52
+ 2. **Add the field to the change-detection `if` comparison, not only to the payload.** An existing
53
+ address otherwise takes the **"no changes necessary"** branch and the value is never written —
54
+ which makes the fix look like a **no-op**.
55
+ 3. **`$api->internalApiRequest()` runs on the SAME authenticated session as the outer call, so ACL
56
+ applies** — and the NetSuite sync authenticates as **`roleId` 3 ("API"**; roles are 1 = Base,
57
+ 2 = Developer, 3 = API, 4 = Public). `Addresses.phoneNumber` is **`Core.RecordFields` id 990** and
58
+ had **no role-3 grant on 20 of the 22** netsuite-module clients (Compass and Prudential already had
59
+ it). Without the grant you get either an **EV-9 write failure** or a **silently dropped field** —
60
+ and the silent case recreates the infinite address re-`PUT` from trap 1. Hence a **MODULE-scoped**
61
+ migration: `_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql`.
62
+ 4. **The ACL migration MUST be applied BEFORE the `_underscore` deploy.** Deploying the code first
63
+ makes the sync write `Addresses.phoneNumber` as the API role with no grant — EV-9 failures or an
64
+ address re-`PUT` on every sync across all 22 clients. (Note the usual *backend-first* rule for the
65
+ frontend did **not** apply here: no new RecordScript parameter was added, so toga2-supply had no
66
+ ordering constraint.)
67
+
68
+ ## Data reality — phones are absent, not wrong
69
+
70
+ Before this change, **only 1 of 12,504** GroWrk ship-to addresses on a sales order had a phone number.
71
+ The few populated addresses elsewhere hold junk (`1234567890`, `(123) 456-7890`), and some **addressee**
72
+ fields have phone numbers jammed into them.
73
+
74
+ **Decision (Mark): no backfill.** The phone only appears on sales orders that **re-sync from NetSuite
75
+ after deploy**, so an existing order will never show one — **a blank phone is not evidence of a failed
76
+ deploy.** Expect a **one-time burst of address `PUT`s** as phones converge on the first sync; that is
77
+ intended, not a fault.
78
+
79
+ ## Gotchas / known issues
80
+
81
+ - **Read grants were already in place** for the values this change consumes — `sales-orders.number`
82
+ (`RecordFields` 49), `purchase-orders.number` (87) and the bridge (1891/1892) on roles **1 and 3** —
83
+ so no migration was needed for the reads. Always check per-role grants for **both** directions
84
+ separately; a read grant is not a write grant.
85
+ - **A `FIELD_CHAR_PHONE` (or any normalising field type) breaks naive change detection.** Compare the
86
+ **normalised** form on both sides, or the record is rewritten forever. This applies to any 2.0 field
87
+ whose type transforms the value on write.
88
+ - **22 clients share this trait.** Grep the `use _Trait_Netsuite_SalesOrder` composition list before
89
+ assuming a change is client-scoped; a "small GroWrk fix" here is a platform change.
90
+
91
+ ## Change history
92
+
93
+ - 2026-08-11 — TRUE-80824: created. Prefilled TOGa Supply's Reference 1 / Reference 2 / Phone # from
94
+ NetSuite by sourcing `SalesOrders.number`, the bridge-linked purchase order (`otherRefNum` — **not**
95
+ `customerPurchaseOrder`, ~59% populated vs the bridge's 12,643/12,658), and the shipping address's
96
+ `addrPhone`. Recorded the four traps: `cleanPhoneNumber()` normalisation on the way in (because
97
+ `FIELD_CHAR_PHONE` reformats on write and would otherwise re-`PUT` every address on every sync for
98
+ all 22 clients), adding the field to the change-detection comparison as well as the payload,
99
+ `internalApiRequest()` inheriting the caller's session so the sync's **roleId 3 ("API")** needs an
100
+ explicit grant on `Core.RecordFields` 990 (missing on 20 of 22 clients → module-scoped migration
101
+ `_modules/netsuite/2026-08-10a`), and the hard **migration-before-deploy** ordering constraint.
102
+ Decision: **no backfill** — a blank phone on an old order is expected. (mhammontree)
@@ -5,6 +5,6 @@
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
6
  | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
8
- | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
8
+ | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
9
9
  | [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
10
10
  | [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini |
@@ -6,9 +6,12 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-10
10
- owners: ["bala"]
9
+ updated: 2026-08-11
10
+ owners: ["bala", "mhammontree"]
11
11
  files:
12
+ - dbchanges2/Client_Growrk/2026-05-28.sql
13
+ - dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql
14
+ - dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql
12
15
  - dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql
13
16
  - dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql
14
17
  - dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql
@@ -38,6 +41,36 @@ Written from repairing `Client_Growrk.Units` (missing `c_lastModified`, 6 of 7 c
38
41
  metadata rows applied) — see
39
42
  [GroWrk Units custom fields](../../../../clients/growrk/features/units-netsuite-custom-fields.md).
40
43
 
44
+ ## First question: was the file PARTIALLY applied, or NEVER applied? (corrected 2026-08-11)
45
+
46
+ The `Client_Growrk` incidents were originally diagnosed as a **partially applied** module migration.
47
+ **That was wrong.** The real cause was **`Client_Growrk/2026-05-28.sql`, committed on `_main`
48
+ (`af30e63`) but NEVER EXECUTED against production** — one skipped commit that explained **two**
49
+ separate 500 incidents (`Units.c_lastModified` on 2026-08-10 and
50
+ `ServiceRequests.c_externalReferenceId` on 2026-08-11).
51
+
52
+ > **"Committed to `_main`" is not "applied to production."** Establish which of the two you are looking
53
+ > at **before** writing a catch-up file, because the repair scope is completely different.
54
+
55
+ **Cheapest discriminator — count the file's fixed UUID literals in the client DB:**
56
+
57
+ - **zero present → the file never ran.** Then audit **every object the file touches**, not just the
58
+ column in the stack trace. In the GroWrk case that meant `ServiceRequests` `c_` columns 0 of 2,
59
+ `CustomRecordFields` recordId 35 0 of 2, `ServiceRequestTypes` 2 of 19, `UnitDispositions` 1 of 5,
60
+ and two **whole missing objects** from sibling files in the same commit (`Core.Records` 315 and
61
+ `Client_Growrk.UnitTypes`).
62
+ - **some present → genuinely partial.** Repair statement by statement.
63
+
64
+ **Rule out an executor date-window gap explicitly** by checking that files with **both earlier and
65
+ later** dates in the same folder did apply (2026-06-17 and 2026-07-31 both had, straddling the skipped
66
+ 2026-05-28) — otherwise you will "fix" the wrong thing.
67
+
68
+ **Check the sibling files in the same git commit.** A skipped commit usually drops **every** file it
69
+ carried, across `Core/`, `Client/` and `Client_<Name>/` folders.
70
+
71
+ **Fix production AND beta.** A hand-fix applied to production only leaves beta still drifted, so the
72
+ catch-up migration is a **prerequisite for testing on beta** even though production already works.
73
+
41
74
  ## Why it 500s: `_Model` ignores the caller's `fields=`
42
75
 
43
76
  `_Model` builds its SELECT from **every declared property on the class and its traits**, not from
@@ -122,6 +155,18 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
122
155
 
123
156
  ## Change history
124
157
 
158
+ - 2026-08-11 — TRUE-80824: **corrected the premise.** The `Client_Growrk` drift was **not** a partially
159
+ applied module migration — `Client_Growrk/2026-05-28.sql` is committed on `_main` (`af30e63`) and was
160
+ **never executed** against production, which explains both the 2026-08-10 `Units` 500s and the
161
+ 2026-08-11 `ServiceRequests.c_externalReferenceId` Fulfill & Ship 500s. Added the
162
+ **partial-vs-never-applied discriminator** (count the file's fixed UUID literals: zero present = never
163
+ ran, so audit every object the file touches, plus the sibling files in the same git commit), the
164
+ requirement to **rule out an executor date-window gap** by confirming earlier and later files applied,
165
+ and the note that a production-only hand-fix leaves **beta** drifted so the catch-up file is a
166
+ prerequisite for beta testing. Also recorded the API-diagnostics traps found on the way: `error.id`
167
+ `95-24` = `Logs.Issue.issueNumber`/`Logs.Event.eventNumber` (not Issue `id`), and
168
+ `Logs.Api.transactionId` stores a **timestamp**, so a request cannot be found by the envelope's
169
+ transactionId. (mhammontree)
125
170
  - 2026-08-10 — Created from the production `Client_Growrk.Units` incident: `_Trait_Netsuite_Unit`
126
171
  declared `c_lastModified` but the column had never been added, so **every** traversal reaching a
127
172
  Growrk Unit 500'd (`EO-1`, MySQL 1054) regardless of the requested `fields=`. Recorded the
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
6
- | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | _underscore/Model/Client/Measure.php, _underscore/Model/Client/TrackingNumber.php, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts, toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/components/ui/Tables/types.ts, toga2-supply/src/components/ui/GoogleMapsLink.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
6
+ | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | _underscore/Trait/Netsuite/SalesOrder.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, _underscore/Model/Client/Measure.php, _underscore/Model/Client/TrackingNumber.php, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts, toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/components/ui/Tables/types.ts, toga2-supply/src/components/ui/GoogleMapsLink.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
7
7
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-supply` (React + Vite) builds and deploys on **AWS Amplify**. | toga2-supply/amplify.yml, toga2-supply/.gitattributes, toga2-supply/.github/workflows/sync-stage-environments.yml, toga2-supply/.env.qc-security |
8
8
  | [Onboarding a client to the supply2 frontend (host-scoping)](workflows/client-host-scoping.md) | How a new tenant becomes a "client" in the `toga2-supply` frontend. | toga2-supply/src/stores/useHostNameStore.ts, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/hooks/useHostName.tsx, toga2-supply/src/hooks/usePageDetails.tsx, toga2-supply/src/hooks/usePageListDetails.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/src/utils/handleClientAuthentication.ts, toga2-supply/src/App.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/api/toga.ts, toga2-supply/package.json |
9
+ | [Onboarding a client to TOGa Supply Fulfill & Ship](workflows/onboarding-a-client-to-fulfill-and-ship.md) | **Onboarding a client to Fulfill & Ship is CONFIGURATION, not development** — unless the client wants tracking pushed into their own system. | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Trait/Netsuite/ItemFulfillment.php, dbchanges2/_modules/netsuite/ |
@@ -6,9 +6,12 @@ project: TOGa Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-03
9
+ updated: 2026-08-11
10
10
  owners: [mhammontree]
11
11
  files:
12
+ - _underscore/Trait/Netsuite/SalesOrder.php
13
+ - dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql
14
+ - toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx
12
15
  - _underscore/Model/Client/Measure.php
13
16
  - _underscore/Model/Client/TrackingNumber.php
14
17
  - toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts
@@ -55,6 +58,8 @@ related:
55
58
  - ../architecture.md
56
59
  - ../../_underscore/features/carrier-shipping-labels.md
57
60
  - ../../_underscore/features/recursive-item-fulfillments.md
61
+ - ../../_underscore/features/netsuite-salesorder-address-phone-sync.md
62
+ - ../workflows/onboarding-a-client-to-fulfill-and-ship.md
58
63
  ---
59
64
 
60
65
  ## Summary
@@ -184,12 +189,38 @@ bare string** — see the gotcha below before wiring a new select field.
184
189
  `renderEditShipmentFormInput` rendering it via `BaseDisabledInput` on an `isReadOnly` flag. **Not
185
190
  persisted to `IF.locationId`** for now — persisting would be a separate backend "set from SO"
186
191
  change plus a NetSuite-mapping check.
187
- - **Reference 1 / Reference 2 are WIRED (2026-07-31, TRUE-80494) — user-entered, NOT persisted.**
188
- Definitions confirmed: **Reference 1 = NetSuite Sales Order #, Reference 2 = NetSuite Purchase
192
+ - **Reference 1 / Reference 2 / Phone # are now PREFILLED from NetSuite (2026-08-11, TRUE-80824) —
193
+ still user-overridable, still NOT persisted.** **This supersedes the earlier SME (Skyler) note
194
+ that they are "user-entered, NOT prefilled from NetSuite"** — that half is no longer true; the
195
+ "not persisted" half still is. Sources, confirmed against production data:
196
+ **Reference 1 = `SalesOrders.number`**; **Reference 2 = NetSuite's `otherRefNum`**, which the SO
197
+ sync stores as a **`PurchaseOrder` reached through the `PurchaseOrders_SalesOrders` bridge** —
198
+ **NOT `SalesOrders.customerPurchaseOrder`** (only ~59% populated and NULL on the order tested,
199
+ while the bridge covers **12,643 of 12,658** orders); **Phone # = the NetSuite shipping address's
200
+ `addrPhone`** (see the
201
+ [NetSuite SO address/phone sync doc](../../_underscore/features/netsuite-salesorder-address-phone-sync.md)).
202
+ Frontend rules learned wiring the prefill:
203
+ - `phoneNumber` was set **only** by the edit-mode rehydrate and was **missing from
204
+ `defaultValues`** (create mode always opened blank even when the address had a phone) **and
205
+ missing from the `ShipmentData` type**, despite being assigned on the edit path and read by
206
+ `validateFormOnSubmit`.
207
+ - **`reset()` in edit mode replaces the WHOLE form state** and omitted `reference1`/`reference2`,
208
+ which blanked the prefill on every edit-mode load. Anything prefilled must be re-supplied in
209
+ every `reset()` call.
210
+ - **Carrier references are truncated to 35 chars in THREE places** — at defaults, at the edit-mode
211
+ `reset()`, and again immediately before `fulfillShipment` — because a **programmatic prefill
212
+ bypasses the field config's `characterLimit`** (a 36-char GroWrk PO was observed).
213
+ - The rehydrate effect's dep array was deliberately **left as `[editShipment]`**: adding the
214
+ reference values would re-run `reset()` and **discard a user's in-progress edits**.
215
+ - **No ACL migration was needed for the reads** — grants already exist for `sales-orders.number`
216
+ (`RecordFields` 49), `purchase-orders.number` (87) and the bridge (1891/1892) on roles 1 and 3.
217
+ - **A blank phone on an OLD order is not a failed deploy** (decision, Mark: **no backfill**) — the
218
+ phone only appears on sales orders that **re-sync from NetSuite after deploy**.
219
+
220
+ Definitions (unchanged): **Reference 1 = NetSuite Sales Order #, Reference 2 = NetSuite Purchase
189
221
  Order #** (the field-config placeholders were authoritative over the initial verbal description,
190
- and testing confirmed it — `281144` = `SalesOrders.number` — entered into Reference 1). Per SME
191
- (Skyler) the values are **user-entered, NOT prefilled from NetSuite, and NOT persisted**; their
192
- **sole purpose is to reach the carrier and print on the label**. Implemented as **transient
222
+ and testing confirmed it — `281144` = `SalesOrders.number` — entered into Reference 1). Their
223
+ **purpose is to reach the carrier and print on the label**. Implemented as **transient
193
224
  query-string params** on the existing carrier RecordScript calls (`fulfillShipment` in
194
225
  `UpdateShipmentApi.ts` → `upsShipmentApi`/`fedexShipmentApi`) — **no defaultValues, no save
195
226
  payload, no DB columns, zero `dbchanges2` work**. Form enforces `characterLimit: 35` (carrier
@@ -532,9 +563,37 @@ the call 403s and returns no label.
532
563
  - **UPS address limits**: `AddressLine` max 35 chars/line; code currently sends
533
564
  `line1 + ' ' + line2` as one line — should send a trimmed array (FedEx path already
534
565
  does).
535
- - **Service codes are carrier-scoped**: UPS numeric (`03`=Ground, `01`=Next Day Air),
536
- FedEx strings (`FEDEX_GROUND`), in `ShippingMethods.code`. Empty code → UPS 120500;
537
- there is now a fast-fail guard.
566
+ - **⚠⚠ `ShippingMethods.code` is EMPTY in production for most clients — and the UI gives the user no
567
+ signal.** Service codes are carrier-scoped: UPS numeric (`03`=Ground, `01`=Next Day Air), FedEx
568
+ strings (`FEDEX_GROUND`). The chain is `ShippingMethods.code` → `shippingMethodCode`
569
+ (`Model/Client/ItemFulfillment.php`) → `$shipmentRequest->service` → UPS `'Service' => ['Code']`
570
+ (`Component/Library/Carriers/Ups/Ups.php`); an empty string → **UPS 120500 "missing or invalid
571
+ service code"**. **The dropdown renders `name`, not `code`, so the form looks completely normal** —
572
+ a warehouse user cannot tell a method is unusable. Production state found 2026-08-11: **GroWrk
573
+ ids 3 and 4 were NULL and were fixed** (`Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql`);
574
+ **Elite 0 of 6** methods coded, **Quad 0 of 12**, **Rate 0 of 4**, **Compass 1 of 23** — and that one
575
+ is `'GND'`, **not a valid UPS REST code**. **Verify `ShippingMethods.code` per client before
576
+ believing a shipping failure is a code bug.**
577
+ - **⚠ GroWrk `ShippingMethods` ids 11/12 ("Overnight Standard" / "Overnight Priority") are still
578
+ NULL — FedEx product names sitting on UPS rows**, duplicating Next Day Air. Left NULL pending a
579
+ data-owner decision (assign real codes or delete the rows); until then, selecting either fails.
580
+ - **⚠ Duplicate `SalesOrders` rows per NetSuite SO id (historic, `Client_Growrk`) break fulfillment
581
+ and make the form's address arbitrary.** Production 2026-08-11: **12,658 rows for 7,776 distinct
582
+ `c_netsuiteInternalSalesOrderId`**, **2,787 ids duplicated (36%)**, worst case **139 rows sharing
583
+ one id and one `number`**. **Historic, not ongoing** — since 2026-08-01 it is 111 rows / 111
584
+ distinct ids. Two consequences: (a) `POST /item-fulfillments` resolves `salesOrder` by
585
+ `c_netsuiteInternalSalesOrderId` and needs **exactly one** match, so a duplicated order fails
586
+ **EV-12**; (b) `getAddress()` returns **all** matches and `EditShipmentPage` takes
587
+ **`salesOrders[0]`**, so which ship-to address and PO the form shows is **arbitrary** (beta SO
588
+ 7221433 has 2 rows with **different `shipToAddressId`** and different `PurchaseOrder` rows).
589
+ **Needs its own ticket:** data dedupe **plus** a decision on whether the code should fail loudly
590
+ instead of silently taking `[0]`.
591
+ - **⚠ The reprint scripted route is NOT registered in production `Core.RecordScripts` — reprint is
592
+ broken for EVERY client.** This doc (and the carrier doc) describe reprint as
593
+ `GET /v2/tracking-numbers/reprint` → `_Model_Client_TrackingNumber::reprintLabelsApi`, but
594
+ production `Core.RecordScripts` has **no `tracking-numbers` rows at all**; the only label-related
595
+ script is `generateReturnLabel` (id **26**, `item-fulfillments`). Discovered 2026-08-11, **not
596
+ fixed — needs its own ticket** (register the RecordScript + its `AclRecordScripts` grants).
538
597
  - **Serialized inventory needs a location**: a SO with no Warehouse Location makes
539
598
  NetSuite reject the serial issue ("Invalid issueinventorynumber reference key").
540
599
  Recommended hardening: always scope `getInventoryNumberFromSerialNumber` by location.
@@ -761,6 +820,22 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
761
820
  for Compass and is not yet in prod.
762
821
 
763
822
  ## Change history
823
+ - 2026-08-11 — TRUE-80824: **Reference 1 / Reference 2 / Phone # are now PREFILLED from NetSuite**
824
+ (still user-overridable, still not persisted) — **correcting the earlier SME note that they are
825
+ never prefilled**. Confirmed sources against production: Ref1 = `SalesOrders.number`; Ref2 =
826
+ NetSuite `otherRefNum`, stored as a `PurchaseOrder` via the `PurchaseOrders_SalesOrders` bridge
827
+ (**not** `customerPurchaseOrder`, ~59% populated vs the bridge's 12,643/12,658); Phone = the NetSuite
828
+ shipping address's `addrPhone`. Frontend fixes: `phoneNumber` missing from `defaultValues` **and**
829
+ from the `ShipmentData` type; edit-mode `reset()` omitted `reference1`/`reference2` and blanked the
830
+ prefill; references truncated to 35 chars in three places because a programmatic prefill bypasses
831
+ the config `characterLimit`; rehydrate dep array deliberately kept `[editShipment]` so a user's
832
+ in-progress edits aren't discarded. Also recorded four production data findings: **`ShippingMethods.code`
833
+ is empty for most clients** (GroWrk ids 3/4 fixed; Elite 0/6, Quad 0/12, Rate 0/4, Compass 1/23 with
834
+ an invalid `'GND'`) and the dropdown shows `name` so the UI looks fine; GroWrk ids 11/12 are FedEx
835
+ names on UPS rows, left NULL pending a data-owner decision; **duplicate `SalesOrders` rows** per
836
+ NetSuite SO id (12,658/7,776; 36% duplicated; historic) causing EV-12 and an arbitrary
837
+ `salesOrders[0]` address; and the **reprint RecordScript is not registered in production**, so
838
+ reprint is broken for every client. The last two need their own tickets. (mhammontree)
764
839
  - 2026-08-03 — TRUE-80494 (hotfix + feature follow-up to TRUE-79191): **ended a TOTAL production
765
840
  Fulfill & Ship submit outage** (all clients, since the 2026-07-29 deploy) — the display-only
766
841
  "Address" section header (`valueKey: ""`, `isRequired: true`) always produced a validation error
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: Onboarding a client to TOGa Supply Fulfill & Ship
3
+ framework: "2.0"
4
+ repo: toga2-supply
5
+ project: TOGa Supply
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-11
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - _underscore/Trait/Netsuite/SalesOrder.php
13
+ - _underscore/Trait/Netsuite/ItemFulfillment.php
14
+ - dbchanges2/_modules/netsuite/
15
+ related:
16
+ - ../features/fulfill-and-ship.md
17
+ - ../../_underscore/features/carrier-shipping-labels.md
18
+ - ../../_underscore/features/acl-permission-chain.md
19
+ - ../../dbchanges2/workflows/client-onboarding.md
20
+ - ../../../1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ **Onboarding a client to Fulfill & Ship is CONFIGURATION, not development** — unless the client wants
26
+ tracking pushed into their own system. Derived from a **GroWrk-vs-Elite production diff** (2026-08-11);
27
+ the platform-wide pieces were already shared by both clients, and only two things are genuinely
28
+ per-client.
29
+
30
+ ## Already shared — nothing to do
31
+
32
+ These were **identical** between the working client (GroWrk) and a candidate (Elite):
33
+
34
+ - **Client models composing the NetSuite traits** — 22 clients have **both**
35
+ `_Trait_Netsuite_SalesOrder` and `_Trait_Netsuite_ItemFulfillment`.
36
+ - **NetSuite sync `Parameters`** — 16 keys.
37
+ - **`Measures`** — Inch / Pound seeded.
38
+ - **`AclRecordScripts` grants** — GroWrk and Elite hold **identical `recordScriptId` sets**
39
+ (1-19, 22, 26, 27).
40
+
41
+ ## Required PER CLIENT
42
+
43
+ 1. **`ShippingCarrierAccountNumbers`** — Elite has **0**. The Carrier dropdown only lists carriers
44
+ that have **at least one account number**, so with none the user sees an **empty list** with no
45
+ error.
46
+ 2. **`ShippingMethods.code` per carrier** — UPS numeric (`03` Ground, `01` Next Day Air), FedEx
47
+ strings (`FEDEX_GROUND`). **Empty → UPS 120500**, and the dropdown renders `name` not `code`, so
48
+ the form looks normal. In production this is empty for most clients — see the fulfill-and-ship
49
+ doc's service-code gotcha.
50
+
51
+ ## Required on the NetSuite side
52
+
53
+ The sales order needs a **warehouse `Location`**, or NetSuite rejects the serial with **"Invalid
54
+ issueinventorynumber reference key"** — and the serials must be **on hand at that location**.
55
+
56
+ ## Optional and code-bearing
57
+
58
+ A **per-client `postPost` webhook** via `ApiPayloadInterceptors` to push tracking into the client's own
59
+ system. This is **GroWrk-only today** and is the one part that is development, not configuration. (It
60
+ also means the client model gains custom `c_` fields — see
61
+ [client schema-drift audit](../../dbchanges2/workflows/client-schema-drift-audit.md), because the
62
+ interceptor is what makes a missing `c_` column fatal.)
63
+
64
+ ## Change history
65
+
66
+ - 2026-08-11 — TRUE-80824: created from a GroWrk-vs-Elite production diff while investigating whether a
67
+ second client could use Fulfill & Ship. Conclusion: onboarding is configuration — the traits,
68
+ NetSuite sync Parameters (16 keys), Inch/Pound Measures and `AclRecordScripts` sets (1-19, 22, 26, 27)
69
+ are already identical across clients; only **`ShippingCarrierAccountNumbers`** (Elite has 0 → empty
70
+ Carrier dropdown) and **`ShippingMethods.code`** are per-client, plus a NetSuite warehouse Location
71
+ with serials on hand. A per-client `postPost` tracking webhook (`ApiPayloadInterceptors`, GroWrk-only)
72
+ is the only code-bearing option. (mhammontree)
@@ -18,11 +18,11 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
 
19
19
  ## 2.0 framework
20
20
 
21
- - **_underscore** (_Underscore) _(framework core)_ — 51 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **_underscore** (_Underscore) _(framework core)_ — 52 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 46 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)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
- - **toga2-supply** (TOGa Supply) — 5 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
25
+ - **toga2-supply** (TOGa Supply) — 6 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
27
  - **toga2-view** (TOGa View Frontend) — 7 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
28
28
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
@@ -3,5 +3,5 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)](features/transfer-order-flow.md) | 2.0 | GroWrk moves inventory between their own locations (warehouses, distribution points). | _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemReceipt.php, _underscore/Model/Client/ItemReceiptItem.php, _underscore/Trait/Netsuite/TransferOrder.php, dbchanges2/Client_Growrk/, dbchanges2/Client/ |
6
- | [GroWrk Units — NetSuite-module custom fields (the c_lastModified 500 incident)](features/units-netsuite-custom-fields.md) | 2.0 | GroWrk is on the dbchanges2 **`netsuite`** module (`dbchanges2/Client_Growrk/_modules.txt`), so its Units carry the module's seven `c_` custom fields. | _underscore/Trait/Netsuite/Unit.php, _underscore/Model/Growrk/Unit.php, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
6
+ | [GroWrk NetSuite-module custom fields the never-executed 2026-05-28 commit (Units + ServiceRequests 500s)](features/units-netsuite-custom-fields.md) | 2.0 | GroWrk is on the dbchanges2 **`netsuite`** module (`dbchanges2/Client_Growrk/_modules.txt`), so its Units carry the module's seven `c_` custom fields. | _underscore/Trait/Netsuite/Unit.php, _underscore/Model/Growrk/Unit.php, _underscore/Model/Growrk/ItemFulfillment.php, dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/Client_Growrk/_modules.txt |
7
7
  | [GroWrk](profile.md) | 2.0 | GroWrk is a 2.0 client using TOGA for inventory and order management. | |
@@ -1,19 +1,23 @@
1
1
  ---
2
- title: "GroWrk Units — NetSuite-module custom fields (the c_lastModified 500 incident)"
2
+ title: "GroWrk NetSuite-module custom fields the never-executed 2026-05-28 commit (Units + ServiceRequests 500s)"
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: growrk
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-10
9
+ updated: 2026-08-11
10
10
  owners: ["bala", "mhammontree", "rgirish"]
11
11
  files:
12
12
  - _underscore/Trait/Netsuite/Unit.php
13
13
  - _underscore/Model/Growrk/Unit.php
14
+ - _underscore/Model/Growrk/ItemFulfillment.php
15
+ - dbchanges2/Client_Growrk/2026-05-28.sql
14
16
  - dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql
15
17
  - dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql
16
18
  - dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql
19
+ - dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql
20
+ - dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql
17
21
  - dbchanges2/Client_Growrk/_modules.txt
18
22
  related:
19
23
  - ../profile.md
@@ -32,12 +36,69 @@ Units carry the module's seven `c_` custom fields. `_Model_Growrk_Unit` is *only
32
36
  declared by the trait**, and each must exist as a physical column in `Client_Growrk.Units`, as a
33
37
  `CustomRecordFields` row (recordId **31** = Units), and as an `AclCustomFieldPermissions` grant.
34
38
 
35
- On **2026-08-10** production GroWrk 500'd on `GET /v2/item-fulfillments` because one of those
36
- columns `c_lastModified` had never been created: the module migration
37
- `_modules/netsuite/2026-07-10a - UnitInventoryFields.sql` had applied to `Client_Growrk` only
38
- **partially**. Two catch-up migrations fixed it in production; the general playbook lives in
39
+ On **2026-08-10** production GroWrk 500'd on `GET /v2/item-fulfillments` (missing `Units.c_lastModified`)
40
+ and on **2026-08-11** it 500'd again on POST/PUT `/v2/item-fulfillments` (missing
41
+ `ServiceRequests.c_externalReferenceId`). **Both incidents have ONE root cause** — see the corrected
42
+ root cause below. Catch-up migrations fixed each in production; the general playbook lives in
39
43
  [client schema-drift audit](../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md).
40
44
 
45
+ ## ⚠ CORRECTED ROOT CAUSE (2026-08-11) — one commit was never executed, nothing was "partially applied"
46
+
47
+ This doc originally attributed the 2026-08-10 Units 500 to a **partially applied**
48
+ `_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`. **That was wrong.** The real cause of both
49
+ incidents is that **`dbchanges2/Client_Growrk/2026-05-28.sql` is committed on `_main`
50
+ (`af30e63`, rgirish97, 2026-06-02) but was NEVER EXECUTED against production.**
51
+
52
+ - **Lines 26-31 of that file add the same five `Units` `c_` columns** that looked like a partial
53
+ module run. One skipped commit explains both incidents; there is no evidence of any file applying
54
+ half-way.
55
+ - All three files in that commit are absent from production — including
56
+ `Core/` and `Client/` **"2026-06-01 - UnitTypes.sql"**: `Core.Records` 315 and
57
+ `Client_Growrk.UnitTypes` **do not exist** in production.
58
+ - **It is NOT an executor date-window gap.** `2026-06-17` and `2026-07-31` files both applied,
59
+ straddling the skipped date.
60
+ - **Measured drift from the unexecuted file:** `ServiceRequests` `c_` columns **0 of 2**;
61
+ `CustomRecordFields` recordId 35 **0 of 2**; `ServiceRequestTypes` **2 of 19**;
62
+ `UnitDispositions` **1 of 5**; and **none of the file's 7 fixed UUIDs** present anywhere.
63
+
64
+ > **Durable lesson: "committed to `_main`" is not "applied to production."** Before theorising about a
65
+ > partial run, check whether the whole file ran at all — the cheapest proof is whether **any** of its
66
+ > fixed UUIDs exist in the client DB. Zero present = never executed; some present = truly partial.
67
+
68
+ ## The 2026-08-11 production outage — Fulfill & Ship EO-1 / HTTP 500
69
+
70
+ **Symptom.** GroWrk Fulfill & Ship returned **EO-1 / HTTP 500** on **POST and PUT
71
+ `/v2/item-fulfillments`** — both the *Save* and the *Fulfill & Ship* buttons.
72
+
73
+ **Mechanism.** `ServiceRequests` lacked `c_externalReferenceId`, so the raw query in
74
+ `_Model_Growrk_ItemFulfillment::sendGrowrkTrackingInfo()` (called from `postPost`) raised
75
+ **MySQL 1054**.
76
+
77
+ **Why it surfaced on that date, not in May.** Two things had to line up:
78
+ 1. the `postPost`/`postPut` **`ApiPayloadInterceptors`** (recordId **28**, both `isActive = 1`) landed
79
+ via `Client_Growrk/2026-07-31 - GrowrkItemFulfillmentTrackingWebhookInterceptors.sql`; and
80
+ 2. the **2026-08-05 NetSuite onboarding** switched GroWrk onto the `_Model_Growrk_*` classes, which
81
+ declare the `c_` fields.
82
+
83
+ **Remediation (production):** `Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql`
84
+ (the 2 `ServiceRequests` `c_` columns + their recordId-35 `CustomRecordFields` rows and grants) and
85
+ `2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql` (the missing `ServiceRequestTypes` /
86
+ `UnitDispositions` seeds), both reusing the unexecuted file's fixed UUIDs.
87
+
88
+ **No orphaned data resulted.** The exception rolls the insert back — `ItemFulfillments` `MAX(id)`
89
+ stayed at **7645** across ~36 attempts; only auto-increment was burned.
90
+
91
+ ### Diagnostic recipes proven on this incident
92
+
93
+ - **The API error id format `95-24` is `Logs.Issue.issueNumber` 95 / `Logs.Event.eventNumber` 24**
94
+ (that Issue's `id` is **166**) — **not** Issue id 95. Looking up id 95 finds an unrelated issue.
95
+ - **`Logs.Api.transactionId` stores a TIMESTAMP, not the API transactionId.** You **cannot** find a
96
+ request by the `transactionId` in the JSON envelope — pivot on `error.id` instead.
97
+ - **6 near-identical hits in ~12s sharing one transactionId is apiproxy's cross-region 5xx retry**,
98
+ not 6 user actions.
99
+ - **Beta was still drifted** after the production hand-fix, because that fix went to production only —
100
+ so `2026-08-10c` is a prerequisite for testing the flow on beta.
101
+
41
102
  ## The seven fields (all from `_Trait_Netsuite_Unit`)
42
103
 
43
104
  `c_netsuiteInternalInventoryAssignmentId` (integer), `c_itemDescription`, `c_lastModified`,
@@ -62,9 +123,11 @@ first-seen timestamp of 18:03:22 exactly. Before onboarding, Units resolved thro
62
123
  `_Model_Client_Unit` (no `c_` fields at all), which is why the same request shape had **1,613
63
124
  successful 200s** right up to 2026-08-05 15:37:51.
64
125
 
65
- **Extent of the drift.** From `_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`:
66
- **6 of its 7 Units columns** landed (`c_lastModified` did not) and **5 of its 6
67
- `CustomRecordFields` rows** never inserted.
126
+ **Extent of the drift.** 6 of the 7 `Units` columns were present (`c_lastModified` was not) and 5 of
127
+ the 6 `CustomRecordFields` rows were missing. This was originally read as a partial run of
128
+ `_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`; **the corrected explanation is the
129
+ never-executed `Client_Growrk/2026-05-28.sql`** (see the corrected-root-cause section above), whose
130
+ lines 26-31 add those same five `Units` columns.
68
131
 
69
132
  ## Remediation (applied to production and verified)
70
133
 
@@ -107,6 +170,21 @@ further 500s. The `ALTER` was an **instant** operation (MySQL 8.0.39, 5,415 `Uni
107
170
 
108
171
  ## Change history
109
172
 
173
+ - 2026-08-11 — TRUE-80824: **Corrected this doc's root cause** and fixed a second production outage.
174
+ GroWrk Fulfill & Ship 500'd (`EO-1`) on POST **and** PUT `/v2/item-fulfillments` because
175
+ `ServiceRequests` lacked `c_externalReferenceId`, which
176
+ `_Model_Growrk_ItemFulfillment::sendGrowrkTrackingInfo()` (via `postPost`) selects — MySQL 1054.
177
+ The true cause of **both** this and the 2026-08-10 Units incident is that
178
+ `dbchanges2/Client_Growrk/2026-05-28.sql` is **committed on `_main` (`af30e63`) but was never
179
+ executed against production**; its lines 26-31 add the very five `Units` columns previously
180
+ attributed to a partially applied `_modules/netsuite/2026-07-10a`. Verified drift (0 of 2
181
+ `ServiceRequests` `c_` columns, 0 of 2 recordId-35 `CustomRecordFields`, 2 of 19
182
+ `ServiceRequestTypes`, 1 of 5 `UnitDispositions`, none of the file's 7 fixed UUIDs) and ruled out an
183
+ executor date-window gap (2026-06-17 and 2026-07-31 both applied). Shipped `2026-08-10c`/`2026-08-10d`
184
+ catch-ups reusing the fixed UUIDs. Recorded the diagnostics: `error.id` `95-24` =
185
+ `Issue.issueNumber`/`Event.eventNumber` (Issue id 166), `Logs.Api.transactionId` holds a **timestamp**
186
+ so the envelope transactionId is not searchable, and the exception rolled every insert back
187
+ (`ItemFulfillments` MAX(id) 7645 unchanged across ~36 attempts). (mhammontree)
110
188
  - 2026-08-10 — **Fixed** the production GroWrk 500s on `GET /v2/item-fulfillments`: `Units` was
111
189
  missing the `c_lastModified` column declared by `_Trait_Netsuite_Unit`, so `_Model`'s
112
190
  SELECT-all-declared-fields behaviour raised MySQL 1054 on **every** load of a Growrk Unit
@@ -46,3 +46,17 @@ module (`dbchanges2/Client_Growrk/_modules.txt`).
46
46
  **1054s (unknown column) on any Unit load, on any route**. This is not only a local-drift
47
47
  problem — it took production down for GroWrk on 2026-08-10; see
48
48
  [Units — NetSuite-module custom fields](features/units-netsuite-custom-fields.md).
49
+ - **⚠ `dbchanges2/Client_Growrk/2026-05-28.sql` (committed `af30e63`) was NEVER EXECUTED against
50
+ production**, which caused **two** separate production outages (Units 2026-08-10, ServiceRequests /
51
+ Fulfill & Ship 2026-08-11). Catch-ups `2026-08-10c`/`2026-08-10d` shipped; treat any other GroWrk
52
+ "missing column/metadata" report as possibly the same commit. See
53
+ [that doc](features/units-netsuite-custom-fields.md).
54
+ - **⚠ Historic duplicate `SalesOrders` rows:** 12,658 rows for 7,776 distinct
55
+ `c_netsuiteInternalSalesOrderId` (36% duplicated, worst case 139 rows on one id). Not ongoing since
56
+ 2026-08-01, but it fails `POST /item-fulfillments` with **EV-12** and makes the Fulfill & Ship
57
+ ship-to address arbitrary (`salesOrders[0]`). Needs its own ticket — see the
58
+ [Fulfill & Ship gotchas](../../2.0/apps/toga2-supply/features/fulfill-and-ship.md).
59
+ - **`ShippingMethods` ids 11/12 ("Overnight Standard"/"Overnight Priority") have NULL `code`** — FedEx
60
+ product names on UPS rows, duplicating Next Day Air. Selecting either fails at UPS (120500);
61
+ awaiting a data-owner decision. Ids 3/4 were coded by
62
+ `Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql`.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-06
9
+ updated: 2026-08-11
10
10
  owners: [mhammontree, sking]
11
11
  files:
12
12
  - worker/crons/sync/nycdoe/import_asn.php
@@ -207,6 +207,77 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
207
207
 
208
208
  ## Gotchas / known issues
209
209
 
210
+ - **⚠ OPEN BUG / NAMED PATTERN — "Lenovo Off-Layout ASN File" (silent column shift + unit
211
+ collapse).** Investigated 2026-08-11 (read-only; **no code fix exists**). Lenovo sends
212
+ NYCDOE ASNs in **two different column layouts**, and `library/app/asnprocessor/lenovo.php`
213
+ picks its column mapping by **column COUNT with no header-name validation** (`:110`,
214
+ `if ($colCount >= 30`), so the off-layout file is parsed with the wrong mapping and
215
+ silently mis-read.
216
+ - **The two layouts.** The normal daily feed `DOE-Lenovo-ASN-MM-DD-YYYY.CSV` (~2 MB) has
217
+ **30** columns and matches the code's mapping. A second file,
218
+ `DOE-Open-Lenovo-MM-DD-YYYY.csv` (**lowercase** `.csv`, ~45 KB), has **31** columns in a
219
+ **different order**: `Contact Email` moves from column 29 to column **6**, and a trailing
220
+ `Full Shipment` column is appended. Everything from `Street Address` onward is therefore
221
+ read **one column too early**. The "extra comma" repair heuristic at `:119-127` does not
222
+ catch this (and is unchanged since March 2026 — commits `3442ddcf`, `6956dfbf`,
223
+ `864cf073`, so this is not a regression).
224
+ - **The four wrong values** (`lenovo.php:129-156`): `serialNumber` ← `$cols[22]` lands on
225
+ **System Quantity**, a constant `"1"` on every row (the real serial ends up in
226
+ `assetTag`); `orderQuantity` ← `$cols[21]` lands on a blank `Quantity Ordered`;
227
+ `partNumber` ← `$cols[17]` lands on the long **Product Description** instead of the OEM
228
+ SKU.
229
+ - **Diagnostic fingerprint.** `AdvanceShippingNoticeItems.partNumber` holds a long product
230
+ description rather than a SKU, and the ASN header carries shifted junk — `productClass` =
231
+ a tracking number, `workPhone` = a person's name, `shippingCarrier` = a date,
232
+ `dateShipped` = `1969-12-31` (unparseable date). Blast-radius query on `legacy`:
233
+ `SELECT id, advanceShippingNoticeItemId, serialNumber, assetTag FROM
234
+ Core.AdvanceShippingNoticeUnits WHERE serialNumber = '1'`.
235
+ - **Why units vanish silently.** Stage 1's dedupe key is
236
+ `vendorId|purchaseOrder|DOE-partNumber|serialNumber` and
237
+ `legacy_import_asn.php:64-68` does `$notices[$dedupeKey] = $asnLine` — **last occurrence
238
+ wins**. With `serialNumber` a constant `"1"`, every line of a given part collapses to one
239
+ key, losing N−1 units. Confirmed: `DOE-Open-Lenovo-08-07-2026.csv` (87 data lines for PO
240
+ `S202648137`) produced exactly **3** `Core.AdvanceShippingNoticeQueue` rows
241
+ (`476108-476110`, one per part), all `status='completed'`, `retryCount=0`, **no error and
242
+ no alert email** — so the max-retry alert never fires. Net effect on ASN `26720`: item
243
+ `54671` (monitor, `qtyOrder` 31) had **zero** `AdvanceShippingNoticeUnits` rows.
244
+ - **Systemic, not a one-off.** A production query found **13 pre-existing** Units rows
245
+ across *other* ASNs with `serialNumber = '1'` and the real serial in `assetTag` — this has
246
+ been corrupting DOE data for months.
247
+ - **Secondary amplifier:** `Core.AdvanceShippingNoticeItems.partNumber` is `varchar(64)` and
248
+ `sql_mode` is **non-strict** on the legacy cluster, so a description-as-partNumber is
249
+ **silently truncated** at 64 chars. That breaks `legacy_process_asn_queue.php`'s
250
+ item/unit lookup keys (built from the truncated DB value, compared against the full
251
+ in-memory string), and two long descriptions sharing their first 64 characters collapse
252
+ into the same item.
253
+ - **Remediation is MANUAL data repair — there is no automated recovery.** Correct
254
+ `Core.AdvanceShippingNoticeItems.partNumber` on the affected items to the real NetSuite
255
+ OEM SKU (replacing the description the malformed file supplied); the pipeline then
256
+ proceeds normally. Done this way for ASN `26720` on 2026-08-11.
257
+ - **Recommended future code fixes (proposed, NOT approved, NOT implemented):** map columns
258
+ by **header name** and hard-reject an unknown layout with a loud alert; stop building the
259
+ dedupe key from a field that can be a constant; widen
260
+ `AdvanceShippingNoticeItems.partNumber`.
261
+ - **UNRESOLVED.** On ASN `26720`, items `54672`/`54673` carried 31 and 25 units with
262
+ **correct** serials in `serialNumber` and `assetTag` NULL — which the three collapsed
263
+ queue rows cannot account for. The run that created those units was **not identified**.
264
+ Still open; do not assume it is explained.
265
+ - **Ruled out:** the recent **Quad** ASN import work is unrelated — Quad lives under
266
+ `worker/crons/toga2/**` against the toga2 `AdvanceShippingNoticeItemUnits` schema, while
267
+ the DOE path is `Core.AdvanceShippingNoticeUnits` via `asnprocessor/lenovo.php`.
268
+
269
+ - **The "Outstanding Sales Orders - PO Reconciliation Required" email is FLEET-WIDE, not a
270
+ per-ASN alert** (`2_send_serials_to_netsuite.php:270-357`). It runs at the end of every
271
+ 5-minute run and re-queries **all** `AdvanceShippingNoticeItems` with
272
+ `netSuiteInternalSalesOrderId` set but `netSuiteInternalPurchaseOrderId IS NULL`, throttled
273
+ to one email per 60 minutes via a sentinel `AdvanceShippingNoticeQueue` row with
274
+ `dedupeKey = 'system|po_reconciliation_alert'`. Receiving it means *at least one* SO is
275
+ unreconciled — it does **not** identify which ASN, so do not attribute it to the ASN you
276
+ happen to be looking at. A benign cause is simply that NetSuite has not created the PO from
277
+ the SO yet (`:76-82` logs "No POs found for NetSuite SO Internal ID <id>"). The subject's
278
+ count is also **inflated**: its `SELECT DISTINCT` includes `customerPurchaseOrder`, so it
279
+ counts SO+customerPO pairs rather than distinct SOs.
280
+
210
281
  - **⚠ NAMED PATTERN — "Pre-ASN Manual Receipt Mismatch"** (the umbrella condition covering
211
282
  this doc's two SO/PO-stamp gotchas below plus a third confirmed instance). Search this
212
283
  title, or its informal name **"Shipment Received Notice"** (Mark's nickname — some vendors,
@@ -447,6 +518,7 @@ use the toga DB MCP + `Logs.API` instead of running prod code locally.
447
518
  of the consumer query; `php -l` every touched file.
448
519
 
449
520
  ## Change history
521
+ - 2026-08-11 — Investigated (read-only, **no code change**) ASN `26720` / PO `S202648137` never completing: root-caused the **"Lenovo Off-Layout ASN File"** bug — `asnprocessor/lenovo.php` maps columns by COUNT with no header validation, so the 31-column `DOE-Open-Lenovo-*.csv` shifts every field +1, making `serialNumber` the constant `"1"`, which collapses 87 source lines into 3 `completed` queue rows and loses units with no error/alert. Documented the diagnostic fingerprint, the 13-row systemic blast radius, the `varchar(64)` partNumber truncation amplifier, and the **manual** partNumber data repair as the only remediation. Also documented that the "Outstanding Sales Orders - PO Reconciliation Required" email is fleet-wide (and its count inflated), ruled out the Quad ASN work, and left the ASN `26720` items `54672`/`54673` correct-serial units as UNRESOLVED. (mhammontree)
450
522
  - 2026-08-06 — Named and cross-referenced the umbrella failure pattern **"Pre-ASN Manual
451
523
  Receipt Mismatch"** (aka "Shipment Received Notice") tying together the existing
452
524
  manually-edited-SO and cross-wired-stamp gotchas under one searchable title, and logged a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.554",
3
+ "version": "1.0.555",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",