toga-ai 1.0.1097 → 1.0.1099

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: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-08
9
+ updated: 2026-10-21
10
10
  owners: ["bala", "tcox"]
11
11
  files:
12
12
  - toga2-commerce/src/utils/checkIsItemPurchasable.ts
@@ -34,6 +34,7 @@ related:
34
34
  - ../../../clients/compass-usa/profile.md
35
35
  - ../../../clients/compass-canada/profile.md
36
36
  - ../../../clients/compass-usa/workflows/changing-a-kit-line-item.md
37
+ - ../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
37
38
  ---
38
39
 
39
40
  `Items.isActive = 0` blocks standalone purchase but means something different on kit components, so the gate keys on purchase CONTEXT not the flag; open when touching item purchasability, cart reconciliation, the submit-time availability re-check, or the order-write interceptor.
@@ -56,7 +57,7 @@ An item with **`Items.isActive = 0`** must not be viewable, addable to a cart, o
56
57
 
57
58
  **Therefore the gate is on the purchase CONTEXT, never on the flag alone.** The discriminator is whether the line belongs to a kit: `isset($payload->bundleItem)` server-side, the bundle wrapper client-side. Kit lines are never evaluated.
58
59
 
59
- > **⚠ Branch state (verified 2026-10-08): two separate implementations exist.** Layers 1-4 (`checkIsItemPurchasable`, `useCartReconciliation`) are on **`_beta` only** — not `_production`, not `_sandbox-client`. Layer 6 (TRUE-81382 submit re-check) was built on `_production`'s base and is on `TRUE-81382` / `_sandbox-client`. They **disagree on kit lines** (first gotcha). Reconcile them before both reach `_production`.
60
+ > **⚠ Branch state (verified 2026-10-08): two separate implementations exist.** Layers 1-4 (`checkIsItemPurchasable`, `useCartReconciliation`) are on **`_beta` only** — not `_production`, not `_sandbox-client`. Layer 6 (TRUE-81382 submit re-check) was built on `_production`'s base and is on `TRUE-81382` / `_sandbox-client` / `_qa-task` (re-checked 2026-10-21, still not `_production`). They **disagree on kit lines and add-ons** (first gotcha). Reconcile them before both reach `_production`.
60
61
 
61
62
  ## How it works
62
63
 
@@ -124,7 +125,10 @@ Implementation:
124
125
 
125
126
  ## Gotchas
126
127
 
127
- - **⚠ OPEN CONFLICT — layer 6 judges kit components; this doc says that must never happen.** `findUnavailableCartItems` blocks a kit line when `bundleItems.item.isActive` is off. Most Compass kit heroes are inactive **on purpose** (Summary table: 87 of 91 primaries on dev-sandbox), so on that data layer 6 would block nearly every kit order. Before TRUE-81382 reaches `_production`: check real kit data, and likely judge kit lines only on `bundleItems.isActive` + bundle / child-bundle flags, not the component item's own flag.
128
+ - **⚠ OPEN CONFLICT — layer 6 judges kit components and add-on bundles on flags that are off BY DESIGN.** Two checks in `findUnavailableCartItems` misfire:
129
+ 1. `bundleItems.item.isActive` — most Compass kit heroes are inactive **on purpose** (Summary table: 87 of 91 primaries on dev-sandbox), so nearly every kit order would block.
130
+ 2. `isFlagOff(freshChildEntry.childBundle?.isActive)` — kit add-on bundles (`BundleItems.childBundleId`) are **always** `Bundles.isActive = 0` so they never show as standalone kits on Home. This blocks **every** cart that holds an add-on with "These items are no longer available..." (found 2026-10-21, TRUE-82981).
131
+ Fix before TRUE-81382 (commit `7fcecbf3`, on `_sandbox-client` and `_qa-task`, not `_production`) ships: judge kit lines and add-ons only on the kit line's `BundleItems.isActive` + the parent bundle, never the component item's or the child bundle's own flag. See [granting persona bundle access](../../../clients/compass-usa/workflows/granting-persona-bundle-access.md#kit-add-on-bundles-need-their-own-personas_bundles-grant).
128
132
  - **⚠ `Items.isActive = 0` means two different things and nothing distinguishes them.** Never gate on the flag alone. Only a **standalone** line is judged; the kit-membership check (`bundleItem` present / absent) is what makes the flag safe to read.
