toga-ai 1.0.795 → 1.0.797

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.
@@ -20,6 +20,8 @@ files:
20
20
  - api2/Component/Api/V2/V2.php
21
21
  - _underscore/Model/Quad/Item.php
22
22
  - _underscore/Model/Quad/VendorItem.php
23
+ - _underscore/Model/Nychh/Unit.php
24
+ - dbchanges2/Client_Nychh/2026-09-10a - UnitsForPoTableViewRebaseAndTransferLocationField.sql
23
25
  - dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
24
26
  - dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql
25
27
  related:
@@ -253,6 +255,23 @@ characters — that is the standard of evidence for touching a field that runs o
253
255
  [save-cascade stored-field deadlocks](./model-save-parent-cascade-stored-field-deadlock.md).
254
256
 
255
257
  ## Change history
258
+ - 2026-09-10 — Added two NYCHH-only calc fields on `_Model_Nychh_Unit`
259
+ (`_underscore/Model/Nychh/Unit.php`) for the rebuilt `units-for-items-for-purchase-orders` grid,
260
+ both plain `FIELD_SQL` (text — no `FIELDOPT_SQL_TYPE`): **(a)** an **override** of
261
+ `_unitDisposition()` — NYCHH does **not** use the `UnitDispositions` lookup; disposition is derived
262
+ purely from the transfer chain (`Deployed` if an `ItemFulfillmentItems` with
263
+ `transferOrderItemId IS NOT NULL` exists for the unit, else `Available`). The override **matches the
264
+ parent's untyped signature exactly** (the contravariance fatal above). **(b)** a **new**
265
+ `_transferLocation` field returning the transfer-order destination location name via
266
+ Units → ItemFulfillmentItemUnits → ItemFulfillmentItems → TransferOrderItems → TransferOrders →
267
+ destinationLocationId → Locations (`ORDER BY` fulfillment id `DESC LIMIT 1`), registered as a
268
+ `Client_Nychh.CustomRecordFields` row (record 31) + Base-role `AclCustomFieldPermissions` grant in
269
+ `Client_Nychh/2026-09-10a`. **Why a calc field and not a TableView join:** the location chain
270
+ re-uses `ItemFulfillmentItems`/`ItemFulfillmentItemUnits` a **second** time, and `TableViewJoins`'
271
+ `onB` renders the raw table name — so a chain off a duplicated table binds to the wrong copy (the
272
+ duplicate-join hazard in [tableview-joins](./tableview-joins.md)). A calc field sidesteps the alias
273
+ problem entirely. Column header labeled via a `ClientCustomRecordFieldSettings` `label-singular`
274
+ row ("Location"). (jcardinal)
256
275
  - 2026-09-10 — **Fixed `_Model_Client_Location::_addressState()` to return the state CODE, not
257
276
  `Addresses.stateId`** (addresses rendered as `Bronx, 33 10451-5504`). Added
