toga-ai 1.0.759 → 1.0.761

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.
@@ -202,6 +202,43 @@ Prudential-EDI, S&P Global**.
202
202
  - **Lesson:** any `sync*FromNetsuite()` that takes the record `&$byReference` and refetches must have
203
203
  its fetch-by-id shim carry **every** field the caller reads afterward — `lastModifiedDate` first.
204
204
 
205
+ ### ⚠ …but the by-reference shim wrote the cursor on the WRONG CLOCK → ITEM_RECEIPTS + ITEM_FULFILLMENTS re-matched forever (found + fixed 2026-09-03, NYCHH prod)
206
+
207
+ A follow-on to the by-reference fix directly above. Once the two shims carried a `lastModifiedDate`
208
+ again, the sections still could **not advance** — the value was in the **wrong timezone**, so
209
+ `lastmodifieddate > cursor` re-matched the **same record every run** (an infinite loop, no crash).
210
+
211
+ - **Root cause — two clocks.** The prior fix set `fetchItemFulfillmentById` / `fetchItemReceiptById`'s
212
+ `->lastModifiedDate` from the **REST record GET**, which returns **UTC** (e.g. `2024-08-06T14:45:00Z`).
213
+ `common_sync_togasupply.php` then `date()`-formats it in the script timezone **America/Chicago** →
214
+ `2024-08-06 09:45:00`, and stores that as the cursor. But the LIST fetch's `WHERE`
215
+ (`App_Api_Netsuite_Rest::listItem*`) compares `lastmodifieddate` via SuiteQL
216
+ `TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS')`, which is the **NetSuite account timezone =
217
+ Eastern** → `2024-08-06 10:45:16`. So the cursor was written **~1 hour behind** the clock the fetch
218
+ reads → `lastmodifieddate > cursor` was always still true for the record just processed → it was
219
+ re-fetched and re-processed forever and the watermark never moved.
220
+ - **Proven on NYCHH prod** (item receipt internalId `4953915`): REST GET `lastModifiedDate`
221
+ `2024-08-06T14:45:00Z` (UTC) · SuiteQL `2024-08-06 10:45:16` (Eastern) · stored cursor
222
+ `2024-08-06 09:45:00` (Chicago). ITEM_RECEIPTS was jammed on it; because that heavy receipt (nested
223
+ PO→SO reconcile) runs **before** fulfillments and starved the run, ITEM_FULFILLMENTS' window kept
224
+ collapsing to 6s and never grew to clear the empty gap to its first real record (2025-01-06; 1,294
225
+ NYCHH fulfillments waited after 2025-01-01).
226
+ - **INVENTORY_ADJUSTMENTS was NOT stuck** — `fetchInventoryAdjustmentById` already reads
227
+ `lastmodifieddate` from SuiteQL `TO_CHAR` (the right clock), so it advanced normally. **That is the
228
+ correct pattern**; the other two shims did not follow it.
229
+ - **Fix (library only, `php -l` clean, fail-loud preserved):** in `library/app/api/netsuite/rest.php`,
230
+ `fetchItemFulfillmentById` (~L2093) and `fetchItemReceiptById` (~L2254) now set
231
+ `$shim->lastModifiedDate` from a SuiteQL
232
+ `SELECT TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS') AS lastmodifieddate FROM transaction WHERE id = $id`
233
+ (Eastern) — matching `fetchInventoryAdjustmentById` — **instead of** the REST header's UTC
234
+ `$header->lastModifiedDate`. No shim reads the REST/UTC `lastModifiedDate` anymore. No worker change,
235
+ no SQL. After deploy the two sections resume and backfill on their own (idempotent).
236
+ - **Lesson:** the per-record cursor value MUST be written in the **same timezone the fetch `WHERE`
237
+ compares against** — SuiteQL account time (Eastern), never the REST record GET's UTC
238
+ `lastModifiedDate`. A cursor even an hour behind the fetch clock re-matches the same record forever.
239
+ This is the same SuiteQL-vs-REST clock trap as the date-only-format bug above — see the
240
+ [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md).
241
+
205
242
  ### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
206
243
 
207
244
  Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
@@ -1175,6 +1212,15 @@ the code deploy).
1175
1212
 
1176
1213
  ## Change history
1177
1214
 
