toga-ai 1.0.666 → 1.0.668
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/_underscore/features/surface-resolver.md +19 -1
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +34 -4
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +17 -1
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga2-commerce/features/bundle-item-visibility-and-selectability.md +162 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Language.php
|
|
@@ -216,6 +216,18 @@ cluster-isolation rule.
|
|
|
216
216
|
| `Core.Surfaces` | 41–43 |
|
|
217
217
|
| `Core.SurfaceElements` | 125–146 |
|
|
218
218
|
|
|
219
|
+
> **The reserved blocks are NOT an inventory of all surface ids.** They record the
|
|
220
|
+
> 2026-08-21 decision-surface reseed only. Surfaces seeded *before* that decision sit
|
|
221
|
+
> outside them and took AUTO_INCREMENT ids — notably the **`navigation`** TAB_STRIP,
|
|
222
|
+
> whose elements are `Core.SurfaceElements` **109–114** (sales-orders, inventory, items,
|
|
223
|
+
> vendor-items, bundles, service-requests). Verified 2026-08-27. Two consequences: (1) do
|
|
224
|
+
> not assume an id outside 125–146 is unseeded or invalid; (2) a *new* Core seed must
|
|
225
|
+
> still claim an unused reserved block, but must check the pre-reseed ids too before
|
|
226
|
+
> assuming a range is free. Nav elements also carry `aclActionId = NULL` with Core base
|
|
227
|
+
> `isVisible = 0`, so the `aclActionId` gate described above is a no-op for navigation —
|
|
228
|
+
> visibility is purely role-scoped `SurfaceOverrides`. See
|
|
229
|
+
> [Compass navigation access](../../../../clients/compass-usa/workflows/granting-navigation-access.md).
|
|
230
|
+
|
|
219
231
|
**Authoring rules that follow from this:**
|
|
220
232
|
- A new Core surface seed **must** pick an unused reserved block and write explicit ids. Never let
|
|
221
233
|
a surface/element/message row take an `AUTO_INCREMENT` id — that re-opens the drift problem.
|
|
@@ -612,6 +624,12 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
612
624
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
613
625
|
|
|
614
626
|
## Change history
|
|
627
|
+
- 2026-08-27 — Corrected the reserved-id framing: the blocks record the 2026-08-21 reseed only and are
|
|
628
|
+
**not** an inventory of all surface ids. Pre-reseed surfaces kept AUTO_INCREMENT ids — the
|
|
629
|
+
`navigation` TAB_STRIP is `SurfaceElements` **109–114** — so an id outside 125–146 is not evidence it
|
|
630
|
+
is unseeded, and a new seed must check pre-reseed rows before calling a range free. Also recorded
|
|
631
|
+
that nav elements carry `aclActionId = NULL` with Core base `isVisible = 0`, making the `aclActionId`
|
|
632
|
+
gate a **no-op for navigation** (visibility is purely role-scoped `SurfaceOverrides`). (apeterson)
|
|
615
633
|
- 2026-08-26 — Recorded that the Core/Client model split is a **cluster** boundary: `SurfaceOverrides`
|
|
616
634
|
is a CLIENT-DB model, so override counts across clients must be a two-step (Core ids → per-client
|
|
617
635
|
count with literal ids), never a join. Matters when deciding whether to fork or share a surface,
|
|
@@ -6,11 +6,12 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: [tcox, bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
related:
|
|
14
|
+
- ../../toga2-commerce/features/bundle-item-visibility-and-selectability.md
|
|
14
15
|
- ../architecture.md
|
|
15
16
|
- v2-api-error-codes.md
|
|
16
17
|
- tableview-apiwhereclause-row-filtering.md
|
|
@@ -109,8 +110,11 @@ switching that tenant to the join endpoint.
|
|
|
109
110
|
|
|
110
111
|
### ⚠ A `where` clause on a single-uuid READ is silently IGNORED
|
|
111
112
|
|
|
112
|
-
**`GET /v2/items/{uuid}` does not filter.**
|
|
113
|
-
|
|
113
|
+
**`GET /v2/items/{uuid}` does not filter.** A route with a uuid (`count($routePair) == 2`) is
|
|
114
|
+
classified **`ACTION__READ`**, not `ACTION__LIST` (`V2.php:3022-3026`). The `ACTION__READ` branch
|
|
115
|
+
runs from `V2.php:4399` to the `ACTION__DELETE` case (~`V2.php:5478`) and contains **no `where`
|
|
116
|
+
handling whatsoever** — `where` is parsed, then applied only in the `ACTION__LIST` branch
|
|
117
|
+
(`V2.php:3526`) and in the uuid-less DELETE/PUT lookup (~`V2.php:5529`). The parameter
|
|
114
118
|
is accepted, produces no error, and has no effect — so a caller that adds
|
|
115
119
|
`where=(Items.isActive:eq:1)` to a single-uuid GET gets the record back **regardless of the
|
|
116
120
|
filter** and believes it has been gated.
|
|
@@ -127,6 +131,17 @@ rest of the code expects — a `null` there is then a truthful *"missing or filt
|
|
|
127
131
|
example: [TOGa Commerce inactive-item purchase
|
|
128
132
|
gating](../../toga2-commerce/features/inactive-item-purchase-gating.md).
|
|
129
133
|
|
|
134
|
+
### ⚠ `where` NEVER prunes a nested child array — not even on LIST
|
|
135
|
+
|
|
136
|
+
`buildWhereExpressionsFromOptions` is invoked with `defaultTable: $modelName::TABLE`, the **root**
|
|
137
|
+
model. A `where` therefore constrains **which root rows return**; the nested one-to-many arrays are
|
|
138
|
+
built afterwards by `getFullModelData()`, which the `where` never touches. **"Filter the nested
|
|
139
|
+
array with `where`" is not a capability the V2 engine has** — at any depth, on any route. To reduce
|
|
140
|
+
a child collection you must either query the child route directly (with the parent FK in the
|
|
141
|
+
`where`) or filter client-side. Worked example:
|
|
142
|
+
[bundle item visibility & selectability](../../toga2-commerce/features/bundle-item-visibility-and-selectability.md),
|
|
143
|
+
where a bundle-item `isActive` filter was inert for this reason *and* the uuid-READ reason above.
|
|
144
|
+
|
|
130
145
|
This is the mirror image of the LIST → READ flip below: there, an unrecognized param moves you onto
|
|
131
146
|
the READ path and you get a loud 404; here, a **recognized** param is simply inert on that path and
|
|
132
147
|
you get a quiet wrong answer.
|
|
@@ -164,7 +179,11 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
|
|
|
164
179
|
`Contacts.uuid` with no read grant on `Contacts`, as long as you never **select** a
|
|
165
180
|
`Contacts.*` field. Useful when you have a child grant but not the parent's fields.
|
|
166
181
|
- **Referencing a table in `where` that you did not `join`** produces invalid SQL, which is
|
|
167
|
-
uncaught → **`EO-1` 500**.
|
|
182
|
+
uncaught → **`EO-1` 500**. The field string is passed through **verbatim**: `V2.php:6959-6962`
|
|
183
|
+
prepends the default table only when the field contains no `.`, and no auto-join is ever generated
|
|
184
|
+
for an arbitrary table prefix (joins come only from explicit `join`/`ojoin` and `SPECIAL_FIELDS`).
|
|
185
|
+
So a **typo'd table prefix is neither rejected nor reported** — it reaches SQL as an unknown
|
|
186
|
+
column.
|
|
168
187
|
- **Nested route `/contacts/{uuid}/contact-attempts`** auto-filters by the child's FK
|
|
169
188
|
(discovered by **model class**, not field name) and requires **record-level READ on the
|
|
170
189
|
parent** but **no field grant** on it.
|
|
@@ -215,6 +234,17 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
|
|
|
215
234
|
invisible; re-request with `fields=` to force `EZ-2` and see the denied list.
|
|
216
235
|
|
|
217
236
|
## Change history
|
|
237
|
+
- 2026-08-27 — Pinned the **mechanism** behind the silently-ignored uuid-READ `where` to exact code:
|
|
238
|
+
a uuid'd route is classified `ACTION__READ` (`V2.php:3022-3026`) and that branch
|
|
239
|
+
(`V2.php:4399` → ~`5478`) has **no `where` handling at all**; `where` is applied only in
|
|
240
|
+
`ACTION__LIST` (`V2.php:3526`) and the uuid-less DELETE/PUT lookup (~`V2.php:5529`). Added two
|
|
241
|
+
further limits found in the same read: **`where` never prunes a nested child array** (it is built
|
|
242
|
+
with `defaultTable = <root>::TABLE`, and nested collections come from `getFullModelData()`, which
|
|
243
|
+
the `where` never touches — so nested filtering is not a V2 capability on any route), and a
|
|
244
|
+
`where` field with an **unknown table prefix passes into SQL verbatim** (`V2.php:6959-6962` only
|
|
245
|
+
prepends the default table when there is no `.`; no auto-join is generated), so a typo'd prefix is
|
|
246
|
+
never reported. Found while investigating unfiltered bundle items on a Compass USA order.
|
|
247
|
+
(apeterson)
|
|
218
248
|
- 2026-08-21 — Documented that **`join`/`ojoin` drive the WHERE/filter, not the response shape**:
|
|
219
249
|
output nesting comes from FK relationships + requested `fields` (`getFullModelData()`), and an
|
|
220
250
|
aliased `ojoin` column never becomes a readable output field. Worked example: Compass's
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql
|
|
@@ -489,6 +489,16 @@ Blocks assigned by `Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql`: *
|
|
|
489
489
|
sales-order status; no `Core.Messages` rows — labels come from the `sales-order-status` vocabulary).
|
|
490
490
|
See [sales-order-status-filter-surface](../../_underscore/features/sales-order-status-filter-surface.md).
|
|
491
491
|
|
|
492
|
+
**⚠ The reserved blocks are not an inventory of every surface id — pre-2026-08-21 surfaces sit
|
|
493
|
+
outside them.** The reserved-id discipline begins at the reseed, so surfaces seeded before it took
|
|
494
|
+
plain `AUTO_INCREMENT` ids: the **`navigation`** TAB_STRIP is `Core.SurfaceElements` **109–114**
|
|
495
|
+
(sales-orders, inventory, items, vendor-items, bundles, service-requests), verified 2026-08-27. So an
|
|
496
|
+
id below a reserved block is **not** evidence that it is unseeded, and "next free" must be checked
|
|
497
|
+
against the pre-reseed rows as well as the blocks above. Those nav elements also carry
|
|
498
|
+
`aclActionId = NULL` with Core base `isVisible = 0`, which makes the ACL-action gate a **no-op for
|
|
499
|
+
navigation** — visibility comes solely from role-scoped `SurfaceOverrides`. See
|
|
500
|
+
[Compass navigation access](../../../../clients/compass-usa/workflows/granting-navigation-access.md).
|
|
501
|
+
|
|
492
502
|
### 🚨 VERIFY the block is free, and never seed literal ids with `INSERT IGNORE`
|
|
493
503
|
|
|
494
504
|
**Incident, 2026-08-24 — a reserved block was quoted as "highest used" and read as "next free", and
|
|
@@ -779,6 +789,12 @@ Using literals is still correct: a collision then fails loudly on the primary ke
|
|
|
779
789
|
mis-wiring (which is what `INSERT IGNORE` did in the 2026-08-24 incident above).
|
|
780
790
|
|
|
781
791
|
## Change history
|
|
792
|
+
- 2026-08-27 — Added the counterpart to the pre-reserved-block `Core.Messages` drift note: the reserved
|
|
793
|
+
blocks are **not** an inventory of every surface id. Pre-2026-08-21 surfaces took plain
|
|
794
|
+
`AUTO_INCREMENT` ids — the `navigation` TAB_STRIP is `Core.SurfaceElements` **109–114** — so an id
|
|
795
|
+
below a block is not evidence it is unseeded, and "next free" must be verified against pre-reseed
|
|
796
|
+
rows too. Also recorded that those nav elements have `aclActionId = NULL` and Core base
|
|
797
|
+
`isVisible = 0`, so the ACL-action gate is a **no-op for navigation**. (apeterson)
|
|
782
798
|
- 2026-08-26 — Seeded the **Service Request** record surfaces as a **fork** of the sales-order ones:
|
|
783
799
|
`Core/2026-08-26a` (Surfaces **45** / elements **161–165**, recordId 35) and `Core/2026-08-26b`
|
|
784
800
|
(Surfaces **46** / elements **166–169**). Recorded three durable rules: forking a surface **drops
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) | `toga2-commerce` (npm package name **`commerce2-react`**, product name **TOGa Commerce**) is the customer-facing **B2B commerce storefront** of the 2.0 platform | src/main.tsx, src/App.tsx, src/routes.tsx, src/contexts/AuthContext.tsx, src/contexts/helpers/getLoginSettings.ts, src/api/axiosInstance.ts, src/stores/, src/themeConfig/ThemeContext.tsx, src/fieldsConfig/index.ts, src/hooks/useAssignClientFields.ts, vite.config.ts, package.json |
|
|
6
|
+
| [Bundle item visibility & selectability (the three flags, and why they are enforced nowhere but the client)](features/bundle-item-visibility-and-selectability.md) | Which components of a kit a shopper can see and choose is decided **entirely in the browser**. | toga2-commerce/src/pages/BundleView/api/BundleApi.tsx, toga2-commerce/src/pages/BundleView/helpers/formatOriginalBundleData.ts, toga2-commerce/src/pages/BundleView/helpers/formatBundleToAddToCart.ts, toga2-commerce/src/pages/BundleView/hooks/useBundleEditFromLocalStorage.ts, toga2-commerce/src/stores/useBundleBuilderZu.ts, _underscore/Model/Client/BundleItem.php, _underscore/Model/Client/SalesOrderItem.php, api2/Component/Api/V2/V2.php |
|
|
6
7
|
| [Cart Bundle Submission & the bundleUuid Identity Contract](features/cart-bundle-submission-and-identity.md) | How cart **bundles** (kits) are turned into `SalesOrderItems` when a cart is submitted or an existing order is edited, and the **identity-field contract** every | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts, src/utils/formatSalesOrderBundlesFromApi.ts, src/stores/useCartStoreZu.ts, src/pages/OrderDetails/helpers/formatSalesOrderDataFromLocalStorage.ts, src/pages/OrderDetails/view/components/OrderItems.tsx |
|
|
7
8
|
| [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
|
|
8
9
|
| [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | The Cart summary section (subtotal / shipping / tax / total) is **data-driven** from `cartData`. | toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts, toga2-commerce/src/pages/Cart/CartPage.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/api/CartApi.ts |
|
package/knowledge/2.0/apps/toga2-commerce/features/bundle-item-visibility-and-selectability.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Bundle item visibility & selectability (the three flags, and why they are enforced nowhere but the client)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-27
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- toga2-commerce/src/pages/BundleView/api/BundleApi.tsx
|
|
13
|
+
- toga2-commerce/src/pages/BundleView/helpers/formatOriginalBundleData.ts
|
|
14
|
+
- toga2-commerce/src/pages/BundleView/helpers/formatBundleToAddToCart.ts
|
|
15
|
+
- toga2-commerce/src/pages/BundleView/hooks/useBundleEditFromLocalStorage.ts
|
|
16
|
+
- toga2-commerce/src/stores/useBundleBuilderZu.ts
|
|
17
|
+
- _underscore/Model/Client/BundleItem.php
|
|
18
|
+
- _underscore/Model/Client/SalesOrderItem.php
|
|
19
|
+
- api2/Component/Api/V2/V2.php
|
|
20
|
+
related:
|
|
21
|
+
- ./cart-bundle-submission-and-identity.md
|
|
22
|
+
- ./inactive-item-purchase-gating.md
|
|
23
|
+
- ../architecture.md
|
|
24
|
+
- ../../api2/features/v2-rest-query-contract.md
|
|
25
|
+
- ../../api2/features/api-payload-interceptors.md
|
|
26
|
+
- ../../../clients/compass-usa/profile.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Summary
|
|
30
|
+
|
|
31
|
+
Which components of a kit a shopper can see and choose is decided **entirely in the browser**.
|
|
32
|
+
Investigated 2026-08-27 while tracing how part number `S24D402GAN` reached Compass USA order
|
|
33
|
+
`SA136441` (bundle 189, "LEVY - HP ZBOOK 8"). Findings are **platform-wide, not tenant-specific** —
|
|
34
|
+
no `_Model_<Client>` override participates in any of these paths.
|
|
35
|
+
|
|
36
|
+
Three facts, in order of importance:
|
|
37
|
+
|
|
38
|
+
1. **The server-side `isActive` filter on the bundle fetch has never executed.** It is inert for
|
|
39
|
+
three independent reasons (below), so bundle items arrive at the client **completely
|
|
40
|
+
unfiltered**.
|
|
41
|
+
2. **`Client.BundleItems`' three flags mean different things depending on the row type**
|
|
42
|
+
(plain item vs. fee vs. child bundle), so no uniform flag filter can express "selectable."
|
|
43
|
+
3. **Nothing on the backend validates that a posted `salesOrderItems[].bundleItem` is currently
|
|
44
|
+
selectable**, so any direct API caller can post any bundleItem uuid.
|
|
45
|
+
|
|
46
|
+
## The flag conventions (non-obvious — the flag names lie)
|
|
47
|
+
|
|
48
|
+
`Client.BundleItems` carries `isActive`, `isRequired`, `isVisible`. Their meaning varies **by row
|
|
49
|
+
type**:
|
|
50
|
+
|
|
51
|
+
| Row type | isVisible | isRequired | isActive | What it actually means |
|
|
52
|
+
|---|---|---|---|---|
|
|
53
|
+
| Plain component | true | either | true | shown, optionally selectable |
|
|
54
|
+
| **FEE** | **false** | **true** | true | *not shown, but mandatory* — must be on the order or it cannot be placed |
|
|
55
|
+
| **Child bundle** (nested kit) | true | false | **false** | intentionally inactive rows; `isActive = false` does **not** mean disabled here |
|
|
56
|
+
|
|
57
|
+
So `isVisible = false` does not mean "not orderable", and `isActive = false` does not mean
|
|
58
|
+
"disabled". **Any selectability filter must branch on row type first.** This is what makes the
|
|
59
|
+
client-side carve-outs below necessary rather than accidental.
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
### The dead `where` clause on the bundle fetch
|
|
64
|
+
|
|
65
|
+
`src/pages/BundleView/api/BundleApi.tsx` — both `fetchSingleBundleData` (~L81-83) and
|
|
66
|
+
`fetchBundleImages` (~L125-127) send, against `GET /bundles/{uuid}`:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
where: { and: [ { "BundlesItems.isActive": { "=": "true" } } ] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Three independent reasons it does nothing:
|
|
73
|
+
|
|
74
|
+
- **(a) It is a uuid'd GET.** The V2 `ACTION__READ` branch contains no `where` handling at all —
|
|
75
|
+
see [V2 REST query contract](../../api2/features/v2-rest-query-contract.md).
|
|
76
|
+
- **(b) Even on the LIST route it would filter the wrong thing.** `where` constrains which **root**
|
|
77
|
+
rows return; it never prunes a nested one-to-many array. Nested children are serialized by
|
|
78
|
+
`getFullModelData()`, which the `where` never touches.
|
|
79
|
+
- **(c) The table name is a typo.** The model declares `TABLE = 'BundleItems'`
|
|
80
|
+
(`_underscore/Model/Client/BundleItem.php:6`); the query says `BundlesItems` (extra `s`).
|
|
81
|
+
|
|
82
|
+
### Client-side filtering, and its two deliberate carve-outs
|
|
83
|
+
|
|
84
|
+
`src/pages/BundleView/helpers/formatOriginalBundleData.ts`:
|
|
85
|
+
|
|
86
|
+
- **The correct filter is `visibleItems`** (L11-13): `isVisible === true && isActive`.
|
|
87
|
+
- **CARVE-OUT A — fees (L81).** Reads the **raw** `bundlesDataOriginal.bundleItems` and filters only
|
|
88
|
+
`assetType.name === "FEE" && isRequired && isActive` — **no `isVisible` check**. Everything
|
|
89
|
+
matching is then force-added to the order as `updatedFees`
|
|
90
|
+
(`formatBundleToAddToCart.ts:32-44`). The user never sees it and cannot deselect it. **Any item
|
|
91
|
+
mis-tagged with assetType `FEE` is therefore ordered silently.**
|
|
92
|
+
- **CARVE-OUT B — child bundles (L29).** Reads the **raw** array and filters **only** on
|
|
93
|
+
`!!bundleItem?.childBundle` — all three flags ignored.
|
|
94
|
+
|
|
95
|
+
These two are the only fresh-page-load paths by which a non-selectable bundle item can reach an
|
|
96
|
+
order. Both are unguarded.
|
|
97
|
+
|
|
98
|
+
### No server-side enforcement anywhere
|
|
99
|
+
|
|
100
|
+
- `_underscore/Model/Client/BundleItem.php` declares `isActive` / `isRequired` / `isVisible` as
|
|
101
|
+
plain `FIELD_BOOLEAN` columns with **no validation logic**.
|
|
102
|
+
- `_underscore/Model/Client/SalesOrderItem.php` has **no pre/post API payload interceptor** and no
|
|
103
|
+
`_Exception_Validation` throw site — it is plain metadata-driven CRUD.
|
|
104
|
+
|
|
105
|
+
## Recommended direction (decided 2026-08-27, not implemented, no ticket yet)
|
|
106
|
+
|
|
107
|
+
**Enforce selectability at the API boundary, not by hardening the client filter.** A `prePost`
|
|
108
|
+
API payload interceptor on the `sales-orders` record should re-derive each posted
|
|
109
|
+
`salesOrderItems[].bundleItem` against its bundle and reject anything not currently selectable,
|
|
110
|
+
throwing `_Exception_Validation` (→ clean 400, no `Logs.Issue`, no Sentry noise — per the
|
|
111
|
+
exception-routing rule in `2.0/standards/backend-php.md`). Rationale: the client-side filter cannot
|
|
112
|
+
be made trustworthy while flag semantics vary by row type, and a client-only fix leaves the direct
|
|
113
|
+
API path wide open.
|
|
114
|
+
|
|
115
|
+
Also decided: **delete** the dead `where` clause in `BundleApi.tsx` rather than "fix" it — the V2
|
|
116
|
+
engine cannot prune nested children via `where` at all, and the clause could not express the
|
|
117
|
+
fee / child-bundle exceptions even if it worked.
|
|
118
|
+
|
|
119
|
+
## Gotchas / known issues
|
|
120
|
+
|
|
121
|
+
- **⚠ Never read a flag on a `BundleItems` row without first knowing its row type.** Fee rows are
|
|
122
|
+
invisible-but-mandatory; child-bundle rows are inactive-by-convention.
|
|
123
|
+
- **⚠ Do not add a `where` to a uuid'd GET expecting nested filtering.** It is doubly inert (READ
|
|
124
|
+
branch ignores it; `where` filters root rows only).
|
|
125
|
+
- **⚠ A `where` field with an unknown table prefix reaches SQL verbatim.** `V2.php:6959-6962` only
|
|
126
|
+
prepends the default table when the field has no `.`, and no auto-join is generated for an
|
|
127
|
+
arbitrary prefix. A typo'd prefix becomes an unknown-column SQL error, not a validation message —
|
|
128
|
+
and on the READ path it is never even evaluated, which is exactly how the `BundlesItems` typo
|
|
129
|
+
survived unnoticed.
|
|
130
|
+
- **Latent, reported unused:** `src/pages/BundleView/hooks/useBundleEditFromLocalStorage.ts`
|
|
131
|
+
(L21-35) restores `bundleProgressContents` and `optionalAddons` verbatim from localStorage with
|
|
132
|
+
**zero flag re-validation**. It is mechanically reachable (`isEdited: true` set by
|
|
133
|
+
`addOptionalChildBundleToParent` / `removeOptionalChildBundleToParent` in
|
|
134
|
+
`useBundleBuilderZu.ts`), but the flow is **reported unused by anyone as of 2026-08-27** and is
|
|
135
|
+
ruled out as the cause of `SA136441`.
|
|
136
|
+
|
|
137
|
+
## Open question (unresolved)
|
|
138
|
+
|
|
139
|
+
Which carve-out actually admitted part number `S24D402GAN` into order `SA136441` is **still
|
|
140
|
+
unknown** — the API response inspected was truncated before that line, so its `BundleItems` row was
|
|
141
|
+
never seen. Resolving it needs, for `S24D402GAN` in bundle 189: `isActive` / `isVisible` /
|
|
142
|
+
`isRequired`, `childBundleId`, and `item.assetType.name`. If it is neither a FEE-assetType row nor a
|
|
143
|
+
childBundle row, no fresh-load path explains it, and the next place to look is the cart sync or a
|
|
144
|
+
direct API write (`Logs.Api` request payload for that order).
|
|
145
|
+
|
|
146
|
+
## Change history
|
|
147
|
+
|
|
148
|
+
- 2026-08-27 — Created from an investigation-only session (no code changed) into how part number
|
|
149
|
+
`S24D402GAN` reached Compass USA order `SA136441`. Recorded that the bundle fetch's
|
|
150
|
+
`BundlesItems.isActive` `where` clause in `BundleApi.tsx` has **never executed** (uuid'd GET
|
|
151
|
+
ignores `where`; `where` cannot prune nested children; and the table name is a typo for
|
|
152
|
+
`BundleItems`), so 100% of visibility/selectability enforcement is client-side; the two deliberate
|
|
153
|
+
raw-array carve-outs in `formatOriginalBundleData.ts` (fees at L81 with no `isVisible` check and
|
|
154
|
+
force-added as `updatedFees`; child bundles at L29 ignoring all three flags); the real
|
|
155
|
+
`Client.BundleItems` flag conventions (fees invisible-but-mandatory, child bundles
|
|
156
|
+
intentionally inactive) and the consequence that flag meaning varies by row type; and that
|
|
157
|
+
neither `_Model_Client_BundleItem` nor `_Model_Client_SalesOrderItem` validates selectability, so
|
|
158
|
+
a direct API caller can post any bundleItem uuid. Decided to enforce via a `sales-orders`
|
|
159
|
+
`prePost` interceptor throwing `_Exception_Validation` and to delete the dead `where` clause.
|
|
160
|
+
(apeterson)
|
|
161
|
+
</content>
|
|
162
|
+
</invoke>
|
package/knowledge/INDEX.md
CHANGED
|
@@ -29,7 +29,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
29
29
|
- **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
30
30
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
31
31
|
- **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
32
|
-
- **toga2-commerce** (TOGa Commerce) —
|
|
32
|
+
- **toga2-commerce** (TOGa Commerce) — 20 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
33
33
|
- **toga25-supply** (TOGa 2.5 Supply) — 13 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
34
34
|
- **toga-blox** (TOGa Blox) — 13 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
35
35
|
- **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
|
package/package.json
CHANGED