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.
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +24 -1
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +23 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/features/bundle-item-pricing.md +89 -0
- package/knowledge/2.0/apps/toga25-supply/features/persisted-query-cache.md +5 -1
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
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-
|
|
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-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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