toga-ai 1.0.886 → 1.0.887

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: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish", "jcardinal"]
11
11
  files:
12
12
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
@@ -17,6 +17,7 @@ files:
17
17
  - _underscore/Model/Elite/TransferOrderStage.php
18
18
  related:
19
19
  - ../architecture.md
20
+ - ../../toga25-supply/features/bundle-item-pricing.md
20
21
  - ../../../1.0/apps/library/features/netsuite-item-assettype-sync.md
21
22
  - ../../toga2-supply/features/currency-amount-lines-editor.md
22
23
  - cxml-shipnotice-gateway.md
@@ -157,6 +158,22 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
157
158
 
158
159
  `recordId 351` is `transfer-order-stages`; the equivalent row exists per tenant with a **different `id`** but often the **same `uuid`** (both Elite and NYCHH carry `b273f4d1-dac0-4093-ad1d-7c3a526ab894`), the tell that the row came from one shared seed and only one tenant's copy has the wrong flag.
159
160
 
161
+ ### ⚠ An inherent child nested in a **PUT** is ALWAYS written as a CREATE
162
+
163
+ The engine recurses into a nested one-to-many child (`Core.InherentRecordChildren`) with
164
+ `httpMethod: 'POST'` **hardcoded** (`V2.php:5415`). There is no uuid check and no PUT branch, so a
165
+ child nested in an update is never updated:
166
+
167
+ | Nested child payload on a `PUT` | Result |
168
+ |---|---|
169
+ | no `uuid` | a **duplicate row** is inserted; the parent looks unchanged |
170
+ | `uuid` of the existing row | fails — `EV-10` / MySQL `Error #: 1062 Duplicate entry … for key '<Table>_UNIQUE'` |
171
+
172
+ **Correct pattern: nest children only on a CREATE.** To change an existing child, send its own
173
+ `PUT /v2/<child-route>/{uuid}` after the parent write (two calls). This is general engine
174
+ behaviour — it bites any write to a nested child, not one feature. Worked example:
175
+ [bundle item pricing](../../toga25-supply/features/bundle-item-pricing.md).
176
+
160
177
  ### Nested inherent-child create — parent FK is auto-set, any depth
161
178
 
162
179
  - V2 auto-sets the child's FK to the parent: first field whose `FIELDOPT_FOREIGNKEY_MODEL` equals the parent class (exact normalized `_Model_Client_` string), then break (`V2.php` ~5211-5221). Payload supplies the OTHER FK only (e.g. bridge row `{ purchaseOrder: { uuid } }` under a transfer order).
@@ -166,6 +183,7 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
166
183
 
167
184
  ## Gotchas
168
185
 
186
+ - **⚠ A nested inherent child on a PUT is created, never updated** (`V2.php:5415` hardcodes `POST`) — you get a duplicate row, or a 1062 duplicate-key error if you send the child's `uuid`. Update children through their own route.
169
187
  - **Nested write with only a non-identifier field silently creates duplicates.** A duplicate-generating footgun: each write inserts a new empty child and the parent links to a fresh shell (or is left with a NULL FK). It compounds on any cron/integration that writes the same entity repeatedly. Root-caused in production on the Prudential device-sync cron, which sent `contact: {c_employeeXid}` and generated ~893k empty Contact rows (~5,600/day). See [Prudential device import + contact linking](../../../clients/prudential/features/device-information-import-and-contact-linking.md).
170
188
  - **A matched nested object is UPDATED, not just linked** — partner data can mutate shared catalog rows. Matching is not read-only.
171
189
  - **A `NULL` `overrideIsIdentifier` unsets the field as an identifier** — a row added only to set a child policy silently strips it, causing EV-12. Correct only where `Core.RecordFields.isIdentifier = 1`; ship it as a `dbchanges2/Client_<Tenant>/` migration (a manual non-prod patch evaporates on reseed).
@@ -177,6 +195,11 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
177
195
  - **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
178
196
  - **Singular-FK back-reference defects:** UPDATE path drops the parent's forward FK (echoes `null`); CREATE path re-parents a value-matched child to the newest writer (stolen child row). One defect, two symptoms.
179
197
 
198
+ ## Change history
199
+ - 2026-09-24 — Documented that the engine recurses into a nested inherent child with `POST`
200
+ hardcoded (`V2.php:5415`), so a child nested in a PUT is duplicated or fails 1062; update
201
+ children through their own route (TRUE-82138). (apeterson)
202
+
180
203
  ## Related
181
204
 