129
133
  - **⚠ A failed lookup must never empty a cart.** `fetchItemsActiveStatus` originally returned `[]` when `data.items` was missing or not an array. Reconciliation reads an empty list as *"every item I asked about is gone"* and clears the cart — so a malformed `200` walked straight past the `try`/`catch` that was supposed to guarantee a lookup failure was harmless. It now **throws**, so the caller's `catch` leaves the cart untouched; a genuine zero-row answer still returns a real empty array. Any "reconcile against the server" routine needs this distinction between *empty* and *unknown*.
130
134
  - **⚠ Judge only the uuids you actually queried.** The reconcile `set` filtered the *current* `state.items` against results computed from a snapshot taken **before** the `await`, so a line added while the lookup was in flight was removed without ever being checked. Fixed by scoping the removal to the queried uuid set.
@@ -135,6 +139,7 @@ Implementation:
135
139
  - **The server check is one query per order, not per line** — uuids are deduped into a single `IN (…)` lookup, and the empty list returns early because `IN ()` is a MySQL syntax error.
136
140
 
137
141
  ## Change history
142
+ - 2026-10-21 — Layer 6 also blocks every kit add-on (child bundles are inactive by design); fix not yet made. (bala)
138
143
  - 2026-10-08 — Added layer 6 (TRUE-81382 submit re-check), branch state, and the open kit-component conflict. (tcox)
139
144
 
140
145
  ## Related
@@ -14,6 +14,7 @@
14
14
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page is meta-driven end to end — page meta + table meta + table data, each keyed by slug via toga-blox hooks; pagination-vs-infinite is decided solely by meta |
15
15
  | [Persisted React Query cache (localStorage `supply-chain-query-cache`)](features/persisted-query-cache.md) | `localStorage["supply-chain-query-cache"]` is the whole React Query cache dehydrated to disk (survives reloads AND sessions); the owning doc for staleTime rules |
16
16
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's modal + nested-table patterns over toga-blox `TableRecordModal` / `PrimaryTable*Layout` — six load-bearing UI patterns (row-click record modal, neste |
17
+ | [Sales order Items table (interim bespoke SalesOrderUpdatedItemsTable — kits, add-ons, parts, fees)](features/sales-order-items-table.md) | The Items table inside the Supply sales order / service request modal: an INTERIM hardcoded table (not Surface-driven) that flattens standalone lines, kits, kit |
17
18
  | [Server Table URL State (page, sort, filters) — `useServerTableUrlState`](features/server-table-url-state.md) | `useServerTableUrlState` — the app's single source of truth for a server-paged table's view state (page/sort/filters/active row) in URL params namespaced by tab |
