toga-ai 1.0.729 → 1.0.731
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +22 -1
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +41 -0
- package/knowledge/clients/compass-usa/workflows/catalog-fold-reversal.md +14 -1
- package/knowledge/clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md +26 -5
- package/knowledge/clients/elite/features/netsuite-togasupply-sync.md +13 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [dfranks, jcardinal, kyalamarthi]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
@@ -169,6 +169,19 @@ Forecast2 tables already store.
|
|
|
169
169
|
cross-checking item id, then number each line by the NetSuite `line` (fall back to `linesequencenumber`
|
|
170
170
|
if the item ids disagree). Self-heals on the next receipt sync (it re-imports the PO). Same "join a
|
|
171
171
|
NetSuite line on `line`, not `linesequencenumber`" lesson as the `tl.id` semantics above.
|
|
172
|
+
- **⚠ `listPurchaseOrders()` scoped POs by the VENDOR header `entity`, silently dropping every
|
|
173
|
+
drop-ship PO for all clients (fixed 2026-09-01).** It reused the shared `buildEntityInFilter()`
|
|
174
|
+
helper — correct for `listSalesOrders`/`listSales` (SO header `entity` = customer) but **wrong for
|
|
175
|
+
POs**, whose header `entity` is the **vendor**. The `AND entity IN (<customer ids>)` filter matched
|
|
176
|
+
ZERO customer POs, so the header query returned nothing and the section imported nothing — no error.
|
|
177
|
+
Replaced with a new `buildPurchaseOrderCustomerFilter()` that scopes by the customer on the LINES or
|
|
178
|
+
the end-customer body field:
|
|
179
|
+
`AND ( id IN (SELECT tl.transaction FROM transactionline tl WHERE tl.entity IN ($ids) AND tl.mainline = 'F') OR CUSTBODY_END_CUSTOMER IN ($ids) )`.
|
|
180
|
+
`buildEntityInFilter()` is unchanged. This is a **fetch-scope** bug (record never returned by the
|
|
181
|
+
`list*`), a class distinct from the per-id `fetchPurchaseOrderById` shim-shape gaps above — but the
|
|
182
|
+
same underlying trap of reusing a sales-order-shaped assumption on a purchase order. Full
|
|
183
|
+
engine-side impact + the tier-wide per-client backfill live on the
|
|
184
|
+
[per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md).
|
|
172
185
|
- **A shim must produce the SOAP object's SHAPE, not just its scalar fields — an absent nested list
|
|
173
186
|
crashes 1.0 (2026-08-29).** `fetchCustomerById()`'s `addressbookList` was where the AIG lookup phase
|
|
174
187
|
crashed: a REST customer with no/odd address made the shim omit the nested `addressbook` structure the
|
|
@@ -262,6 +275,14 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
262
275
|
|
|
263
276
|
## Change history
|
|
264
277
|
|
|
278
|
+
- 2026-09-01 — Recorded that **`listPurchaseOrders()` scoped POs by the VENDOR header `entity`** (via
|
|
279
|
+
the reused `buildEntityInFilter()`), silently dropping every drop-ship / special-order PO for all
|
|
280
|
+
togasupply clients since the SOAP→REST migration. A PO header `entity` is the vendor, not the
|
|
281
|
+
customer, so `AND entity IN (<customer ids>)` matched zero rows and the section imported nothing with
|
|
282
|
+
no error. Fixed by a new `buildPurchaseOrderCustomerFilter()` that scopes by the line-level customer
|
|
283
|
+
or `CUSTBODY_END_CUSTOMER` body field; `buildEntityInFilter()` untouched. Fetch-scope bug (distinct
|
|
284
|
+
from the per-id shim-shape gaps). Engine impact + per-client backfill on the per-client-sync doc.
|
|
285
|
+
(jcardinal)
|
|
265
286
|
- 2026-08-30 — Recorded that **`fetchPurchaseOrderById` numbered PO lines by `linesequencenumber` instead
|
|
266
287
|
of the NetSuite `line`** — a receipt for a post-gap line (PO with a deleted middle line, real lines
|
|
267
288
|
1,2,4) failed `EV-12` "unable to find a unique match" (GroWrk PO 168223). The PO record line sublist
|
|
@@ -389,6 +389,47 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
389
389
|
skipped; and because the location filter is shared, receipts belonging to **other** clients at a
|
|
390
390
|
shared warehouse are returned and correctly skipped. Modelling receipts as "location-matched"
|
|
391
391
|
produces wrong diagnostics.
|
|
392
|
+
- **⚠ The PURCHASE_ORDERS section silently dropped EVERY drop-ship / special-order PO on ALL
|
|
393
|
+
togasupply clients — a PO header `entity` is the VENDOR, not the customer (fixed 2026-09-01).**
|
|
394
|
+
`App_Api_Netsuite_Rest::listPurchaseOrders()` (`library/app/api/netsuite/rest.php`) scoped the PO
|
|
395
|
+
HEADER query with `AND entity IN (<client customer ids>)` via the shared `buildEntityInFilter()`
|
|
396
|
+
helper. That helper is correct for **sales** orders (an SO header `entity` IS the customer) but
|
|
397
|
+
**wrong for POs** — a PO header `entity` is the **vendor**. So the customer-id filter matched ZERO
|
|
398
|
+
of a client's POs, the `foreach` never ran, and there was **no interest check, no throw, no log** —
|
|
399
|
+
a pure fetch-scope miss. Proven on Elite PO tranid 128238 (NS internal 5412515, customer 36443,
|
|
400
|
+
vendor TD Synnex 20202): `... AND entity IN (36443)` returned 0 rows; unfiltered returned 1;
|
|
401
|
+
filtering by LINE entity (`transactionline.entity IN (36443)`) returned 1.
|
|
402
|
+
- **Why drop-ship POs vanished ENTIRELY (two-path gap).** Two import paths reach a PO and both
|
|
403
|
+
excluded drop-ship: (a) the dedicated PURCHASE_ORDERS section, via the header-entity filter above;
|
|
404
|
+
(b) the nested `syncItemReceiptFromNetsuite → syncPurchaseOrderFromNetsuite` path (via
|
|
405
|
+
`fetchPurchaseOrderById`, which has **no** entity filter) never fires for a drop-ship PO because a
|
|
406
|
+
drop-ship PO produces item **fulfillments**, not item **receipts**. Stock POs still imported via
|
|
407
|
+
the item-receipt path — which is why Elite had 51 NS POs (all stock, from the 2026-08-12 backfill)
|
|
408
|
+
yet was missing drop-ship ones like 128238. Drop-ship POs carry `customForm.internalId = 148`
|
|
409
|
+
("ASI - Drop Ship Purchase Order"), the end customer on the transaction LINES
|
|
410
|
+
(`transactionline.entity`) and on `shipTo`, so the dedicated PURCHASE_ORDERS section is their
|
|
411
|
+
**only** import path.
|
|
412
|
+
- **Fix (library only, no SQL, no other repos):** added
|
|
413
|
+
`private static function buildPurchaseOrderCustomerFilter(array $entityInternalIds): string` and
|
|
414
|
+
switched `listPurchaseOrders()` to use it. It scopes POs by the customer on the LINES or the
|
|
415
|
+
end-customer body field instead of the vendor header:
|
|
416
|
+
`AND ( id IN (SELECT tl.transaction FROM transactionline tl WHERE tl.entity IN ($ids) AND tl.mainline = 'F') OR CUSTBODY_END_CUSTOMER IN ($ids) )`.
|
|
417
|
+
`buildEntityInFilter()` is left **untouched** (still correct for `listSalesOrders`/`listSales`/
|
|
418
|
+
`listItemFulfillments`, where header entity = customer). Empty-ids behavior preserved (returns
|
|
419
|
+
`''` = no scoping). Deployed; PO 128238 then imported for Elite (dev confirmed). **PO fetch must
|
|
420
|
+
scope by line-level customer / end-customer, never the vendor header entity — the opposite of the
|
|
421
|
+
sales-order shape, and the reason the reused helper silently lost data.** Cross-ref: the sibling
|
|
422
|
+
per-id path's own shim gaps are in the
|
|
423
|
+
[SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md).
|
|
424
|
+
- **⚠ Tier-wide backfill owed, per-client.** Because `listPurchaseOrders` is shared, EVERY
|
|
425
|
+
togasupply client has been missing its drop-ship POs since the SOAP→REST migration. Backfill each
|
|
426
|
+
client by rewinding `NETSUITE_LAST_SYNC_DATETIME_PURCHASE_ORDERS` (idempotent) — do it
|
|
427
|
+
**deliberately, one client at a time**, not all at once (Elite's first window 2024-04-15..04-18
|
|
428
|
+
alone held 269 POs).
|
|
429
|
+
- **Diagnostic lesson (durable):** a "silent, no-error, not-imported" record in this engine can be a
|
|
430
|
+
**fetch-scope mismatch** (the record is never returned by `list*`), distinct from a per-record
|
|
431
|
+
throw. The `$isInterested == 1` you see when feeding a single PO directly (or via the diagnose
|
|
432
|
+
script) can disagree with the scheduled section, which never fetched it.
|
|
392
433
|
- **`Units.locationId` is never populated by this sync — transaction location lives on the item
|
|
393
434
|
rows.** All 229 `Client_Elite.Units` have `locationId` NULL. `locationId` exists only on `Units`
|
|
394
435
|
and `InventoryAdjustmentItems` (not on the `*ItemUnits` bridge tables), and
|
|
@@ -6,7 +6,7 @@ client: compass-usa
|
|
|
6
6
|
type: workflow
|
|
7
7
|
status: active
|
|
8
8
|
updated: 2026-09-01
|
|
9
|
-
owners: ["jcardinal"]
|
|
9
|
+
owners: ["jcardinal", "bala"]
|
|
10
10
|
files:
|
|
11
11
|
- test/team/2.0 deployment/duplicate_items_merge_ALL_CLIENTS.sql
|
|
12
12
|
- test/team/2.0 deployment/duplicate_items_merge_analysis.sql
|
|
@@ -59,6 +59,15 @@ NULL-catalog junk items** and **199 duplicate-Agilant items** (from the NetSuite
|
|
|
59
59
|
inventory-adjustment item-create bug). **Only the 199 catalog-1 → catalog-2 folds were the
|
|
60
60
|
damage.**
|
|
61
61
|
|
|
62
|
+
**⚠ Open contradiction — not resolved, do not assume either side.** Folding by
|
|
63
|
+
`UPPER(partNumber)` requires those 199 pairs to have **shared a part number across catalogs**
|
|
64
|
+
pre-merge, and the reversal restored them. But a prod check of `Client_Compass` on **2026-09-01**
|
|
65
|
+
found **zero part numbers duplicated at all, and zero spanning more than one catalog**. Both
|
|
66
|
+
statements cannot be true of prod today. Re-check on prod whether the restored catalog-1 twins
|
|
67
|
+
carry **distinct** part-number variants (the `#B1H` / `-S` forms) or genuinely-shared ones before
|
|
68
|
+
relying on either the 199 figure or table-wide `partNumber` uniqueness. Same flag from the other
|
|
69
|
+
side: [two-catalog item model](../features/item-catalogs-and-duplicate-items.md).
|
|
70
|
+
|
|
62
71
|
## The reversal playbook (durable)
|
|
63
72
|
|
|
64
73
|
1. **Restore a pre-merge snapshot as a scratch DB** — e.g. `Client_Compass_20260831_120000`
|
|
@@ -127,6 +136,10 @@ catalog-1 items were folded into catalog-2 **AND** storefront/transmit depends o
|
|
|
127
136
|
NULL-`assetType` order lines alone are **not** damage (Prudential and Quad show that as normal).
|
|
128
137
|
|
|
129
138
|
## Change history
|
|
139
|
+
- 2026-09-01 — Flagged (did **not** resolve) the contradiction between this doc's 199
|
|
140
|
+
shared-`UPPER(partNumber)` catalog-1→catalog-2 folds and the same-day prod verification that zero
|
|
141
|
+
Compass part numbers span two catalogs; mirrored from the item-catalogs doc so it is visible from
|
|
142
|
+
both sides. (bala)
|
|
130
143
|
- 2026-09-01 — **Blast-radius assessment complete.** Resolved the outstanding per-client
|
|
131
144
|
assessment: only **2 of 17** clients were damaged — Compass (reversed prior session) and Quad
|
|
132
145
|
(storefront-only, reversed this session). The other 15 needed no reversal: 13 single-catalog
|
|
@@ -160,7 +160,8 @@ Cron 3a resolves each line through that SKU when the `VA` text does not match th
|
|
|
160
160
|
**onto the ODP SalesOrder (customerId=1), never the Compass SO (customerId=2)**.
|
|
161
161
|
**Exclusion filters** (an order legitimately waiting is not a bug):
|
|
162
162
|
- `CompassSalesOrders.number NOT LIKE 'MA%'` — MA orders are excluded.
|
|
163
|
-
- the two-part **release gate** below (
|
|
163
|
+
- the two-part **release gate** below (usually releases on the **next 10-minute run**; the
|
|
164
|
+
30-minute timeout is the fallback, not the normal path).
|
|
164
165
|
- **computer-kit orders** (Bundles 187–196) wait for a **2nd PO** before syncing. This one is
|
|
165
166
|
**not** in the `WHERE` clause — it is a per-row check run in PHP *after* the query
|
|
166
167
|
(`Bundles.number IN (192, 187, 188, 195, 193, 189, 194, 190, 191, 196)`, then a count of that
|
|
@@ -182,6 +183,19 @@ holds (verified against the cron source 2026-08-28, the `WHERE` block around `:9
|
|
|
182
183
|
created at least **30 minutes** ago (`IFNULL(MIN(OfficeDepotPO.dtCreated), NOW()) <=
|
|
183
184
|
DATE_SUB(NOW(), INTERVAL 30 MINUTE)`), in which case it transmits with whatever has come back.
|
|
184
185
|
|
|
186
|
+
**⚠ Branch (a) is the normal path — do NOT expect a 30-minute wait.** (a) is satisfied the instant
|
|
187
|
+
every expected Office Depot PO has arrived, so a straightforward order transmits on the **very next
|
|
188
|
+
`*/10` run**, typically **within ~2 minutes** of the ODP PO being created. The 30-minute timeout is
|
|
189
|
+
the **exception**, reserved for orders where something never comes back (see the mixed buy-from-ODP
|
|
190
|
+
case below). Verified on prod (2026-09-01): ODP POs created 14:50-14:55 on 2026-08-31 transmitted at
|
|
191
|
+
14:57-14:58; ODP PO `41850937-5125` created `2026-09-01 13:18:20` transmitted at `13:20:46`
|
|
192
|
+
(NetSuite SO `7441681`).
|
|
193
|
+
|
|
194
|
+
**Why this matters when triaging:** if you assume half an hour is normal, a genuinely stalled
|
|
195
|
+
transmission looks fine for 30 minutes. It is not. An order that has all its 850s back and is still
|
|
196
|
+
untransmitted **two `*/10` runs later** is already abnormal — go look at the bridge rows
|
|
197
|
+
(`PurchaseOrders_SalesOrders` / `SalesOrders_PurchaseOrders`) rather than waiting.
|
|
198
|
+
|
|
185
199
|
**Cron 5 never opens an EDI file and never looks in S3.** It infers that the 850 arrived purely from
|
|
186
200
|
the **link rows cron 3a writes**: Compass PO → `PurchaseOrders_SalesOrders` → Office Depot SO →
|
|
187
201
|
`SalesOrders_PurchaseOrders` → Office Depot PO. If 3a did not write the bridges, cron 5 cannot tell
|
|
@@ -210,10 +224,12 @@ order never reached NetSuite," and it is not.
|
|
|
210
224
|
`7437998` / `7437898` / `7438098`. Only after that check is "it never reached NetSuite" a real
|
|
211
225
|
finding.
|
|
212
226
|
|
|
213
|
-
**⚠ Do not infer the schedule from transmission times.**
|
|
214
|
-
which reads like a 30-minute cron — it is not. The schedule is **`*/10`** (re-verified in
|
|
215
|
-
`worker/schedules/cron.worker.sync.json` on 2026-08-28);
|
|
216
|
-
gate** above, not the cadence.
|
|
227
|
+
**⚠ Do not infer the schedule from transmission times.** Some transmissions cluster at **:00 and
|
|
228
|
+
:30**, which reads like a 30-minute cron — it is not. The schedule is **`*/10`** (re-verified in
|
|
229
|
+
`worker/schedules/cron.worker.sync.json` on 2026-08-28); that clustering is the timeout branch of
|
|
230
|
+
the **release gate** above firing on the orders that can only release that way, not the cadence.
|
|
231
|
+
Orders released by branch (a) do **not** cluster — they land on whichever `*/10` run follows their
|
|
232
|
+
last 850 (e.g. `13:20:46`).
|
|
217
233
|
|
|
218
234
|
### ⚠ Office Depot only sends an 850 back for what they buy FROM us
|
|
219
235
|
|
|
@@ -356,6 +372,11 @@ wrong record makes every order look "stuck." Use the **join**, never a number ma
|
|
|
356
372
|
transmission feature.)
|
|
357
373
|
|
|
358
374
|
## Change history
|
|
375
|
+
- 2026-09-01 — **Corrected the expected cron 5 timing:** release branch (a) ("every expected ODP
|
|
376
|
+
PO has arrived") is the **normal** path and fires on the next `*/10` run (~2 min), so the
|
|
377
|
+
30-minute timeout is the exception, not the rule — previously this doc read as if 30 minutes were
|
|
378
|
+
the standard delay. Prod evidence: 2026-08-31 POs created 14:50-14:55 transmitted 14:57-14:58; ODP
|
|
379
|
+
PO `41850937-5125` created 13:18:20 transmitted 13:20:46 (NetSuite SO `7441681`). (bala)
|
|
359
380
|
- 2026-09-01 — Added the diagnostic that the **Office Depot PO number lands in NetSuite's
|
|
360
381
|
`otherRefNum`, which global search does not index** — searching it finds nothing even on an order
|
|
361
382
|
that transmitted fine, so open the SO by `c_netsuiteInternalSalesOrderId` off the ODP SO
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: elite
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["snaredla"]
|
|
9
|
+
updated: 2026-09-01
|
|
10
|
+
owners: ["snaredla", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/sync_togasupply_elite.php
|
|
13
13
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -125,6 +125,17 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
|
|
|
125
125
|
|
|
126
126
|
## Change history
|
|
127
127
|
|
|
128
|
+
- 2026-09-01 — **Drop-ship / special-order POs were never importing for Elite** (e.g. PO tranid
|
|
129
|
+
128238). Root cause was in shared code, not Elite: `listPurchaseOrders()` filtered the PO header by
|
|
130
|
+
the VENDOR `entity` (TD Synnex 20202) instead of the customer, so Elite's 36443-scoped filter matched
|
|
131
|
+
0 POs, and drop-ship POs — which make item fulfillments, not receipts — had no other import path.
|
|
132
|
+
Fixed engine-side (new `buildPurchaseOrderCustomerFilter()` scoping by line-level customer /
|
|
133
|
+
end-customer); PO 128238 then imported (dev confirmed). Full write-up on the
|
|
134
|
+
[engine doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md) and
|
|
135
|
+
[SuiteQL/REST shim doc](../../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md). Elite's
|
|
136
|
+
older stock-only PO count (51) will grow once its `NETSUITE_LAST_SYNC_DATETIME_PURCHASE_ORDERS` is
|
|
137
|
+
rewound to backfill drop-ship POs (idempotent; first 2024-04-15..04-18 window alone held 269 POs).
|
|
138
|
+
(jcardinal)
|
|
128
139
|
- 2026-08-12 — TRUE-80499: **onboarded Elite to the NetSuite → TOGa Supply importer.** Built the
|
|
129
140
|
wrapper (customer 36443, `isParentCustomer` false, 30-day window, invoices temporarily disabled),
|
|
130
141
|
the schedule entry, 18 `_Model_Elite_*` netsuite trait models, the `_modules/netsuite` opt-in, and
|
package/package.json
CHANGED