182
205
  - [api2 architecture](../architecture.md) — the V2 CRUD engine (`processRoutePairs`).
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: [tcox, bala, apeterson, jcardinal, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -64,6 +64,24 @@ controller.
64
64
  the join is genuinely 1:1, or the duplicates agree. Worked example:
65
65
  [category tile sort order](../../toga2-commerce/features/category-tile-sort-order.md).
66
66
 
67
+ ### ⚠ `depth` and `calcDepth` are IGNORED inside the base64 `_` options blob
68
+
69
+ `fields`, `where`, `sort`, `join` work inside the `_` blob. **`depth` and `calcDepth` do not** —
70
+ they are only read as plain query params. Silent: no error, the request just runs at the defaults
71
+ (`depth 3`, `calcDepth 1`), so nested calculated fields come back absent and the engine looks
72
+ incapable of returning them. Mixing is fine — put the blob in `_` and add `&calcDepth=2`.
73
+ (toga-blox `assembleOptions` already appends any unrecognised option key to the query string,
74
+ which is why mixed requests work from the FE.) Verified 2026-09-24 (TRUE-82138).
75
+
76
+ ### ⚠ A field THREE levels under an inherent child is silently DROPPED, at any depth
77
+
78
+ Distinct from the max-depth omission above — raising `depth` does not help. A field two hops below
79
+ an inherent child (`bundleItems.bundleItemPrices.priceType` on `GET /v2/bundles/{uuid}`) is absent
80
+ from the payload with no error and no message; the same field is returned by the shallower
81
+ `GET /v2/bundle-items/{uuid}`. **Consequence:** a caller cannot tell one child row from another
82
+ (e.g. a unit-price row from a lease-price row) on the parent payload, and must not guess — fetch
83
+ the child route instead.
84
+
67
85
  ### How `sort` becomes `ORDER BY` — and why "descending sort is broken" is usually just NULLs
68
86
 
69
87
  `ORDER BY` is a **plain string concatenation** of the resolved column and `ASC`/`DESC` — no cast, no
@@ -565,6 +583,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
565
583
  - **⚠ A 500 / `EO-1` on a `where` over a calculated field ⇒ you table-PREFIXED the field name.**
566
584
  Drop the prefix: `_qtyAvailable`, not `PurchaseOrderItems._qtyAvailable`. The prefixed form is
567
585
  routed to a `HAVING` against an alias that does not exist.
586
+ - **⚠ `depth`/`calcDepth` inside the `_` blob do nothing** — send them as plain query params.
568
587
  - **⚠ Do not raise `calcDepth` for a calculated field you named in `fields=`** — it is classified
569
588
  before the gate and already resolves at the default `1`.
570
589
  - **Do not trust a 200 with missing fields.** Without an explicit `fields=` list, ACL removal is
@@ -585,6 +604,9 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
585
604
  [V2 request logging](request-logging.md).
586
605
 
587
606
  ## Change history
607
+ - 2026-09-24 — Recorded that `depth`/`calcDepth` are ignored inside the base64 `_` options blob
608
+ (plain query params only), and that a field three levels under an inherent child is silently
609
+ dropped at any depth (TRUE-82138). (apeterson)
588
610
  - 2026-09-23 — Documented how `sort` becomes `ORDER BY` (plain concatenation, no cast or NULL
589
611
  handling), that the same expression doubles as the keyset-pagination cursor so any change must be
590
612
  mirrored in the seek `WHERE`/`getSeekFieldType()`, and that "descending sort is broken" reports are
@@ -4,6 +4,7 @@
4
4
  |-----|---------|
5
5
  | [TOGa 2.5 Supply — Architecture](architecture.md) | Architecture of toga25-supply — the React/TS Vite SPA frontend for 2.0 Supply, multi-client + meta-driven, built on `@agilant/toga-blox`; open before any struct |
6
6
  | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | Declarative config-driven Flag/Rule grammar that resolves `isEnabled`/`isVisible`/`isComplete` on SalesOrder action-button options and approval stage inputs (ev |
7
+ | [Bundle item pricing (unit / lease rows, edit save path, duplicate rows)](features/bundle-item-pricing.md) | How a bundle item's unit/lease price is read, displayed and SAVED in supply — the two-call save (never nest the price in a PUT), `_unitPrice`/`_leasePrice` + `c |
7
8
  | [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | Resolves a client's API-fetch projection (`fields`/`ojoin`/`join`/`where`) from one colocated per-page `apiFields.json` keyed by client slug (useClientApiFields |
8
9
  | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | Config that varies by client (or client × role) — field overrides, filter buttons, group-by, column pickers, layout toggles — resolved through `src/fieldsConfig |
9
10
  | [Column State (URL-driven show/hide, order & pinning)](features/column-visibility.md) | URL-driven "Columns" button + modal (`cols` param) for show/hide + order + pinning of table columns, client-configurable then Surface-driven; open for column-vi |
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Bundle item pricing (unit / lease rows, edit save path, duplicate rows)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-24
10
+ owners: ["apeterson"]
11
+ files:
12
+ - toga25-supply/src/layout/BundleItemRecordModalLayout/hooks/useBundleItemEditForm.tsx
13
+ - toga25-supply/src/layout/BundleRecordModalLayout/types.ts
14
+ - toga25-supply/src/layout/BundleRecordModalLayout/viewModel/FIELDS/DEFAULT/bundleRecordViewFields.json
15
+ - toga25-supply/src/layout/BundleRecordModalLayout/components/BundleItemsTable.tsx
16
+ - dbchanges2/Client_Compass/2026-09-24a - DeduplicateBundleItemPrices.sql
17
+ related:
18
+ - ../../api2/features/nested-relationship-writes.md
19
+ - ../../api2/features/v2-rest-query-contract.md
20
+ - ../../_underscore/features/calculated-sql-fields.md
21
+ - ./record-modals-and-nested-tables.md
22
+ - ../../../clients/compass-usa/workflows/changing-a-kit-line-item.md
23
+ ---
24
+
25
+ How a bundle item's unit/lease price is read, displayed and SAVED in supply — the two-call save (never nest the price in a PUT), `_unitPrice`/`_leasePrice` + `calcDepth: 2`, and the duplicate `BundleItemPrices` rows.
26
+
27
+ ## Summary
28
+
29
+ A bundle item has **many** `BundleItemPrices` rows, one per `priceTypeId` (unit, lease-48, …).
30
+ Supply shows Default Price Type / Unit Price / Lease Price in the bundle items table and edits a
31
+ price in the bundle-item modal. Two engine behaviours shape all of it: nested children are never
32
+ updated on a PUT, and the calculated price fields need `calcDepth: 2` when read through a bundle.
33
+
34
+ ## How it works
35
+
36
+ ### Reading — `_unitPrice` / `_leasePrice`
37
+
38
+ Both are `FIELD_SQL` calculated fields on `_Model_Client_BundleItem`: they resolve the row
39
+ matching `defaultPriceTypeId` with `ORDER BY id DESC LIMIT 1` (**newest** row wins).
40
+
41
+ - The table columns are sourced from these fields, not from the `bundleItemPrices` array.
42
+ - For **nested** bundle items (inside `GET /v2/bundles/{uuid}`) they need **`calcDepth: 2`**, sent
43
+ as a **plain query param** — inside the base64 `_` blob it is ignored. At the default `calcDepth: 1`
44
+ both fields are silently absent. See
45
+ [V2 REST query contract](../../api2/features/v2-rest-query-contract.md).
46
+ - `priceType` on a nested price row is **never returned** by `GET /v2/bundles/{uuid}` (three levels
47
+ under an inherent child — dropped silently). Re-fetch `GET /v2/bundle-items/{uuid}` when you need
48
+ typed rows.
49
+
50
+ ### Saving — two calls, never a nested price in a PUT
51
+
52
+ The V2 engine writes a nested inherent child with `POST` hardcoded, so a price nested in a **PUT**
53
+ inserts a duplicate row (or fails 1062 if you send its uuid) —
54
+ [nested-relationship writes](../../api2/features/nested-relationship-writes.md).
55
+
56
+ 1. `PUT /v2/bundle-items/{uuid}` — **without** the nested price.
57
+ 2. `PUT /v2/bundle-item-prices/{uuid}` — the price row.
58
+
59
+ **Create still nests** (correct there — it is a POST).
60
+
61
+ `resolvePriceRowUuid` picks the row to update **strictly by `priceType`**. If the payload rows carry
62
+ no `priceType`, it re-fetches typed rows from `/bundle-items/{uuid}`; if the type still cannot be
63
+ established it **creates a new row** rather than overwriting an unknown one. Never fall back to
64
+ "the newest row" — during a unit-price edit that can overwrite the LEASE price.
65
+
66
+ ## Gotchas
67
+
68
+ - **⚠ An edited price that looks unchanged on screen is the duplicate-row bug.** `_unitPrice` reads
69
+ the newest row (`ORDER BY id DESC`) while the FE rendered `bundleItemPrices[0]` (the oldest) — the
70
+ insert was invisible in the UI for a long time.
71
+ - **⚠ Duplicate `BundleItemPrices` rows already exist in production data**, predating this front end
72
+ (one bundle item has 15 rows, ids 13→4355). The table has no `dtCreated`/`dtUpdated`, so age cannot
73
+ be proven. Local-copy counts: `Client_Compass` 5117 rows / 2605 duplicates / 971 groups (36 groups
74
+ with differing prices); `Client_CompassCanada` 333 / 75 / 63; all other tenants zero. **Prod counts
75
+ not yet taken.**
76
+ - **Dedupe migrations exist but have only been run locally** —
77
+ `dbchanges2/Client_Compass/2026-09-24a - DeduplicateBundleItemPrices.sql` and the
78
+ `Client_CompassCanada` twin keep `MAX(id)` per `(bundleItemId, priceTypeId)` — the row the
79
+ calculated fields already read, so nothing changes on screen. Safe to run: no foreign key anywhere
80
+ references `BundleItemPrices`, and `SalesOrderItems` snapshots its own price column, so no order or
81
+ quote is affected. `currencyId`/`planId` are NULL on every row.
82
+ - **⚠ FIELDS JSON edits appear to do nothing** — the persisted React Query cache freezes them; see
83
+ [persisted query cache](./persisted-query-cache.md).
84
+
85
+ ## Change history
86
+ - 2026-09-24 — Documented the two-call save (price edits were inserting duplicates via a nested PUT),
87
+ the strict-`priceType` row match with create-not-guess fallback, the `_unitPrice`/`_leasePrice`
88
+ columns needing `calcDepth: 2` as a plain query param, and the Compass/Compass Canada
89
+ `BundleItemPrices` dedupe migrations (TRUE-82138). (apeterson)
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-09-24
10
10
  owners: ["apeterson"]
11
11
  files:
12
12
  - toga25-supply/src/App.tsx
@@ -145,7 +145,11 @@ Type-check **both** `-p tsconfig.app.json` and `-p tsconfig.node.json` — `vite
145
145
 
146
146
  - **⚠ A non-zero `staleTime` on a persisted query serves yesterday's data and never fetches.** Data hooks in this app use `staleTime: 0`. Hooks that currently break this rule **and are persisted**: `useClientFields`, `useCurrentUser`, `useStatusTypeValues`, `useStatusColors`, `useSalesOrderUuidForServiceRequest` (all `Infinity`), and the three VendorItem form hooks (`5 * 60_000`). Each can serve a previous session's answer.
147
147
  - `useTalosSession` and `useTalosThreads` also set a non-zero `staleTime`, but their keys start with `talos`, so they are **not** persisted — in-memory only, safe.
148
+ - **⚠ An edit to a client `FIELDS` JSON looks like it does nothing — and it is NOT Vite HMR.** `useClientFields` is `staleTime: Infinity` **and** persisted, so the old config is frozen in `supply-chain-query-cache`. Clear that key to see the edit. Proper fix: exclude `clientFields` in `shouldDehydrateQuery` (`src/App.tsx:42`).
148
149
  - **⚠ Never verify a config, surface, or ACL change on a warm browser.** Log out, or delete the `supply-chain-query-cache` key in devtools. Most "it didn't deploy" reports are a rehydrated cache.
149
150
  - **⚠ `gcTime: 0` does not stop persistence.** It only evicts a query once it becomes *inactive*; an active query is still dehydrated. To keep something off disk, exclude its namespace in `shouldDehydrateQuery`.
150
151
  - **⚠ Any new `queryClient.clear()` must `await awaitRestoration()` first**, or the restore race undoes it.
151
152
  - The `maxAge` is one timestamp for the entire blob — you cannot expire a single query through it.
153
+
154
+ ## Change history
155
+ - 2026-09-24 — Noted the frozen-`FIELDS`-JSON symptom (persisted `useClientFields` at `staleTime: Infinity`, mistaken for broken Vite HMR) and the `shouldDehydrateQuery` fix. (apeterson)
@@ -33,7 +33,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
33
33
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
34
34
  - **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
35
35
  - **toga2-commerce** (TOGa Commerce) — 26 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
36
- - **toga25-supply** (TOGa 2.5 Supply) — 21 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
36
+ - **toga25-supply** (TOGa 2.5 Supply) — 22 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
37
37
  - **toga-blox** (TOGa Blox) — 15 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
38
38
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.886",
3
+ "version": "1.0.887",
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",