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.
@@ -6,7 +6,7 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-30
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 (this is the "30-minute delay").
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.** Transmissions cluster at **:00 and :30**,
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); the clustering is the 30-minute **release
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-08-12
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.729",
3
+ "version": "1.0.731",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",