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.
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +14 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +2 -1
- package/knowledge/2.0/apps/_underscore/features/carrier-shipping-labels.md +65 -2
- package/knowledge/2.0/apps/_underscore/features/netsuite-salesorder-address-phone-sync.md +102 -0
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md +47 -2
- package/knowledge/2.0/apps/toga2-supply/INDEX.md +2 -1
- package/knowledge/2.0/apps/toga2-supply/features/fulfill-and-ship.md +84 -9
- package/knowledge/2.0/apps/toga2-supply/workflows/onboarding-a-client-to-fulfill-and-ship.md +72 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/growrk/INDEX.md +1 -1
- package/knowledge/clients/growrk/features/units-netsuite-custom-fields.md +87 -9
- package/knowledge/clients/growrk/profile.md +14 -0
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +73 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
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-
|
|
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
|
-
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-
|
|
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
|
|
188
|
-
|
|
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).
|
|
191
|
-
|
|
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
|
-
-
|
|
536
|
-
|
|
537
|
-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
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) —
|
|
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
|
|
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
|
|
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-
|
|
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`
|
|
36
|
-
|
|
37
|
-
`
|
|
38
|
-
|
|
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.**
|
|
66
|
-
|
|
67
|
-
`
|
|
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-
|
|
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