258
277
  `LEFT JOIN States ON States.id = Addresses.stateId` inside the subquery and selected `States.code`
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-26
10
- owners: [tcox, bala]
9
+ updated: 2026-09-10
10
+ owners: [tcox, bala, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Core/Page.php
13
13
  - _underscore/Model/Client/TableView.php
@@ -64,6 +64,19 @@ record route resolved through `$lookupRecordIdFromCustomRecordFieldId` →
64
64
  metadata**, and no amount of adding/correcting those rows will move the header. (Measured
65
65
  2026-08-26: two hours lost adding label rows that could never be read.)
66
66
 
67
+ > **⚠ SCOPE — this "dead metadata" is the legacy `Page.php` / `toga2-supply` path ONLY. On the
68
+ > `toga25-supply` table path it WORKS.** `toga25-supply` reads its grid headers from
69
+ > `/table-views/meta` (`_Model_Client_TableView::meta()`), which fetches
70
+ > `ClientCustomRecordFieldSettings` directly — so a **base** custom-field column (both context
71
+ > columns NULL) IS labeled by a `ClientCustomRecordFieldSettings` row with `settingId = 5`
72
+ > (`Core.Settings.slug = 'label-singular'`), `isOverridable = 1`. Verified live 2026-09-10: this is
73
+ > exactly how `Client_Elite` labels its custom unit columns (`c_grade` → "Grade", `c_chargingBrick`
74
+ > → "Charging Brick", …), and it is how NYCHH's `_transferLocation` column on
75
+ > `units-for-items-for-purchase-orders` gets its "Location" header
76
+ > (`Client_Nychh/2026-09-10c - TransferLocationColumnLabel.sql`). Do **not** move a custom field to
77
+ > a Core `RecordField` just to get a header on a `toga25-supply` grid — the client custom-field
78
+ > settings row is enough. The dead-metadata trap above still holds for the old `Page.php` consumers.
79
+
67
80
  Where the label must come from instead, for such a column:
68
81
 
69
82
  - **`Core.DefaultRecordFieldSettings`** on the underlying **Core** `RecordField`, or
@@ -181,6 +194,14 @@ data change, is what makes it fatal.
181
194
  customization. The guard belongs in the framework.
182
195
 
183
196
  ## Change history
197
+ - 2026-09-10 — Scoped the "custom record field's label row is dead metadata" finding to the **legacy
198
+ `Page.php` / `toga2-supply`** consumer only. On the **`toga25-supply`** grid (headers from
199
+ `/table-views/meta` via `_Model_Client_TableView::meta()`, which fetches
200
+ `ClientCustomRecordFieldSettings` directly) a **base** custom-field column IS labeled by a
201
+ `ClientCustomRecordFieldSettings` row (both context columns NULL, `settingId = 5` =
202
+ `label-singular`, `isOverridable = 1`) — so the field need NOT be promoted to a Core `RecordField`
203
+ for a header. Verified against `Client_Elite`'s custom unit columns and NYCHH's `_transferLocation`
204
+ ("Location") on the units-for-items-for-purchase-orders view. (jcardinal)
184
205
  - 2026-08-26 - Documented that a grid column's **type** and its **header label** come from two
185
206
  different endpoints: type/formatting (the currency icon) from `/table-views/meta`
186
207
  `table.fields[].type` via `generateDynamicConfig` → `formatTableData`, header text from
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-03
9
+ updated: 2026-09-10
10
10
  owners: [bala, apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
@@ -19,6 +19,8 @@ files:
19
19
  - dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
20
20
  - dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
21
21
  - dbchanges2/Client_Nychh/2026-09-02a - PurchaseOrderItemsTableViewRebase.sql
22
+ - dbchanges2/Client_Nychh/2026-09-10a - UnitsForPoTableViewRebaseAndTransferLocationField.sql
23
+ - dbchanges2/Client_Nychh/2026-09-10b - UnitsForPoBridgeBaseAclGrant.sql
22
24
  related:
23
25
  - ../../api2/features/tableview-field-metadata.md
24
26
  - ./page-meta-context-field-settings.md
@@ -232,6 +234,29 @@ So a role gap only 500s when a **visible column** hangs off the missing join —
232
234
  client tolerates ACL gaps on records another client cannot. A new joined table (here
233
235
  `Manufacturers`, record 20) needs its `AclRecordPermissions` grant in the same migration.
234
236
 
237
+ #### A dropped BRIDGE join in the middle of a chain 1054s even with NO column on it
238
+
239
+ The "harmless until a column hangs off it" rule has a sharp exception: a **bridge** join that sits
240
+ **in the middle of a chain**. Nothing shows a column from the bridge, but a **later** join's `onB`
241
+ names the bridge's raw table. When ACL drops the bridge, that later join emits an ON clause
242
+ referencing a table that was never added → **MySQL 1054**, not the readable
243
+ `AclRecordPermissions`-message from mode (b).
244
+
245
+ Confirmed NYCHH 2026-09-10 on the rebuilt `units-for-items-for-purchase-orders` view. The bridge
246
+ record **`PurchaseOrderItems_SalesOrderItems`** (Core record **290**) carried only an **API-role**
247
+ read grant in `Client_Nychh`. Records 30/29/15/18 in the chain already had a **Base** grant, so a
248
+ Base user's joins query dropped only the bridge — and the next join's ON referenced it:
249
+
250
+ ```
251
+ Unknown column 'PurchaseOrderItems_SalesOrderItems.purchaseOrderItemId' in 'on clause'
252
+ ```
253
+
254
+ Fix: add the **Base** read grant as the full 4-table ACL chain (`AclRecordPermissions` →
255
+ `AclLogicGroups` → `AclLogicGroupExpressions`), reusing record 290's existing `all`
256
+ `AclRecordExpression` (`Client_Nychh/2026-09-10b - UnitsForPoBridgeBaseAclGrant.sql`). Watch for
257
+ **bridge records that ship with API-only grants** — a view that only ever ran for an API caller
258
+ (or in an environment tested as API) hides this until a Base user opens it.
259
+
235
260
  ### The debugging signature: 500 + EMPTY body + NO row in `Logs_<client>.Api`
236
261
 
237
262
  Mode (a) throws *before* the response is assembled, so the caller gets a **500 with an empty
@@ -347,6 +372,14 @@ Core-level reference ids used (identical across all clients and environments): *
347
372
 
348
373
  ## Change history
349
374
 
375
+ - 2026-09-10 — Added the **bridge-in-a-chain ACL-drop = 1054** exception to mode (b): a dropped
376
+ bridge join with **no column on it** still breaks, because the next join's `onB` names the bridge's
377
+ raw table. NYCHH's rebuilt `units-for-items-for-purchase-orders` view 1054'd for a **Base** user —
378
+ bridge record 290 (`PurchaseOrderItems_SalesOrderItems`) had only an **API-role** grant while the
379
+ rest of the chain (30/29/15/18) had Base; error *"Unknown column
380
+ 'PurchaseOrderItems_SalesOrderItems.purchaseOrderItemId' in 'on clause'"*. Fixed with the full
381
+ 4-table Base ACL chain reusing record 290's `all` expression (`Client_Nychh/2026-09-10b`). Watch
382
+ for bridge records that ship with API-only grants. (jcardinal)
350
383
  - 2026-09-03 — Added the **filter-only join counterpart** to the PO-items rebase: dropping a
351
384
  column-less join that only *filtered* rows (SalesOrders→bridge→PurchaseOrders, ids 123/124/141)
352
385
  breaks any row-filter still targeting that table. NYCHH's `items-for-purchase-orders`
@@ -6,12 +6,18 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: ["bala", "tcox"]
9
+ updated: 2026-09-10
10
+ owners: ["bala", "tcox", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Model/Client/ServiceRequest.php
13
+ - _underscore/Model/Nychh/Unit.php
13
14
  - dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
15
+ - dbchanges2/Client_Nychh/2026-09-10a - UnitsForPoTableViewRebaseAndTransferLocationField.sql
16
+ - dbchanges2/Client_Nychh/2026-09-10b - UnitsForPoBridgeBaseAclGrant.sql
17
+ - dbchanges2/Core/2026-09-10a - InventoryGroupingsUnitsLeafPoItemFilter.sql
14
18
  related:
19
+ - ./calculated-sql-fields.md
20
+ - ./tableview-joins.md
15
21
  - ./tracking-number-bridges.md
16
22
  - ../../../clients/elite/features/supply2-tableview-config-drift.md
17
23
  - ../../../clients/prudential/features/supply-orders-csv-export.md
@@ -74,6 +80,41 @@ a *different field per client*, so client custom columns (and their
74
80
  Elite's four columns at index 14-17 (`grade`, `chargingBrick`, `chargingCable`, `wipeStatus` via
75
81
  `c_deviceStatus`).
76
82
 
83
+ ## NYCHH rebuilt the view onto the FULFILLMENT chain (2026-09-10)
84
+
85
+ This is the "resolver/view change that also walks the fulfillment path" the first gotcha below asks
86
+ for — shipped for one tenant. `Client_Nychh`'s `units-for-items-for-purchase-orders` view (base
87
+ record **Units 31**) was rebuilt to list the serialized units for a clicked **purchase-order item**,
88
+ showing Serial #, Asset Tag, Disposition, Location. The old 25-join / 12-column config (inaccurate)
89
+ was replaced with an **INNER** chain:
90
+
91
+ ```
92
+ Units
93
+ -> ItemFulfillmentItemUnits (30)
94
+ -> ItemFulfillmentItems (29)
95
+ -> PurchaseOrderItems_SalesOrderItems bridge (290)
96
+ -> PurchaseOrderItems (18)
97
+ ```
98
+
99
+ plus a **filter-only** `SalesOrderItems (15)` join kept so the `itemsByPOs` drill path (which filters
100
+ by `SalesOrderItems.uuid`) still works. The grid is filtered at drill-down time by
101
+ `(PurchaseOrderItems.uuid:eq:...)`. File:
102
+ `dbchanges2/Client_Nychh/2026-09-10a - UnitsForPoTableViewRebaseAndTransferLocationField.sql`.
103
+
104
+ Two column values come from **NYCHH-only calc fields** on `_Model_Nychh_Unit`, not joins —
105
+ Disposition (`_unitDisposition`, overridden to derive from the transfer chain) and Location
106
+ (`_transferLocation`, new). Location is a calc field precisely because its chain re-uses
107
+ `ItemFulfillmentItems`/`ItemFulfillmentItemUnits` a **second** time, which the TableView alias model
108
+ cannot express (see [calculated-sql-fields](./calculated-sql-fields.md) and the duplicate-join hazard
109
+ in [tableview-joins](./tableview-joins.md)).
110
+
111
+ Two failures surfaced during this rebuild, both documented on
112
+ [tableview-joins](./tableview-joins.md): the mid-chain bridge record 290 shipped with an **API-only**
113
+ ACL grant, so a Base user 1054'd on `PurchaseOrderItems_SalesOrderItems.purchaseOrderItemId` in the
114
+ next join's ON clause (fixed by `Client_Nychh/2026-09-10b`); and the Inventory `posByItems` **leaf**
115
+ `additionalDataSlug` had to be the bare table name `PurchaseOrderItems` (the FE appends `.uuid`, so a
116
+ stored `PurchaseOrderItems.uuid` doubled to `.uuid.uuid` and 500'd — `Core/2026-09-10a`).
117
+
77
118
  ## Serial numbers for a Service Request - the bridge table is empty, use the fulfillment chain
78
119
 
79
120
  `_Model_Client_ServiceRequest::_serialNumbers` (a `FIELD_SQL` field, so every client inherits it)
@@ -143,7 +184,10 @@ None — uniform. The linkage structure is framework-level and identical across
143
184
  - **Missing item bridge → no units**, even when units were received. Without a
144
185
  `SalesOrderItems_PurchaseOrderItems` row, the receipt path has no starting link.
145
186
  - **A present bridge is not sufficient.** If the bridged PO line has no
146
- `ItemReceiptItemUnits`, the receipt path still resolves to nothing.
187
+ `ItemReceiptItemUnits`, the receipt path still resolves to nothing. The **fulfillment-chain**
188
+ analog (NYCHH rebuild): a PO line with a bridge **and** a fulfillment but **zero
189
+ `ItemFulfillmentItemUnits`** (no serialized units captured) correctly shows an **empty grid** — that
190
+ is not a bug, just a line with no serials recorded.
147
191
  - **`inventoryType` determines unit existence**: `SERIALIZED` and `HYBRID` items can produce
148
192
  unit records; `NON_SERIALIZED` (bulk/consumable) never do. `HYBRID` items fulfilled as a
149
193
  plain quantity (no serial capture) also produce no units — legitimately blank.
@@ -172,6 +216,15 @@ None — uniform. The linkage structure is framework-level and identical across
172
216
  rather than reading the SQL.
173
217
 
174
218
  ## Change history
219
+ - 2026-09-10 - `Client_Nychh` **rebuilt this view onto the fulfillment chain** (Units → 30 → 29 →
220
+ 290 bridge → PurchaseOrderItems 18, filtered by `(PurchaseOrderItems.uuid:eq:...)`), the first
221
+ "resolver that walks the fulfillment path" — lists serialized units for a clicked PO item with
222
+ Serial #, Asset Tag, Disposition, Location. Disposition/Location come from NYCHH-only
223
+ `_Model_Nychh_Unit` calc fields (`_unitDisposition` override, new `_transferLocation`) because the
224
+ location chain re-uses `ItemFulfillmentItems` twice. Added the empty-grid gotcha (bridge +
225
+ fulfillment but zero `ItemFulfillmentItemUnits` = correctly blank). Cross-refs the bridge Base-ACL
226
+ 1054 and the surface-leaf slug-doubling fixes on [tableview-joins](./tableview-joins.md). Files:
227
+ `Client_Nychh/2026-09-10a`, `Client_Nychh/2026-09-10b`, `Core/2026-09-10a`. (jcardinal)
175
228
  - 2026-08-28 - Corrected the `ServiceRequests_Units` "0 rows" note: that is an **Elite** measurement,
176
229
  not a schema fact - `Client_Prudential` has **30,268 rows** and the bridge is the only unit-to-order
177
230
  route there for never-shipped units (`SalesOrderItems_CommittedUnits` is the empty one on Prudential,
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-25
9
+ updated: 2026-09-10
10
10
  owners: [mhammontree, tcox, jcardinal, ajean, bala, apeterson]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -140,6 +140,13 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
140
140
  `error.id` like `"8K-6"`. The part **before the dash** is `Logs.Issue.reference` —
141
141
  `SELECT * FROM Logs.Issue WHERE reference = '8K'` gives the full `errorMessage` and stack
142
142
  trace. The envelope itself tells you nothing useful; always go to `Logs.Issue`.
143
+ **A developer will often hand you this id verbally (e.g. "V7-6") to investigate — split on the
144
+ hyphen: the first part (`V7`) is `Logs.Issue.reference`, the second (`6`) is
145
+ `Logs.Event.eventNumber`. The two tables join on `Logs.Event.issueId = Logs.Issue.id`, so the
146
+ exact occurrence (and its `Logs.Event.clientId` tenant) is
147
+ `SELECT * FROM Logs.Event JOIN Logs.Issue ON Logs.Event.issueId = Logs.Issue.id WHERE
148
+ Logs.Issue.reference = 'V7' AND Logs.Event.eventNumber = 6`.** (`Logs.Issue` has no `dtUpdated`
149
+ column.)
143
150
  Corollary: if `transactionId`, `authority` and `audience` are **all NULL** in the error
144
151
  envelope, the request failed **before client context was established**.
145
152
 
@@ -292,6 +299,10 @@ Full mechanics:
292
299
  - The response-envelope shape and code families: see [api2 architecture](../architecture.md).
293
300
 
294
301
  ## Change history
302
+ - 2026-09-10 — Made the developer-shorthand decode of an `error.id` explicit under diagnosis note 9:
303
+ a verbally-provided id like "V7-6" splits into `Logs.Issue.reference` (before the hyphen) +
304
+ `Logs.Event.eventNumber` (after), joined by `Logs.Event.issueId = Logs.Issue.id`; added the ready
305
+ `Logs.Event JOIN Logs.Issue` lookup and noted `Logs.Issue` has no `dtUpdated` column. (jcardinal)
295
306
  - 2026-08-27 — Added three codes/notes from the NYCHH transfer-order POST work: **EV-10** (a NOT NULL
296
307
  column received NULL — `originLocationId cannot be null`; resolve upstream or default in the ORM), a
297
308
  second **EV-12** flavor (`searchableIdentifierFields:[id]` — a nested FK resolved *through* a `c_`
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
10
- owners: [rgirish]
9
+ updated: 2026-09-10
10
+ owners: [rgirish, jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Clickup/Fluffer.php
13
13
  - worker2/Worker/Clickup.php
@@ -121,6 +121,14 @@ never by value.
121
121
 
122
122
  ## Gotchas / known issues
123
123
 
124
+ - **⚠ OPEN (found 2026-09-10, NOT fixed): the Talos agent call is failing in prod with HTTP 403.**
125
+ `Clickup/Fluffer/Process` failed **168 times over 7 days** with `Talos agent returned HTTP 403` — an
126
+ auth/permission failure on the `POST /api/ai/agent` call (`callTalosAgent()`,
127
+ `worker2/Worker/Clickup/Fluffer.php` ~L230, from `Process()` ~L94). 403 points at a bad/expired
128
+ `[talos] agent_api_key` (the `dev-core`-scoped key sent as `X-API-Key`) or the agent bridge revoking
129
+ it — **not** a ClickUp problem. Left unfixed by the developer for now; recorded so the next triage
130
+ starts from "check the `agent_api_key` / agent-bridge auth", not from the code. Surfaced by the
131
+ `Core.WorkerJobs` action+outcome census (group by action, isSuccess over 7 days).
124
132
  - **Watchdog race.** The fluffer is an **ACTION** job; its watchdog is the Lambda env
125
133
  `MAX_EXECUTION_TIME_WORKER_ACTION` (default 300s). The Talos timeout is set to **280** (just
126
134
  under 300) so the job doesn't lose the race against its own watchdog out of the box.
@@ -144,6 +152,11 @@ never by value.
144
152
 
145
153
  ## Change history
146
154
 
155
+ - 2026-09-10 — **Discovered (NOT fixed): the fluffer is down in prod on a Talos-agent HTTP 403.**
156
+ `Clickup/Fluffer/Process` failed 168x over 7 days with `Talos agent returned HTTP 403` — an
157
+ auth/permission failure on the `/api/ai/agent` call, most likely an expired/wrong `[talos]
158
+ agent_api_key`. Deferred by the developer; logged as an open known issue for the next triage. Found
159
+ via the `Core.WorkerJobs` action+outcome census (toga-db MCP, no ClickUp/Talos access). (jcardinal)
147
160
  - 2026-07-20 — Built the ClickUp task description fluffer: new `_Worker_Clickup_Fluffer`
148
161
  (action `Clickup/Fluffer/Process`, `taskCreated` webhook auto-trigger, `force` regenerate)
149
162
  calling the Talos `dev-core` KB-investigation agent at `/api/ai/agent` via `_ApiRequest`
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-04
10
- owners: ["dfranks", "kyalamarthi", "ajean"]
9
+ updated: 2026-09-10
10
+ owners: ["dfranks", "kyalamarthi", "ajean", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite.php
13
13
  - worker2/Worker/Netsuite/Opportunity.php
@@ -387,6 +387,22 @@ the legacy cron's OPPORTUNITIES section:
387
387
  (`hasEndCustomerColumn()`), since the migration is applied by hand and an unknown column would fail
388
388
  the whole query and break every opportunity webhook. See
389
389
  [the labels doc](./netsuite-opportunity-client-labels.md).
390
+ - **⚠ `hasEndCustomerColumn()` guards the DB COLUMN, NOT the ORM MODEL field — and that gap took the
391
+ whole opportunity sync down for 8 days (2026-09-02 → 2026-09-10).** The guard does a `SHOW COLUMNS`
392
+ on `Forecast.Opportunities`, so once the migration was applied it passed and the handler ran
393
+ `$opportunity->endCustomerName = …`. But the 2.0 model `_Model_Forecast_Opportunity`
394
+ (`_underscore/Model/Forecast/Opportunity.php`) never **declared** the field, so `_Model.__set`
395
+ (`_underscore/Model.php`) threw *"There is no field called 'endCustomerName' in the
396
+ '_Model_Forecast_Opportunity' model."* on **every** `Netsuite/Opportunity/post|put` — 100% failure,
397
+ every opportunity import dead. `Core.WorkerJobs` census: Sep 1 = 115 ok / 60 fail (last good day),
398
+ Sep 2 → Sep 10 = 0 ok / 100% fail (61 fails on Sep 10 alone). This was a **half-deploy**: the
399
+ migration + the handler write shipped, the model field declaration did not.
400
+ **Durable rule: a new column that the handler writes must ship THREE things together — the
401
+ `dbchanges2` migration, the handler write, AND the `_Model` field declaration.** A column-existence
402
+ guard (`SHOW COLUMNS`) proves nothing about the ORM; both ORMs enumerate *declared* properties, so an
403
+ undeclared field is a hard `__set` throw, not a silent skip. Fixed 2026-09-10 by declaring
404
+ `public $endCustomerName = self::FIELD_CHAR;` (matching sibling char fields clickupTaskId/title/memo
405
+ over the varchar column); deployed to worker2 `_production`, sync recovered.
390
406
  - Writes use raw `_Query` against `_underscore::DB_FORECAST` and **must**
391
407
  `_Database::transactionCommit(DB_FORECAST)` (lazy-transaction gotcha).
392
408
 
@@ -759,6 +775,19 @@ deprecated** for production opportunity code.
759
775
  blocked on the Aaron stakeholder decision noted above.
760
776
 
761
777
  ## Change history
778
+ - 2026-09-10 — **Fixed an 8-day total opportunity-sync outage caused by a half-deploy: a missing ORM
779
+ model field.** `Forecast.Opportunities.endCustomerName` (column added ~Sep 1) and the worker2 handler
780
+ write shipped, but `_Model_Forecast_Opportunity` (`_underscore/Model/Forecast/Opportunity.php`) never
781
+ **declared** the field. `hasEndCustomerColumn()` guards the DB column (`SHOW COLUMNS`), not the ORM
782
+ model, so the guard passed and `_Model.__set` then threw *"There is no field called 'endCustomerName'
783
+ in the '_Model_Forecast_Opportunity' model."* on **every** `Netsuite/Opportunity/post|put` —
784
+ `Core.WorkerJobs` shows Sep 1 = 115 ok / 60 fail (last good day), Sep 2 → Sep 10 = 0 ok / 100% fail.
785
+ Fix: declared `public $endCustomerName = self::FIELD_CHAR;` (php-reviewer clean); deployed to worker2
786
+ `_production`, sync recovered. Durable lesson added to the `endCustomerName` data-model bullet: a
787
+ handler-written column must ship its migration, its handler write, AND its `_Model` field declaration
788
+ together — a `SHOW COLUMNS` guard does not protect against a missing ORM field. Diagnosed with only the
789
+ `Core.WorkerJobs` action+outcome census (group by action, isSuccess over 7 days) via the toga-db MCP —
790
+ no NetSuite/ClickUp access. (jcardinal)
762
791
  - 2026-09-04 — **Investigation + design session; no code shipped (two prototype fixes were written and
763
792
  deliberately reverted).** Four corrections and four new facts. **Corrected:** the `STAGE_MAP`
764
793
  section described a constant that does not exist — the shipped map is **`SALES_STAGE_MAP`** on
@@ -33,6 +33,12 @@ files:
33
33
  - dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql
34
34
  - dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql
35
35
  - dbchanges2/Client_Nychh/2026-09-09c - UpdateLocationShippingAddresses.sql
36
+ - _underscore/Model/Nychh/Unit.php
37
+ - dbchanges2/Client_Nychh/2026-09-10a - UnitsForPoTableViewRebaseAndTransferLocationField.sql
38
+ - dbchanges2/Client_Nychh/2026-09-10b - UnitsForPoBridgeBaseAclGrant.sql
39
+ - dbchanges2/Client_Nychh/2026-09-10c - TransferLocationColumnLabel.sql
40
+ - dbchanges2/Core/2026-09-10a - InventoryGroupingsUnitsLeafPoItemFilter.sql
41
+ - dbchanges2/Core/2026-09-10b - UnitDispositionColumnLabel.sql
36
42
  related:
37
43
  - ./features/location-shipping-addresses.md
38
44
  - ../../2.0/apps/dbchanges2/workflows/deleting-contacts-from-a-client-database.md
@@ -206,6 +212,51 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
206
212
  `PurchaseOrders.id` 81, onB `PurchaseOrderItems.purchaseOrderId` 90, sortOrder 7, no column). No
207
213
  new ACL — record 17 already has Base/Developer/API read. Mechanism:
208
214
  [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md).
215
+ - **`units-for-items-for-purchase-orders` view rebuilt to list serials for a clicked PO item
216
+ (2026-09-10).** Base record Units (31). The old 25-join / 12-column config (inaccurate) was replaced
217
+ with an INNER chain **Units → ItemFulfillmentItemUnits (30) → ItemFulfillmentItems (29) →
218
+ PurchaseOrderItems_SalesOrderItems bridge (290) → PurchaseOrderItems (18)**, plus a **filter-only**
219
+ `SalesOrderItems (15)` join kept so the `itemsByPOs` drill (filters by `SalesOrderItems.uuid`) still
220
+ works. Filtered at drill-down by `(PurchaseOrderItems.uuid:eq:...)`. Columns: Serial #, Asset Tag,
221
+ Disposition, Location. File `Client_Nychh/2026-09-10a -
222
+ UnitsForPoTableViewRebaseAndTransferLocationField.sql`. Four connected pieces:
223
+ 1. **Two NYCHH-only calc fields on `_Model_Nychh_Unit`** (`_underscore/Model/Nychh/Unit.php`), both
224
+ plain `FIELD_SQL` text. **`_unitDisposition` is OVERRIDDEN** — NYCHH does **not** use the
225
+ `UnitDispositions` lookup; disposition is `Deployed` if an `ItemFulfillmentItems` with
226
+ `transferOrderItemId IS NOT NULL` exists for the unit, else `Available` (override matches the
227
+ parent's untyped signature exactly). **`_transferLocation` is NEW** — the transfer-order
228
+ destination location name via Units → ItemFulfillmentItemUnits → ItemFulfillmentItems →
229
+ TransferOrderItems → TransferOrders → destinationLocationId → Locations (`ORDER BY` fulfillment
230
+ id `DESC LIMIT 1`); registered as a `Client_Nychh.CustomRecordFields` row (record 31) + Base
231
+ `AclCustomFieldPermissions`. It is a calc field, **not** a join, because the chain re-uses
232
+ `ItemFulfillmentItems`/`ItemFulfillmentItemUnits` a second time and the TableView alias model
233
+ would bind to the wrong copy. See
234
+ [calculated-sql-fields](../../2.0/apps/_underscore/features/calculated-sql-fields.md).
235
+ 2. **Bridge Base ACL grant** — record 290 (`PurchaseOrderItems_SalesOrderItems`) shipped with only
236
+ an **API-role** read grant, so a Base user's joins query dropped the bridge and the next join's
237
+ ON errored `Unknown column 'PurchaseOrderItems_SalesOrderItems.purchaseOrderItemId' in 'on
238
+ clause'` (MySQL 1054). Fixed with the full 4-table Base ACL chain reusing record 290's `all`
239
+ expression: `Client_Nychh/2026-09-10b - UnitsForPoBridgeBaseAclGrant.sql`. Records 30/29/15/18
240
+ already had Base. Mechanism: [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md).
241
+ 3. **Surface drill-down LEAF slug** — the `inventory-list-groupings` `posByItems` **leaf**
242
+ `additionalDataSlug` had to be the bare table name `PurchaseOrderItems` (NOT
243
+ `PurchaseOrderItems.uuid`); the FE appends `.uuid` itself, so a stored `.uuid` doubled to
244
+ `.uuid.uuid` and 500'd (1054). Same doubling rule as the swap level above, one level deeper. This
245
+ is `Core.SurfaceElements` (all-client, no NYCHH override); `Core/2026-09-10a -
246
+ InventoryGroupingsUnitsLeafPoItemFilter.sql` (guard covers both `SalesOrderItems.uuid` and the
247
+ interim `PurchaseOrderItems.uuid`). Confirmed in toga25-supply
248
+ `useInventoryPageViewModel.tsx` (getAdditionalData).
249
+ 4. **Column labels (settings layer, `Core.Settings` id 5 = `label-singular`)** — Disposition via a
250
+ `Core.DefaultRecordFieldSettings` row on Core field 619 (`_unitDisposition`), value "Disposition"
251
+ (`Core/2026-09-10b - UnitDispositionColumnLabel.sql`); Location via a
252
+ `Client_Nychh.ClientCustomRecordFieldSettings` row on `_transferLocation` (context columns NULL,
253
+ settingId 5, `isOverridable 1`), value "Location" (`Client_Nychh/2026-09-10c -
254
+ TransferLocationColumnLabel.sql`). Without a label row the grid falls back to the raw
255
+ `TableViewFields.slug`.
256
+ - **Empty grid for a given PO item is correct** when that line has a bridge + fulfillment but zero
257
+ `ItemFulfillmentItemUnits` (no serialized units captured) — not a bug.
258
+ - **Deploy:** the `_underscore` model change must land on **`_production`** and **`_sandbox-client`**
259
+ (api2 pulls `_underscore` per environment by branch); the SQL files run on prod + client-sandbox.
209
260
  - **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
210
261
  and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
211
262
  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.795",
3
+ "version": "1.0.797",
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",