1215
+ - 2026-09-03 — **By-reference shim wrote the cursor on the WRONG CLOCK → ITEM_RECEIPTS + ITEM_FULFILLMENTS
1216
+ re-matched the same record forever (fixed, `library` only).** Follow-on to the same-day by-reference
1217
+ fix below: the shims set `lastModifiedDate` from the REST GET (UTC), `date()`-formatted in
1218
+ America/Chicago, but the LIST `WHERE` compares SuiteQL `TO_CHAR(lastmodifieddate)` in the NetSuite
1219
+ account tz (Eastern), so the cursor sat ~1h behind the fetch clock and `lastmodifieddate > cursor`
1220
+ always re-matched — infinite loop, no crash. `fetchItemFulfillmentById`/`fetchItemReceiptById`
1221
+ (`rest.php` ~L2093/~L2254) now read `lastmodifieddate` from SuiteQL `TO_CHAR` (Eastern), matching
1222
+ `fetchInventoryAdjustmentById` (already correct). Proven on NYCHH prod (item receipt 4953915). No
1223
+ worker/SQL change; sections self-backfill after deploy. (jcardinal)
1178
1224
  - 2026-09-03 — **By-reference refetch dropped `lastModifiedDate` → ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS
1179
1225
  froze (fixed, `library` only).** GroWrk's ITEM_FULFILLMENTS was re-throwing every 5 min since the
