toga-ai 1.0.726 → 1.0.727

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.
@@ -0,0 +1,109 @@
1
+ ---
2
+ type: session
3
+ slug: transfer-orders-qty-blocker
4
+ title: NYCHH Transfer Orders item picker empty — root-caused to the Nychh _qtyReceived override
5
+ author: apeterson
6
+ repos: [toga25-supply, dbchanges2, api2, _underscore]
7
+ framework: "2.0"
8
+ client: nychh
9
+ status: active
10
+ created: 2026-09-01
11
+ updated: 2026-09-01
12
+ ---
13
+
14
+ # Session: transfer-orders-qty-blocker
15
+ **Date:** 2026-09-01
16
+ **Project/Repo:** toga25-supply (2.0), with dbchanges2; api2 + _underscore read-only
17
+ **Task:** Finish the NYCHH Create Transfer Order modal (contact picker, notes, PO select, item picker) and find out why the item picker renders empty even though `/purchase-order-items` returns 823 rows.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ - **ROOT CAUSE FOUND.** `_Model_Nychh_PurchaseOrderItem` overrides `_qtyReceived` to source stock-in from **inventory adjustments** instead of item receipts. Verified both formulas against the same row (sandbox `Client_Nychh`, `PurchaseOrderItems.id = 196`, uuid `01110c53-ba73-e60a-d94c-0ccf77327c0e`, PO 80622 line 1):
24
+ - base `_Model_Client_PurchaseOrderItem::_qtyReceived` → **40.00**
25
+ - `_Model_Nychh_PurchaseOrderItem::_qtyReceived` → **0.00**
26
+ - **Why the override yields 0** — its chain does `INNER JOIN InventoryAdjustmentItems ON itemFulfillmentItemId = ItemFulfillmentItems.id`, and that column is NULL on every row:
27
+ - sandbox `Client_Nychh`: 292 `InventoryAdjustmentItems`, **0** with `itemFulfillmentItemId` set
28
+ - production `Client_Nychh`: 317 `InventoryAdjustmentItems`, **0** with `itemFulfillmentItemId` set
29
+ - production PO lines the override reports with qty > 0: **0**
30
+ This is the override file's own documented "old-data caveat" (NetSuite only began linking adjustments to fulfillments within the last year) — currently it is 100% of the data, not a corner case.
31
+ - **Correct way to detect a tenant model override** — `git cat-file -e origin/<branch>:Model/<Slug>/<Model>.php`. Confirmed PRESENT on `origin/_sandbox-client` and `origin/_production`, absent on `origin/_beta` and `origin/_sandbox-dev`.
32
+ - **Documented the full `_qty*` formula chain** from `_underscore/Model/Client/PurchaseOrderItem.php` (lines 41–158): `_qtyReceived` = SUM(ItemReceiptItems.quantity); `_qtyFulfilled` = shipped ItemFulfillmentItems via `PurchaseOrderItems_TransferOrderItems`; `_qtyOnHand` = received − fulfilled; `_qtyCommitted` = SO qtyCommitted − shipped, matched by **vendor item** not PO line; `_qtyAvailable` = onHand − committed.
33
+ - **Fixed: numeric `where` values silently cancel the request.** blox `urlEncode(input: string)` calls `input.replace()`, so a numeric `1` throws inside `buildUrl` → `queryFn`. React Query swallows it and **no HTTP request is sent** — an empty dropdown with nothing in the network tab. Corrected to `{ "Locations.isActive": { "=": "1" } }` in `useTargetLocationOptions.ts` and `useTransferSourceLocation.ts`; the target-location and PO dropdowns then issued requests.
34
+ - **Fixed: non-zero `staleTime` suppresses fetches across sessions.** `App.tsx` uses `PersistQueryClientProvider` + `createSyncStoragePersister` on localStorage key `supply-chain-query-cache`, and the QueryClient default is `staleTime: 0`. Three new hooks had `staleTime: 5*60*1000`, letting a persisted entry from an earlier session count as fresh. All set to 0.
35
+ - **Ran `Core/2026-09-01a - TransferOrderCreatePoColumnAndFilter.sql` successfully on sandbox** — message 288, SurfaceElements 214 (PO column) + 215 (poFilter), absolute `CASE` sortOrder assignment, `isVisible = 0` on 209/211/213. Confirmed by the resulting sortOrder rows.
36
+ - **Wrote and ran `Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql`** on sandbox `Client_Nychh` — 8 role-3 grants landed, verified. (It is NOT the fix; see below.)
37
+ - **Fleet ACL survey (production, 36 client DBs)** — 31 grant all eight `_qty*` to role 1 and none to role 3; 4 (Compass, Imhouston, Mcgrawhill, Tdsynnex) have none at all; only Prudential (roles 1+3) and Northwell (role 3) are correct.
38
+ - **Verified `Core.Records`/`Core.RecordFields` ids are identical between production Core and sandbox Core** for record 18 — this is what makes literal id references safe in `Client/` migrations.
39
+ - **Knowledge captured and pushed** — commit `261d8e7` on `_main`, 9 docs, validate OK (32 repos).
40
+
41
+ ## What did NOT work — DO NOT RETRY THESE
42
+
43
+ - **Granting `AclFieldPermissions` to role 3 does NOT fix the empty picker.** The 8 grants landed and the response was byte-for-byte unchanged — still `_qtyAvailable: 0`. Reason: `V2.php:3338` classifies a field as calculated when `$modelFieldConfig[$f]['type'] == _Model::FIELD_SQL`, and that branch runs **before** the ACL branch at `:3353` and checks no permissions. An explicit `fields=_qtyAvailable` therefore bypasses `AclFieldPermissions` entirely. The ACL gate for calc fields is at `V2.php:7216`, which is the *no-explicit-fields* (`getFullModelData`) path only.
44
+ - **Checking `Core.AclFieldPermissions` for record 18 is meaningless.** `Core.Records.id = 18` has `aclDatabase = CLIENT`, so grants are read from `Client_<Tenant>.AclFieldPermissions`. I checked Core, got a confident answer, and it sent the investigation down a wrong path for a long stretch.
45
+ - **Eliminating the tenant model override by listing the local working tree.** `ls Model/Nychh/` showed 21 files and no `PurchaseOrderItem.php` — because the local checkout is on branch `features-talos-transferorders`, which does not carry it. The file **is** on the deployed branches. This single wrong elimination is why the root cause took so long.
46
+ - **Trusting the sibling drift check.** `git diff origin/_sandbox-client -- Model/Client/PurchaseOrderItem.php` was empty and all four branches are byte-identical — a true result that made the override elimination feel safe. The drift was one directory over, in `Model/Nychh/`.
47
+ - **Mirroring a sibling field's roles in an ACL grant migration.** Drafted it (matching house precedent `Client/2026-07-22c`) and rejected it before shipping: Compass grants `quantity` to **role 4 (Public)**, so the mirror would have handed Public read on eight derived inventory quantities, and Imhouston has no `quantity` grant so the mirror would have silently inserted nothing there.
48
+ - **Reading `Core.RecordFields` from a `Client/` migration.** First draft did this (copying `Client/2026-07-22c`); the `dbchanges2-cluster-isolation.js` PreToolUse hook blocked it, correctly. Verified on production that the client cluster (reader **and** writer endpoints) hosts only `Client_*` databases — no `Core`. `2026-07-22c` is a shipped violation, not a pattern to copy.
49
+ - **Inferring a table's column list from a sibling migration's VALUES row.** Produced `Unknown column 'slug' in 'field list'`. `Core.Messages` columns are `(id, uuid, dtCreated, messageKey, name, defaultValue)`.
50
+ - **`ON DUPLICATE KEY UPDATE id = id` on literal-id rows** — self-caught before running; violates the rule that a stolen team-assigned id must fail loudly. Replaced with `NOT EXISTS` guards on the natural key.
51
+ - **Relative `sortOrder + 1` shift guarded by a subquery on the table being updated** — MySQL error 1093 risk. Replaced with an idempotent absolute `CASE` assignment that needs no guard.
52
+ - **Flipping `[debug] show_queries` in `Config/sandbox-client.ini`** — was queued as the next diagnostic and is now unnecessary. There is nothing left to find in the query.
53
+ - **`ERROR 1054 Unknown column 'f.sql'` on `Core.RecordFields`** — the table has no `sql` column; calculated-field SQL lives in the `_underscore` model as `FIELD_SQL` static methods.
54
+ - **`SELECT ... slug FROM Roles`** — the client `Roles` table has no `slug` column (id, name only).
55
+
56
+ ## Not tried yet (candidates for next session)
57
+
58
+ - **Decide the stock-in source for NYCHH.** Either (a) get the NetSuite integration to backfill `InventoryAdjustmentItems.itemFulfillmentItemId`, or (b) change `_Model_Nychh_PurchaseOrderItem::_qtyReceived`. This is the actual blocker and is an integration/data-owner call (Jason owns the availability-decrement behavior).
59
+ - **Ask why the NYCHH override exists at all** — if item receipts are the truth for availability, the override may simply be wrong for this use case rather than needing a backfill.
60
+ - **Notes persistence** — `TransferOrders` has no notes/description column. Options: `TransferOrders.description varchar(255)` mirroring `SalesOrders`, or a `TransferOrderNotes` child table mirroring `SalesOrderNotes`.
61
+ - **Build the review step** — currently a placeholder ("Review step — not built yet"); the Create button is disabled.
62
+ - **Strip the diagnostics** — `_qtyReceived`/`_qtyFulfilled` were added to `PO_ITEM_FIELDS` purely to diagnose, and `sampleBreakdown` threads through to the empty-state message. Remove both once the blocker is resolved.
63
+ - **Decide whether `Client/2026-09-01a` should fan out to all tenants** or stay NYCHH-only. It is currently applied to sandbox `Client_Nychh` only.
64
+ - **`allowReceiving` data hygiene** — decide whether the flag should be maintained so the target dropdown can filter on it.
65
+ - **Flag `Client_Compass_20260831_120000`** — a dated backup database on the production client cluster that any `Client/` fan-out would hit.
66
+ - Parked, do not resume unasked: Line Items + Units columns, Target Contact column on the list table, the status 5→3 vocabulary relabel, Target Location path rendering, the location pin icon.
67
+
68
+ ## Current file state
69
+
70
+ | File | Status | Notes |
71
+ |------|--------|-------|
72
+ | `toga25-supply/.../CreateTransferOrderModal/CreateTransferOrderModal.tsx` | Modified, uncommitted | Bidirectional PO binding (`lockedPo`/`preLockPoRef`/`wasLockedRef`), search input first then PO select, `FILTER_ROLES` splits controls from columns, empty-state cascade names the real cause |
73
+ | `toga25-supply/.../hooks/usePurchaseOrderItemRows.ts` | Created, uncommitted | Fetches all PO lines, derives PO options; still carries the `_qtyReceived`/`_qtyFulfilled` + `sampleBreakdown` diagnostics |
74
+ | `toga25-supply/.../hooks/useTargetLocationOptions.ts` | Created, uncommitted | All active locations minus source; `where` values are strings; `staleTime: 0` |
75
+ | `toga25-supply/.../hooks/useTransferSourceLocation.ts` | Created, uncommitted | Resolves the single `allowSending` location; exports `formatLocationAddress`; returns `hasAmbiguousSource` |
76
+ | `toga25-supply/.../hooks/useLocationContacts.ts` | Created, uncommitted | Route `/contact-locations` (Record 76); `DEFAULT_CONTACT_TYPE = "SHIPPING"` |
77
+ | `toga25-supply/.../hooks/useCreateLocationContact.ts` | Created, uncommitted | POST `/contacts` then POST `/contact-locations` |
78
+ | `toga25-supply/.../view/AddContactModal.tsx` | Created, uncommitted | Two required names, capture-phase Escape, own scrim |
79
+ | `toga25-supply/.../view/TransferNotesField.tsx` | Created, uncommitted | `useAutoGrow`, `FieldClear`, Escape blurs |
80
+ | `toga25-supply/.../view/TransferItemPickerTable.tsx` | Modified, uncommitted | Type renamed `PurchaseOrderItemRow`; caller-owned `emptyMessage` |
81
+ | `toga25-supply/.../hooks/useTransferableItemRows.ts` | Deleted, uncommitted | Superseded |
82
+ | `toga25-supply/.../hooks/usePurchaseOrderOptions.ts` | Deleted, uncommitted | Superseded — options now derived from item rows |
83
+ | `dbchanges2/Core/2026-09-01a - TransferOrderCreatePoColumnAndFilter.sql` | Created, uncommitted | **RUN on sandbox, succeeded** |
84
+ | `dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql` | Created, uncommitted | **RUN on sandbox `Client_Nychh`, succeeded** — but is NOT the fix; reversible via its 8 uuid literals |
85
+ | `toga-tech/knowledge/**` (9 docs) | Committed + pushed | Commit `261d8e7` on `_main` |
86
+
87
+ `npx tsc --noEmit` from the toga25-supply root: exit 0, clean. Nothing committed or pushed in any project repo.
88
+
89
+ ## Decisions made
90
+
91
+ - **Grant role 3 explicitly rather than mirroring a sibling field's roles.** Rationale: mirroring `quantity` would have granted Compass's role 4 (Public) eight derived inventory quantities and would have inserted nothing on Imhouston. Rejected: the house precedent `Client/2026-07-22c`, which mirrors.
92
+ - **Use hardcoded v4 uuid literals, not `UUID()`, in fan-out ACL grant migrations.** Rationale: rollback becomes exact — `DELETE ... WHERE uuid IN (<literals>)` removes only the rows that file created and cannot touch Prudential's/Northwell's pre-existing role-3 grants. A rollback keyed on `(recordFieldId, roleId)` would have deleted theirs. uuid is unique per table per database, so one literal reused across tenants is valid. Rejected: the house habit of `UUID()` "so each client's rows are distinct" — it buys nothing here and costs the clean undo.
93
+ - **Reference `Core.RecordFields` ids as literals in `Client/` migrations.** Rationale: Core is on a separate cluster in production, so a `Core.*` lookup resolves only in non-prod; `Records`/`RecordFields` ids are team-assigned and verified identical across environments. Rejected: the cross-database lookup used by `2026-07-22c`.
94
+ - **Target Location lists all active locations minus the source, NOT filtered on `allowReceiving`.** Rationale: exactly 1 of NYCHH's 31 active locations has `allowReceiving = 1`, and it is the same location that has `allowSending = 1` — filtering would offer only the place you cannot transfer to. The flag is unmaintained for this tenant.
95
+ - **Source Location is resolved by flag (`allowSending = 1`), read-only from modal open** — not derived from row selection and not hardcoded. Rationale: another tenant, or a NYCHH rename, then needs no code change. The old selection-derived source lock was replaced by the PO lock.
96
+ - **The PO lock replaces the location lock** — all lines on one transfer must come from the same purchase order, per the live Claude Design export.
97
+
98
+ ## Blockers
99
+
100
+ - **HARD BLOCKER — no NYCHH PO line reports available stock, in sandbox or production.** `_Model_Nychh_PurchaseOrderItem::_qtyReceived` INNER JOINs `InventoryAdjustmentItems.itemFulfillmentItemId`, which is NULL on all 292 sandbox rows and all 317 production rows. Production PO lines with qty > 0: 0. The Create Transfer Order item picker cannot show a selectable line until either the NetSuite integration backfills that FK or the override's stock-in source changes. **This is an integration/data-owner decision, not a frontend fix.**
101
+ - **Demo trap:** the override is absent from `_beta` and `_sandbox-dev`, so those tiers fall back to the base item-receipt formula and **will show a working picker with real quantities**. A beta demo will look healthy while production is structurally empty. Do not read beta as evidence the feature works.
102
+ - Notes field has nowhere to persist (`TransferOrders` has no notes/description column).
103
+
104
+ ## Exact next step
105
+
106
+ > Take the blocker to Jason and the NetSuite integration owner with these two numbers: production `Client_Nychh` has 317 `InventoryAdjustmentItems` and **0** with `itemFulfillmentItemId` set, so `_Model_Nychh_PurchaseOrderItem::_qtyReceived` returns 0.00 for every PO line. Ask which of the two fixes they want — backfill `InventoryAdjustmentItems.itemFulfillmentItemId`, or change the override's stock-in source (base `_Model_Client_PurchaseOrderItem::_qtyReceived` uses `ItemReceiptItems` and returns 40.00 for `PurchaseOrderItems.id = 196`). No further frontend work on the item picker until that is answered.
107
+
108
+ ---
109
+ _Saved by /session-save on 2026-09-01_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.726",
3
+ "version": "1.0.727",
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",