18
19
  | [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | 2.5 nav/routes/landing are 100% backend-driven by the `navigation` surface; a user with zero visible nav elements gets a BLANK page reported as "can't log in" — |
19
20
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 decides on every navigation whether to bounce an anonymous visitor to their client's SSO IdP vs the local `/login` (useAuthenticationFlow); open for SSO |
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: Sales order Items table (interim bespoke SalesOrderUpdatedItemsTable — kits, add-ons, parts, fees)
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-10-21
10
+ owners: ["bala"]
11
+ files:
12
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderUpdatedItemsTable/SalesOrderUpdatedItemsTable.tsx
13
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx
14
+ - toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx
15
+ related:
16
+ - ./surface-frontend.md
17
+ - ./record-modals-and-nested-tables.md
18
+ - ./bundle-item-pricing.md
19
+ - ../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
20
+ ---
21
+ The Items table inside the Supply sales order / service request modal: an INTERIM hardcoded table (not Surface-driven) that flattens standalone lines, kits, kit add-ons, parts and fees into tiered rows; open when an order line is missing or mis-drawn in that table.
22
+
23
+ ## Summary
24
+ `SalesOrderUpdatedItemsTable.tsx` is a self-contained table ported from the approved Supply mockup. `SalesOrderView.tsx` and `ServiceRequestsView.tsx` render it directly.
25
+ - **Not Surface-driven.** No `tenantFields` are passed; columns are hardcoded in `COLS`. A Surface or FIELDS JSON change does not touch it.
26
+ - **Interim on purpose.** The file header says to delete the whole folder when the shared blox table can draw kits. Do not build new features on it; keep changes inside the folder.
27
+ - Data comes from `useSingleOrderData(uuid)` as `{ standaloneItems, bundles }`.
28
+
29
+ ## How it works
30
+ `buildRows()` turns the data into rows. Each row has a `tier` and a `depth` (0 = top level, 1 = part).
31
+
32
+ | Tier | What it is | Drawn as |
33
+ |---|---|---|
34
+ | `normal` / `fee` | standalone order line | depth 0, part number, SKU, prices |
35
+ | `kit` | a bundle on the order | depth 0, `KitIcon`, `Kit ID: <Bundles.number>`, "Kit" badge |
36
+ | `comp` / `fee` | a kit line (`buildBundleComponentRow`) | depth 1 under its kit |
37
+ | `addOn` | a kit add-on (`childBundle` entry with no `bundleItem.item`) | **exactly like the kit**: depth 0, `KitIcon`, `Kit ID`, badges "Add-on" + "Kit"; then its own order lines at depth 1 |
38
+
39
+ - **Ordering.** Top-level units sort by line number. Inside a kit, children sort primary → required → other parts and add-ons → fees, keeping original order within each group. An add-on header and its lines are **one group**, so they always stay together.
40
+ - **Add-on header key = the add-on's first order-line uuid**, not the bundle uuid. The same add-on bundle can appear twice on one order; a bundle-uuid key would collide.
41
+ - Add-on lines get `isPrimary = false` so kit-level badges (e.g. Computer Support) never land on them.
42
+ - The developer decision (TRUE-82981): the add-on must match the main kit layout exactly. Do not invent a different style for it.
43
+
44
+ ## Gotchas
45
+ - **⚠ Before 2026-10-21 add-on parts never showed.** From the first commit (`983686a`, 29 Jul 2026) `buildRows()` drew a `formatChildBundle` entry as one minimal row with "–" for part number / SKU / manufacturer and skipped `childBundle.bundleItems`. Fixed in PR agilantsolutions/toga25-supply#71 (`b2679f2`, `f4c604c`).
46
+ - **An add-on that shows blank (no name, no Kit ID) is usually data, not this table.** If the persona has no `Personas_Bundles` grant on the add-on bundle, the API returns it without a uuid. See [granting persona bundle access](../../../clients/compass-usa/workflows/granting-persona-bundle-access.md#kit-add-on-bundles-need-their-own-personas_bundles-grant).
47
+ - Test orders: copies of Compass SA139536 and SA139507 on client-sandbox carry kits with add-ons. How to copy a prod order: [Compass order lifecycle](../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md#copying-a-prod-order-into-client-sandbox-for-testing).
48
+
49
+ ## Change history
50
+ - 2026-10-21 — Added the `addOn` tier: add-ons render like the main kit with their parts listed (TRUE-82981, PR #71). (bala)
@@ -31,7 +31,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
31
31
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
32
32
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
33
33
  - **toga2-commerce** (TOGa Commerce) — 33 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
34
- - **toga25-supply** (TOGa 2.5 Supply) — 26 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga25-supply** (TOGa 2.5 Supply) — 27 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga25-iq** (TOGa IQ) — 1 doc(s) → [2.0/apps/toga25-iq/INDEX.md](2.0/apps/toga25-iq/INDEX.md)
36
36
  - **toga25-desk** (TOGa Desk 2.5) — 1 doc(s) → [2.0/apps/toga25-desk/INDEX.md](2.0/apps/toga25-desk/INDEX.md)
37
37
  - **toga25-blox** (TOGa Blox 2.5) — 7 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
@@ -40,7 +40,7 @@
40
40
  | [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | Read-only detection query + repair plan for kit line items mis-attributed to the wrong kit in Client_Compass.SalesOrderItems; open when fees/warranties render u |
41
41
  | [Giving a Compass API consumer its own scoped key (one role per key)](workflows/granting-api-access.md) | 2.0 | How to build a Compass API key with one dedicated read-only role (the Refresh-team template); open for any "give team X scoped API access" request. |
42
42
  | [Granting a Compass user the same toga25-supply navigation as another user (role vs. SurfaceOverride)](workflows/granting-navigation-access.md) | 2.0 | How to give a Compass user another user's toga25-supply menu items via role-scoped SurfaceOverrides (not ACL); open for "same nav as user Y" and "user sees a bl |
43
- | [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | The five-table procedure to give a Compass user sight of a kit (persona + bundle + items + assortments + user) plus the SuperUser role; open for any "give user |
43
+ | [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | The five-table procedure to give a Compass user sight of a kit (persona + bundle + items + assortments + user, plus the kit's add-on child bundles) and the Supe |
44
44
  | [Backfilling Compass ODP tracking and Fulfilled status by replaying ASNs (historical orders)](workflows/odp-asn-replay-backfill.md) | 2.0 | Runbook to move old Compass ODP orders from Pending Fulfillment to Fulfilled with tracking, by POSTing one ASN per ODP PO without emailing customers; open befor |
45
45
  | [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | Why the NetSuite fulfillment-SO sync duplicated Office Depot PO lines on a dual-catalog SKU, the forward fix, and the one-time cleanup migration; open when inve |
46
46
  | [Recovering Lost Compass ODP EDI 850 Imports (re-drop from Logs.FileLog, move stuck AS2 files, manual SQL import)](workflows/odp-edi-import-recovery.md) | 1.0 | Runbook to recover Compass ODP EDI 850 imports: re-drop an x12 that cron 3a deleted after a failure, move AS2 files stuck in `processed/` when nothing reaches 3 |
@@ -113,7 +113,7 @@ The insert-time hook designed 2026-08-12 was built and grew into a reusable engi
113
113
  - **Widening the lookup window is not a free fix.** Removing or extending the grace window would stop new duplicates at source, but also changes which rows the `contactId`-unlinking logic considers and would begin reactivating long-dead accounts. Deliberate deferred item, not an oversight.
114
114
  - **`PeopleFile.php` contains a literal SFTP credential in class constants.** Do not paste class constants from this file into tickets, docs, or chat transcripts.
115
115
  - **⚠ `TEST_MODE` and `DRY_RUN` do different jobs and NEITHER alone is safe — set BOTH to test.** `TEST_MODE` reads a local file instead of SFTP and skips the SFTP delete, but does **not** stop a single database write. `DRY_RUN` skips the DB writes but does **not** stop the SFTP delete — a "safe" dry run still removes the PEOPLE file from `sftp.goagilant.com` and destroys the input you were inspecting. `DRY_RUN` also skips the whole contact/user insert path (it sits inside `if (!self::DRY_RUN)`), so a dry run **cannot** test that code and reports a misleading success. `TEST_FILE_PATH`'s committed value (`//var//www//html//...`) is not where the app lives on Elastic Beanstalk (`/var/app/current/...`).
116
- - **⚠ Read newly inserted user ids with `MAX(id)`, on the transaction connection.** A returning employee usually keeps the **same username** as their dead row, so a plain username lookup after the insert can return the **dead** id. And the new rows are invisible to any other connection until the tenant transaction commits, so the read has to happen on the transaction connection. **Open (not fixed, TRUE-82976):** the post-insert refresh SELECT (~line 1150) that fills `$lookups['users']` has **no `ORDER BY`**, and the last row per username wins. New-user roles and personas resolve their `userId` through that map, so they can land on the old inactive row. Fix candidate: `ORDER BY id` so the newest row wins.
116
+ - **⚠ Read newly inserted user ids with `MAX(id)`, on the transaction connection.** A returning employee usually keeps the **same username** as their dead row, so a plain username lookup after the insert can return the **dead** id. And the new rows are invisible to any other connection until the tenant transaction commits, so the read has to happen on the transaction connection. **Fixed 2026-10-21 (TRUE-82976, worker2 `e22b3ec`):** the post-insert refresh SELECT (~line 1150-1152) that fills `$lookups['users']` now has `AND isActive = 1 ORDER BY id`. Before, it read the dead and new rows in no order and the last one per username won, so new-user roles, personas and the supervisor link could land on the old inactive row. Keep both clauses: the loop is last-row-wins, so dropping `ORDER BY id` or the `isActive` filter brings the bug back. Scale: prod `Client_Compass` has 4,954 usernames with both an active and an inactive row.
117
117
  - **⚠ A new user with no `Users_Roles` row 403s on every api2 call** (EZ-1 "You are not authorized to perform this action"). From commit `51cfcb9` (TRUE-76423, 2026-01-29) to 2026-10-21 the new-user role staging was missing, so new users got roles only on the **next** daily run (cron `Client/Compass/PeopleFile/ProcessPeopleFiles`, ~09:15 CT). Users created 2026-10-21 were backfilled by `dbchanges2/Client_Compass/2026-10-21a - NewUsersBaseRoles.sql` (76 users) and `Client_CompassCanada/2026-10-21a - NewUsersBaseRoles.sql` (12 users). Any backfill must be date-scoped: Canada dev account `Jugveer.Randhawa` (id 2) lacks Compass Base **on purpose**. Commerce symptom: [View As](../../../2.0/apps/toga2-commerce/features/view-as-session-tokens.md#gotchas).
118
118
  - **The `_needsUserId` resolver drops a staged row silently** when its username is not in the refreshed map. No error, no count. A missing role or persona after a "successful" run can be this.
119
119
  - **`_Model_Client_Logs_Record::addNote()` is a no-op in this cron.** Staged notes are only flushed by api2, so a worker must write its `Record` rows itself. See [Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
@@ -143,6 +143,7 @@ The insert-time hook designed 2026-08-12 was built and grew into a reusable engi
143
143
  - [Users.email as login identity](../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md)
144
144
 
145
145
  ## Change history
146
+ - 2026-10-21 — TRUE-82976: fixed the post-insert refresh (`AND isActive = 1 ORDER BY id`, worker2 `e22b3ec`) so new-user roles, personas and supervisor link always go to the newest active row. (bala)
146
147
  - 2026-10-21 — TRUE-82976: restored new-user Base + Compass Base role staging (`_needsUserId`), lost in `51cfcb9`; prod backfill 152 US / 24 CA `Users_Roles` rows; logged the open no-`ORDER BY` refresh risk. (bala)
147
148
  - 2026-10-19 — TRUE-82932: files for Oct 17/18/19 missed while the shared SFTP server banned worker2; no email = import never ran; linked the shared-server doc. No code changed. (bala)
148
149
  - 2026-10-06 — Noted cart Add New User as a second source of duplicate `Users` rows (now checks email too, TRUE-82423). (bala)
@@ -23,7 +23,7 @@ project: _Underscore
23
23
  client: compass-usa
24
24
  type: profile
25
25
  status: active
26
- updated: 2026-10-19
26
+ updated: 2026-10-21
27
27
  owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean, rgirish]
28
28
  files: []
29
29
  related:
@@ -218,7 +218,8 @@ separate, related client (see its own profile).
218
218
  persona-settings tables are empty across all 38 prod personas), "bundle 243" is `Bundles.number`
219
219
  not `id`, and **`Users` has no `username` column** — it is `c_hrEmpUsername`, stored lowercase.
220
220
  Super user = `Roles.id 5` added **additively** via `Users_Roles`, never replacing Base /
221
- Compass Base.
221
+ Compass Base. **Kit add-on bundles (`BundleItems.childBundleId`) need their own persona grant**
222
+ and stay `isActive = 0` by design (TRUE-82981, 2026-10-21).
222
223
  - [Prod ↔ dev-sandbox persona comparison](workflows/persona-population-env-comparison.md) — the
223
224
  full-population diff technique, the **join-on-email** rule (`Users.id` means different people per
224
225
  environment), and the drift bundles (188/191/242) that mimic a catalogue grant.
@@ -6,11 +6,14 @@ project: Database Changes
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-21
10
10
  owners: [bala, ajean]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
+ - dbchanges2/Client_Compass/2026-10-21b - KitAddOnPersonaBundleAccess.sql
13
14
  related:
15
+ - ../../../2.0/apps/toga2-commerce/features/inactive-item-purchase-gating.md
16
+ - ../../../2.0/apps/toga25-supply/features/sales-order-items-table.md
14
17
  - ./granting-navigation-access.md
15
18
  - ./changing-a-kit-line-item.md
16
19
  - ../../../2.0/apps/_underscore/features/effective-persona-resolution.md
@@ -22,7 +25,7 @@ related:
22
25
  - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
23
26
  - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
24
27
  ---
25
- The five-table procedure to give a Compass user sight of a kit (persona + bundle + items + assortments + user) plus the SuperUser role; open for any "give user X kit N / make them super user" request.
28
+ The five-table procedure to give a Compass user sight of a kit (persona + bundle + items + assortments + user, plus the kit's add-on child bundles) and the SuperUser role; open for any "give user X kit N / make them super user" request, or an add-on showing blank / `$undefined` on an order.
26
29
 
27
30
  ## Summary
28
31
  "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". It looks like a one-row insert into `Personas_Bundles`. It is not — a bundle grant without the matching **item** grant renders a **half-visible kit**, because item-level ACL hides any kit line whose item the persona cannot see.
@@ -45,6 +48,13 @@ That asymmetry is the thing to remember: the two bridges that most need a guard
45
48
  ### `Personas_Items` is not optional
46
49
  Every `BundleItems.itemId` of the bundle must also be granted to the persona. Of bundle 243's **19** active line items, persona 1 ("All", which the user already held) covered only **2** — so a bundle-only grant would have shown the kit with 17 of its lines missing. **Persona 1 "All" is the grant that carries the catalogue.** It holds **130,926** rows in `Users_Personas` (prod, 2026-09-02), so it is what makes an item visible to nearly every Compass buyer. When you diff item grants, persona 1 is the one to check first.
47
50
 
51
+ ### Kit add-on bundles need their OWN `Personas_Bundles` grant
52
+ A kit references its add-ons through `BundleItems.childBundleId`. Each add-on is a separate bundle, and the bundles ACL (`AclRecordExpressions` id **235**, restrict-by-persona-with-access-to-bundle) judges it on its own. Granting the kit does **not** grant its add-ons. So when you grant a kit, grant every active add-on (`BundleItems.childBundleId` on an `isActive = 1` kit line) to the same personas.
53
+ - Template / backfill: `dbchanges2/Client_Compass/2026-10-21b - KitAddOnPersonaBundleAccess.sql` (TRUE-82981). One guarded insert gives every active add-on each persona its parent kit already has; re-run safe; verify SELECT at the end expects 0.
54
+ - Prod had **22** missing pairs (kit 236 → add-ons 237-241 for personas 4/12/37; kit 222 → 223, 225 for persona 4; kit 197 → 204-207, 218 for persona 37). Run in prod 2026-10-21, verified 0 missing. **client-sandbox and dev-sandbox still had the same 22 missing** on that date.
55
+ - **No `Personas_Items` grant was needed** for the add-on parts; they showed once the add-on bundle was granted.
56
+ - **⚠ Add-on bundles are `Bundles.isActive = 0` BY DESIGN.** Never activate one: an active add-on shows as a standalone kit on the storefront Home page. Judge add-on availability on the kit line (`BundleItems.isActive`), never on the child bundle's own flag. Code that checks `childBundle.isActive` blocks every add-on (see [Inactive-item purchase gating](../../../2.0/apps/toga2-commerce/features/inactive-item-purchase-gating.md)).
57
+
48
58
  ### Derive the assortment set — never hardcode it
49
59
  An assortment qualifies when it holds one of the bundle's items **and** the persona that owns the bundle today already grants it:
50
60
  ```sql
@@ -73,6 +83,7 @@ A user's **effective** persona set is three UNIONed sources, not just `Users_Per
73
83
  The framework implements exactly this in `_Model_Client_User::_personaIds()` — mirror it rather than re-deriving it. On prod the **sector** hop is where almost every Compass user gets their personas. Full rule + measured numbers: [Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md). **Check all three before adding anything** — the user may already reach the bundle through their location, in which case the right answer is "no change needed". After the grant, re-run the same resolution and confirm the bundle number appears; the verification block at the end of the template file does exactly this.
74
84
 
75
85
  ## Gotchas
86
+ - **⚠ Symptom of a missing add-on grant: blank add-on rows on an order.** The ACL hides the add-on bundle, so the API returns the sales order line's `bundle` relation **without a `uuid`**. toga2-commerce Edit Order then draws the add-on rows blank with `P/N:` and `$undefined` (`CartTableBundle.tsx` falls to the plain-item path when `nestedItem.uuid` is missing), and OrderDetails calls `GET /v2/bundles/undefined` (`getChildBundleDataFromApi` → `fetchSingleChildBundleData`). Seen on SA139536 (ET401 kit 236). Fix is the grant, not the frontend.
76
87
  - **A new Compass persona needs NO settings rows.** Verified across **all 38** personas in prod: `PersonaAppSettings`, `PersonaGlobalSettings`, `PersonaPageSettings`, `PersonaRecordSettings`, `PersonaSectionSettings`, `PersonaRecordFieldSettings`, `PersonaTranslations`, `Personas_Currencies` and `Personas_VendorItems` are **all empty — 0 rows**. A Compass persona is purely **catalogue scoping** (bundles / items / assortments). There is no supporting configuration.
77
88
  - **Persona numbers in prod (2026-08-26): 38 personas.** Numbers in use: **1-23, 27-40, and 100** ("QA Full Access", id 37). **The next free number is 41.** `Personas.number` is a *string* column, so quote it.
78
89
  - **"Bundle 243" is `Bundles.number`, not `Bundles.id`.** The business always quotes the **number**. Bundle **243** is `Bundles.id` **171** ("Compass Creative Studio MacBook"). Resolve it in the query (`WHERE b.number = '243'`), never paste an id, and never assume the two match.
@@ -83,7 +94,12 @@ The framework implements exactly this in `_Model_Client_User::_personaIds()` —
83
94
  - **⚠ Do not write this SQL against the persona-refactor target state.** As of 2026-08-26 the refactor has **not** shipped to prod: persona 1 is still named **"All"** (not "Base"), there is **no "Non-Levy" persona**, and persona **number 40 belongs to id 38 "MyDining- KDS"**. See [Persona Refactor](./persona-refactor-migration.md).
84
95
  - **Everything here is additive.** The user keeps persona 1 and their existing roles; nothing is removed. A "remove access" request is a different, destructive job and needs its own review.
85
96
 
97
+ ## Change history
98
+ - 2026-10-21 — Kit add-on child bundles need their own persona grant; add-ons stay inactive by design; 22 prod pairs backfilled (TRUE-82981). (bala)
99
+ - 2026-08-26 — Five-table procedure from the Compass Creative Studio persona grant. (bala, ajean)
100
+
86
101
  ## Related
102
+ - [Inactive-item purchase gating](../../../2.0/apps/toga2-commerce/features/inactive-item-purchase-gating.md), [Supply sales order items table](../../../2.0/apps/toga25-supply/features/sales-order-items-table.md)
87
103
  - [Granting navigation access](./granting-navigation-access.md)
88
104
  - [Changing what is IN a Compass kit](./changing-a-kit-line-item.md)
89
105
  - [Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md)
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: workflow
7
7
  status: active
8
- updated: 2026-10-15
8
+ updated: 2026-10-21
9
9
  owners: ["jcardinal", "bala", "dfranks"]
10
10
  files: []
11
11
  related:
@@ -72,6 +72,16 @@ Sweeps against `Client_Compass`:
72
72
  ### Repairing a header-only scalar field: direct SQL vs `PUT`
73
73
  When only a scalar needs setting on an existing order (e.g. `customerPurchaseOrder`, which the ODP importer never backfills), **direct SQL is the deliberate choice over `PUT /sales-orders/{uuid}`**: a `PUT` fires the record-14 `POST`/`PUT` [payload interceptor](../../../2.0/apps/api2/features/api-payload-interceptors.md) and drags the whole order back through MITS/NetSuite logic for one field. Accept the two tradeoffs: **no API log row** for the change, and **`dtUpdated` still moves** (the column is `ON UPDATE CURRENT_TIMESTAMP`), which can look like a fresh modification to anything doing `dtUpdated`-based change detection downstream.
74
74
 
75
+ ### Copying a prod order into client-sandbox for testing
76
+ To reproduce an order bug (e.g. SA139536) on client-sandbox `Client_Compass`, copy these rows from prod, in this order:
77
+ 1. `Addresses` — the ship-to address.
78
+ 2. `SalesOrders` — the header.
79
+ 3. `SalesOrderItems` — insert **ordered by `id`**, so parent lines (kit headers, `parentSalesOrderItemId`) exist before their children.
80
+ 4. `Approvals` (`recordId = 14`, sales-orders) + their `ApprovalDecisions`.
81
+ - **Keep the prod `id` / `uuid` when they are free** in sandbox, so the same Supply / Commerce URL opens the copy.
82
+ - "Pending Initial Approval" = every `ApprovalDecisions.isApproved` is `NULL`.
83
+ - A standalone `.sql` file outside a dbchanges repo is blocked by a harness hook, so one-off test SQL is delivered as `.sql.txt` or pasted, never committed.
84
+
75
85
  ### Retrofix tooling
76
86
  The current repair script is **`test/@jeff/compass/retrofix2.php`** (1.0 `App_`), which **supersedes** the older `retrofix.php` sibling. retrofix2 **emits review-first `.sql`** rather than writing the DB directly. It runs three phases newest-first per SO — **Phase 0** (CG-unit asset-tag merge; must run first because units are shared up the chain), **Phase 1** (header/link/bridge repairs, levels L1–L4, where L3 fixes the `PurchaseOrderItems_SalesOrderItems` rung the mirror needs), **Phase 2** (fulfillment mirror climb). Each level is wrapped in `runLevel()` so one level's exception (notably L1's unconditional NetSuite call failing) is logged as a skip and swallowed, letting the independent NetSuite-free repairs still emit. Write guards: `emit()` hard-blocks `Units` and `TrackingNumbers` writes; the Phase-0-only `emitUnit()` permits `Units` UPDATE/DELETE but still blocks `TrackingNumbers`. Full structure: [retrofix2 feature doc](../../../1.0/apps/test/features/compass-retrofix2-sql-generator.md). It is a diagnostic/repair tool, not part of the runtime workflow.
77
87
 
@@ -92,6 +102,7 @@ The current repair script is **`test/@jeff/compass/retrofix2.php`** (1.0 `App_`)
92
102
  - **Incomplete orders (no PO/ASN/Agilant leg yet) are normal** — repair only the layers that exist.
93
103
 
94
104
  ## Change history
105
+ - 2026-10-21 — Added how to copy a prod order into client-sandbox for testing (TRUE-82981). (bala)
95
106
  - 2026-10-15 — Order status now the base per-line rule incl. Picked/Packed (TRUE-79669). (jcardinal)
96
107
 
97
108
  ## Related
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1097",
3
+ "version": "1.0.1099",
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",