1180
1226
  2026-09-02 per-record cursor deploy (`Undefined property: stdClass::$lastModifiedDate` at
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-02
9
+ updated: 2026-09-03
10
10
  owners: [bala, apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
@@ -28,6 +28,7 @@ related:
28
28
  - ../../../../clients/compass-canada/features/item-fulfillment-tableviews.md
29
29
  - ./acl-permission-chain.md
30
30
  - ../../toga25-supply/features/transfer-orders-page.md
31
+ - ../../toga25-supply/features/record-modals-and-nested-tables.md
31
32
  ---
32
33
 
33
34
  ## Summary
@@ -134,6 +135,36 @@ For NYCHH both hops are **100% populated** (PO items 1129/1129, VendorItems 2414
134
135
  **INNER** — no empty-grid risk (third failure mode above). Verify FK population per tenant first: a
135
136
  tenant with sparse `vendorItemId` would need LEFT OUTER or would empty the grid.
136
137
 
138
+ ### A dropped FILTER-ONLY join must be re-added when a row-filter still targets that table
139
+
140
+ The rebase above **dropped** the old SalesOrders→bridge→PurchaseOrders chain (join ids 123/124/141)
141
+ because those joins showed **no column** — they only filtered rows. But a filter-only join is still
142
+ needed if something still filters on that table. NYCHH's Inventory "Items by Customer POs" grouping
143
+ drills into `items-for-purchase-orders` with an `additionalDataSlug` that binds a
144
+ `(PurchaseOrders.uuid:eq:...)` where clause (see
145
+ [record-modals-and-nested-tables](../../toga25-supply/features/record-modals-and-nested-tables.md),
146
+ Pattern 5, and
147
+ [tableview-apiwhereclause-row-filtering](../../api2/features/tableview-apiwhereclause-row-filtering.md)).
148
+ After the rebase the view no longer joined PurchaseOrders, so that filter failed with **error
149
+ 3L-2492**.
150
+
151
+ Fix (confirmed working NYCHH 2026-09-03): **INSERT one filter-only `TableViewJoins` row** on the
152
+ `items-for-purchase-orders` view re-adding a real PurchaseOrders join — no column hangs off it:
153
+
154
+ - `joinRecordId = 17` (PurchaseOrders), `joinOnRecordFieldId = 81` (`PurchaseOrders.id`) as onA,
155
+ `parentRecordFieldId = 90` (`PurchaseOrderItems.purchaseOrderId`) as onB.
156
+ - `type INNER` — `purchaseOrderId` is always populated on PO items, so no empty-grid risk (third mode
157
+ above). `sortOrder 7` (after the existing 1–6).
158
+ - **No ACL row needed** if PurchaseOrders already has read grants for the caller's roles: NYCHH's
159
+ record 17 already had Base(1)/Developer(2)/API(3), so the ACL-INNER-JOIN joins query keeps it.
160
+ - Idempotent: key to the view by `TableViews.slug` (id-agnostic) with a fixed uuid and a `NOT EXISTS`
161
+ guard on `(tableViewId, joinRecordId=17)`. Core ids 17/81/90 verified stable/identical across prod
162
+ and client-sandbox.
163
+
164
+ **Both env states were verified rebased** (prod AND client-sandbox: view 16 at base recordId 18 with
165
+ no PurchaseOrders join) — the earlier "rebase is prod-only, not on client-sandbox" note is now stale.
166
+ Applied to client-sandbox then prod (Client_Nychh).
167
+
137
168
  Core ids for this conversion (verified prod 2026-09-02, identical across clients/envs): **Records** —
138
169
  sales-order-items 15, purchase-order-items 18, vendor-items 19, items 21; item-level SO/PO bridges
139
170
  289 (downstream) / 290 (upstream). **RecordFields** — `PurchaseOrderItems.quantity` 94,
@@ -316,6 +347,13 @@ Core-level reference ids used (identical across all clients and environments): *
316
347
 
317
348
  ## Change history
318
349
 
350
+ - 2026-09-03 — Added the **filter-only join counterpart** to the PO-items rebase: dropping a
351
+ column-less join that only *filtered* rows (SalesOrders→bridge→PurchaseOrders, ids 123/124/141)
352
+ breaks any row-filter still targeting that table. NYCHH's `items-for-purchase-orders`
353
+ `(PurchaseOrders.uuid:eq:...)` filter errored **3L-2492** post-rebase; fix is one INNER filter-only
354
+ PurchaseOrders join (joinRecordId 17, onA `PurchaseOrders.id` 81, onB `PurchaseOrderItems.purchaseOrderId`
355
+ 90, sortOrder 7, no column, no new ACL). Confirmed working; both prod and client-sandbox verified
356
+ rebased (the prior "prod-only" note is stale). (jcardinal)
319
357
  - 2026-09-02 — Documented the **sales-order-item → purchase-order-item view conversion**: it needs an
320
358
  extra **VendorItems** hop because `PurchaseOrderItems` has no `itemId` (catalog reached via
321
359
  `vendorItemId → VendorItems (record 19) → itemId → Items`), unlike `SalesOrderItems` which joins
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-02
10
- owners: [apeterson, tcox]
9
+ updated: 2026-09-03
10
+ owners: [apeterson, tcox, jcardinal]
11
11
  files:
12
12
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
13
13
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx
@@ -127,6 +127,18 @@ level. Each level fetches its own meta + data and renders a `GenericTableLayout`
127
127
 
128
128
  `additionalData` is produced per-level by callbacks (the WHERE-filter shape from Pattern 4). Each
129
129
  level runs its own `useServerTableUrlState` and strips its own slug key, isolating URL state.
130
+
131
+ > **The FE APPENDS `.uuid` to a swap level's `additionalDataSlug` itself — so the stored surface
132
+ > value must be the BARE table name (`PurchaseOrders`), never `<Table>.uuid`.** The grouping config
133
+ > lives on `Core.SurfaceElements.config` (`inventory-list-groupings`, e.g. element key `posByItems`,
134
+ > in `$.levels[N].additionalDataSlug`). If you store `PurchaseOrders.uuid` there, the FE produces a
135
+ > **doubled** where clause `(PurchaseOrders.uuid.uuid:like:...)` and the filter never binds. Fix the
136
+ > stored value to just `PurchaseOrders` (JSON_SET keyed by `Surfaces.slug` + `config.key`, guarded to
137
+ > the stale value so it is re-runnable — id/uuid-agnostic because ids differ per env). Confirmed on
138
+ > NYCHH 2026-09-03: this Core row is all-client (no NYCHH SurfaceOverride), and its old
139
+ > `SalesOrders.uuid` was already wrong for a child view named `items-for-purchase-orders`. Sibling
140
+ > element `itemsByPOs` (view `purchase-orders_items`) still carries the stale `SalesOrders.uuid` —
141
+ > likely the same bug, not yet fixed.
130
142
  A page-level `pageMeta` (from `useFetchPageMeta`) flows down for field-label overrides. The page
131
143
  view model builds `TableLevel[]` arrays, picks the active one via a URL param, and passes `levels`
132
144
  to `GenericNestedTables` with `key={activeGrouping.key}` to force remount and avoid stale state.
@@ -274,6 +286,12 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
274
286
  [Transfer Orders page](transfer-orders-page.md).
275
287
 
276
288
  ## Change history
289
+ - 2026-09-03 — Documented that the FE **appends `.uuid`** to a `GenericNestedTables` swap level's
290
+ `additionalDataSlug`, so the stored `Core.SurfaceElements.config` value must be the **bare table
291
+ name** (`PurchaseOrders`), not `<Table>.uuid` — a `.uuid` in the stored value produces a doubled
292
+ `(PurchaseOrders.uuid.uuid:like:...)` where clause and the filter never binds. Found fixing the
293
+ NYCHH Inventory "Group: Items by Customer POs" grouping (`inventory-list-groupings`, element
294
+ `posByItems`); sibling `itemsByPOs` still stale. (jcardinal)
277
295
  - 2026-09-02 — Fixed the approve/deny modal's Submit/Cancel row being cut off by a long "Reason for
278
296
  Request" (found in testing by Skyler; approach agreed with David — grow the height a little, scroll
279
297
  when it is very long). The single-action dialog is now taller but viewport-capped, and its action row
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: nychh
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-02
21
+ updated: 2026-09-03
22
22
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
23
23
  files:
24
24
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -97,15 +97,32 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
97
97
  items 1129/1129, VendorItems 2414/2414) so both stay INNER. Seeds Developer-role record grants for
98
98
  records 18 and 19 (both had only Base+API) plus Developer read on `PurchaseOrderItems.quantity`
99
99
  (field 94); roles resolved by name, uuid-keyed, idempotent. Mechanism:
100
- [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md). 🚨 **Applied to PROD
101
- only so far** (view 16 rebased, chain returns 1131 rows) — **NOT run on client-sandbox** (still the
102
- old sales-order-items version); the parent 2026-08-31a went to prod + client-sandbox, this one is
103
- prod-only. **OPEN:** the parent `purchase-orders` view filters to vendor 172 (`apiWhereClause
100
+ [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md). **Applied to prod AND
101
+ client-sandbox** — both verified rebased 2026-09-03 (view 16 at base recordId 18, no PurchaseOrders
102
+ join). An earlier note here said this rebase was prod-only / not on client-sandbox; that is **stale**
103
+ — client-sandbox is also rebased. **OPEN:** the parent `purchase-orders` view filters to vendor 172
104
+ (`apiWhereClause
104
105
  (vendorId:eq:172)`); this item view has no `apiWhereClause`, so it lists items for all POs — left
105
106
  unfiltered (mirrors prior state), flagged as a decision for the dev. A "3L-2250" error reported
106
107
  right after this deploy was **unrelated** — prod `Logs.Issue` reference **3L** is a Prudential ASN
107
108
  error (`Client_Prudential.AdvanceShippingNoticeUnits` doesn't exist); the suffix is the occurrence
108
109
  count (3L, occurrence 2250).
110
+ - **"Group: Items by Customer POs" grouping fixed to drill into PO items (2026-09-03).** After the
111
+ `items-for-purchase-orders` view was rebased to PO-items, the Inventory groupings surface
112
+ (`inventory-list-groupings`, element `posByItems`) was broken. Two fixes, both applied to
113
+ client-sandbox then prod (confirmed working):
114
+ 1. **Stale filter slug** — the swap level's `additionalDataSlug` was `SalesOrders.uuid`; changed to
115
+ `PurchaseOrders`. The FE **appends `.uuid` itself**, so the stored value must be the bare table
116
+ name — `PurchaseOrders.uuid` produced a doubled `(PurchaseOrders.uuid.uuid:like:...)` clause.
117
+ This config is on `Core.SurfaceElements.config` (all-client, no NYCHH override); JSON_SET keyed by
118
+ `Surfaces.slug` + `config.key`, guarded/re-runnable. Sibling `itemsByPOs` still carries the stale
119
+ `SalesOrders.uuid` — likely the same bug, **not yet fixed**.
120
+ 2. **Missing join** — the `(PurchaseOrders.uuid:eq:...)` filter errored **3L-2492** because the
121
+ rebase dropped the old SalesOrders→PurchaseOrders filter chain and the view no longer joined
122
+ PurchaseOrders. Added one filter-only INNER `TableViewJoins` row (joinRecordId 17, onA
123
+ `PurchaseOrders.id` 81, onB `PurchaseOrderItems.purchaseOrderId` 90, sortOrder 7, no column). No
124
+ new ACL — record 17 already has Base/Developer/API read. Mechanism:
125
+ [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md).
109
126
  - **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
110
127
  and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
111
128
  Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.759",
3
+ "version": "1.0.761",
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",