toga-ai 1.0.290 → 1.0.291

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.
@@ -98,6 +98,26 @@ which surfaced latent resolution gaps in `_buildBundle`/`_resolveElement`:
98
98
  - `bundle.surface.titleMessageKey` is set from `titleMessageId`.
99
99
  - The 2nd `_loadMessages` return is merged into `$messageKeyById`; dangling message ids are logged.
100
100
 
101
+ ## Vocabulary / label per-client relabeling (slugs are internal, labels are messages)
102
+
103
+ Clients **never rename a Vocabulary**. `Vocabularies.slug` (`sales-order-status`) and
104
+ `VocabularyTerms.value` (`pendingApproval`) are **stable internal identifiers, never shown to
105
+ users**. The displayed label always resolves from **`Core.Messages`** (every visible string is a
106
+ message key; per-term labels come from each `VocabularyTerm.labelMessageId`). A client customizes
107
+ the *displayed* name two ways, **without touching Core**:
108
+
109
+ 1. **`SurfaceOverrides` row with `attribute = LABEL_MESSAGE`** — scoped by persona/role/language —
110
+ re-points an element's label to a different message.
111
+ 2. **`MessageTranslations` row** (per `languageId`; a null translation falls back to
112
+ `Messages.defaultValue`).
113
+
114
+ Because status-comparison / gating rules compare the **`value`**, not the label, relabeling never
115
+ breaks logic.
116
+
117
+ > ⚠ **Unverified — flag for future confirmation:** whether a `SurfaceOverrides.attribute =
118
+ > LABEL_MESSAGE` value stores a **message id** or a **raw message key** is not yet confirmed. Verify
119
+ > against `Model/Core/Surface.php` `_loadOverrides`/`_buildBundle` before relying on either form.
120
+
101
121
  ## Caching & invalidation
102
122
 
103
123
  - Disk cache keyed `surface:meta:{app}:{slug}:{client}:{sortedPersonaIds}:{sortedRoleIds}:{lang}`.
@@ -214,6 +234,12 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
214
234
  match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
215
235
 
216
236
  ## Change history
237
+ - 2026-07-01 — Clarified vocabulary/label per-client relabeling (new section): Vocabulary `slug`
238
+ and VocabularyTerm `value` are stable internal ids never shown to users; the displayed label
239
+ always resolves from `Core.Messages`; clients relabel via a `SurfaceOverrides` `LABEL_MESSAGE`
240
+ override (persona/role/language-scoped) or a `MessageTranslations` row (null → `defaultValue`),
241
+ and status-comparison rules compare `value` not the label so relabeling never breaks logic.
242
+ Flagged unverified: whether `LABEL_MESSAGE` stores a message id vs a raw key. (apeterson)
217
243
  - 2026-07-01 — Documented the full route-level authorization mechanism (new section): the surfaces
218
244
  script gate is `Client_<tenant>.AclRecordScripts` keyed on JWT `id.client.roles` (client roles,
219
245
  not `core.roles`, not `Core.AclRecordPermissions`); element-drop is a separate
@@ -3,5 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
6
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
@@ -6,9 +6,13 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
10
- owners: [jcardinal]
9
+ updated: 2026-07-01
10
+ owners: [jcardinal, apeterson]
11
11
  files:
12
+ - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
13
+ - dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql
14
+ - _underscore/Model/Client/ThemeToken.php
15
+ - toga25-supply/src/themeConfig.json
12
16
  - dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql
13
17
  - dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql
14
18
  - dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
@@ -57,13 +61,41 @@ per-tenant deltas live in **Client** (mirrors `TableViews`/ACL/`ItemTranslations
57
61
  | `Messages` | Core | i18n key catalog + ICU default value |
58
62
  | `SurfaceOverrides` | Client | THE single sparse cascade table (+`c_longValue mediumtext`) replacing ~40 EAV tables |
59
63
  | `MessageTranslations` | Client | per-language overlay (sidecar pattern) + `c_longValue` |
60
- | `ThemeTokens` | Client | per-tenant semantic token → value (the only place a hex/icon name lives) |
64
+ | `ThemeTokens` | Client | per-tenant semantic token → value (the only place a hex/icon name lives) — see ThemeTokens section below |
61
65
 
62
66
  `SurfaceOverrides` carries the entire cascade in one sparse table: scope columns
63
67
  `personaId`/`roleId`/`languageId` (NULL = not scoped on that axis), an `attribute` ENUM, and
64
68
  `value varchar(255)` + `c_longValue mediumtext`. Precedence **base < client < persona < role**
65
69
  with language as an orthogonal overlay; the resolver applies it most-specific-last in one pass.
66
70
 
71
+ ## ThemeTokens — per-tenant, physically isolated in each client DB
72
+
73
+ `ThemeTokens` lives **inside each tenant's own database** (`Client_Compass.ThemeTokens`,
74
+ `Client_Quad.ThemeTokens`, `Client_CompassCanada.ThemeTokens`) — **not** in Core. Per-tenant
75
+ physical DB isolation is the multi-tenancy mechanism; there is no confirmed Core-held default
76
+ ThemeTokens table. It is a **flat `slug → value` map** (no nesting):
77
+
78
+ | Column | Type | Notes |
79
+ |---|---|---|
80
+ | `slug` | varchar(64), UNIQUE | e.g. `color.status.approved` |
81
+ | `category` | ENUM(`COLOR`,`BACKGROUND`,`SPACING`,`RADIUS`,`TYPOGRAPHY`,`ICON`) | |
82
+ | `name` | varchar | human label |
83
+ | `value` | varchar(64) | resolved hex / css unit / icon name |
84
+
85
+ `_Model_Client_ThemeToken.php` is the ORM model; `_Model_Core_Surface::resolve()` reads the
86
+ tenant's tokens during resolve to map element token-slugs → values (see
87
+ [surface-resolver](../../_underscore/features/surface-resolver.md)).
88
+
89
+ **Seed gap (known, not a capability gap):** the current Compass seed
90
+ (`Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql`) covers **only semantic status
91
+ tokens** (~15 statuses × fg `color.status.*` + bg `bg.status.*` ≈ 30 rows). The ~1004 design-
92
+ foundation CSS vars (`--btn-*`, `--baseInput-*`, `--sideNav-*`, …) are **not** in the DB — they
93
+ still live only in the frontend file `toga25-supply/src/themeConfig.json` (single DEFAULT theme).
94
+ The `category` enum already includes `SPACING`/`RADIUS`/`TYPOGRAPHY`/`ICON`, so the table is
95
+ **designed** to hold the full foundation eventually; it just isn't seeded with it yet. Migrating
96
+ the foundation into `ThemeTokens` rows is the target end-state (see the theming-architecture
97
+ decision, held for senior review).
98
+
67
99
  ## Migration files (typed columns, not EAV)
68
100
 
69
101
  CREATE order is FK-safe (`Messages → Actions → Vocabularies → VocabularyTerms → Surfaces →
@@ -158,6 +190,12 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
158
190
  because `TOOLTIP_MESSAGE` is numeric-only (cosmetic, deferred).
159
191
 
160
192
  ## Change history
193
+ - 2026-07-01 — Documented `ThemeTokens` in detail (new section): it lives **inside each tenant's
194
+ own DB** (physical isolation, no confirmed Core default table), is a flat `slug→value` map with
195
+ the `category` ENUM(`COLOR`/`BACKGROUND`/`SPACING`/`RADIUS`/`TYPOGRAPHY`/`ICON`), and read by
196
+ `resolve()`. Recorded the known seed gap: only status tokens are seeded (~30 rows); the ~1004
197
+ design-foundation vars still live only in `toga25-supply/src/themeConfig.json` — the table is
198
+ designed to hold the foundation but isn't seeded with it yet. (apeterson)
161
199
  - 2026-06-30 — Added the SalesOrders SECTION-surface migration seeds: Core `meta-group` RecordScript +
162
200
  `order-recurring` + 5 display-toggle SECTION surfaces (each with a `sectionVisibility` marker
163
201
  element, since the cascade overrides elements not a surface's own isVisible), the `meta-group`
@@ -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
+ | [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 |
6
7
  | [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 |
7
8
  | [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/pages/Cart/view/cartForm/CartFormSection.tsx, src/pages/Cart/view/cartForm/CartFormRenderer.tsx, src/pages/Cart/view/EditCart.tsx, src/pages/Cart/view/EditOrder.tsx, src/pages/Cart/viewModel/useEditOrderOrEditCartViewModel.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts, src/hooks/useAssignClientFields.ts |
8
9
  | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx |
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: Cart Bundle Submission & the bundleUuid Identity Contract
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-08
10
+ owners: ["apeterson"]
11
+ files:
12
+ - src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts
13
+ - src/utils/formatSalesOrderBundlesFromApi.ts
14
+ - src/stores/useCartStoreZu.ts
15
+ - src/pages/OrderDetails/helpers/formatSalesOrderDataFromLocalStorage.ts
16
+ - src/pages/OrderDetails/view/components/OrderItems.tsx
17
+ related:
18
+ - 2.0/apps/toga2-commerce/architecture.md
19
+ - clients/compass-usa/workflows/cross-kit-bundle-corruption.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ How cart **bundles** (kits) are turned into `SalesOrderItems` when a cart is submitted or an
25
+ existing order is edited, and the **identity-field contract** every consumer of a cart bundle
26
+ must obey: the cart bundle object (`BundleForZuCart`) exposes **`bundleUuid`** as its identity —
27
+ there is **no `uuid` field** on it. Reading `bundle.uuid` returns `undefined` and silently
28
+ corrupts kit/fee attribution.
29
+
30
+ ## How it works
31
+
32
+ The cart (`useCartStoreZu`) holds bundles alongside loose items. On submit/edit,
33
+ `syncSalesOrderItemsFromLocalStorageCartToApi.ts` (`syncSalesOrderLocalStorage`) builds the
34
+ `SalesOrderItems` payload:
35
+
36
+ - Loose items are pushed directly.
37
+ - For each cart bundle, `processBundleContents(bundle.bundleProgressContents, salesOrderItems,
38
+ bundle.bundleUuid)` emits the kit's product lines, stamping each with the bundle's identity.
39
+ - A second loop emits each bundle **fee** line, matching an existing SO item on **both**
40
+ `item.uuid` **and** `bundleItem.bundle.uuid === bundle.bundleUuid` so a fee is not matched
41
+ across bundles, then setting `bundleItem.bundle.uuid = bundle.bundleUuid`.
42
+
43
+ Downstream, `updateParentSalesOrderItemId` links each child line to its kit's **primary** by
44
+ comparing bundle identity, and `useCartSalesQuoteZu.updateSalesOrderItem`'s `findIndex` uses
45
+ `item.uuid` **plus** the bundle identity to place a line in the right kit.
46
+
47
+ On the read side, `formatSalesOrderBundlesFromApi.ts` (via `pairSalesOrderBundlesFromApi`)
48
+ rebuilds the cart bundles from the API by grouping API lines on their `parentSalesOrderItem`
49
+ identity.
50
+
51
+ ## The bundleUuid identity contract
52
+
53
+ - `BundleForZuCart` exposes **`bundleUuid`** (the bundle/kit identity) and
54
+ **`bundleZuCartUuid`** (the per-cart-instance id). It has **no `uuid`**.
55
+ - Any code consuming a cart bundle must read **`bundleUuid`** (or `bundleZuCartUuid` when it
56
+ needs the per-cart instance) — never `bundle.uuid`.
57
+ - The producer field was renamed `uuid` → `bundleUuid` in commit **e5115167** *"Fix bundle
58
+ formatting during edit"* (2026-03-18), in `formatSalesOrderBundlesFromApi.ts` and
59
+ `useCartStoreZu.ts`. Several consumers were **not** updated at the time.
60
+
61
+ ## Gotchas
62
+
63
+ - **Cart bundles flow through variables typed `any`** (`cartBundles.forEach((bundle: any) => …)`),
64
+ so TypeScript does **not** flag a wrong field read. A stale `bundle.uuid` compiles cleanly and
65
+ yields `undefined` at runtime — grep every new consumer by hand.
66
+ - **`bundle.uuid` (undefined) mis-attributes kit lines/fees.** When the submit path read
67
+ `bundle.uuid`, every emitted line got `bundleItem.bundle.uuid = undefined`, so (a)
68
+ `updateParentSalesOrderItemId` compared `undefined == undefined` → true for every line against
69
+ every primary, collapsing lines onto the **last** kit's primary; and (b)
70
+ `updateSalesOrderItem`'s `findIndex` degraded to matching on `item.uuid` alone — so lines that
71
+ **share** an `item.uuid` across kits (fees/warranties, the same catalog item reused in every
72
+ kit) collided and merged into the wrong kit. Products with unique `item.uuid`s mostly survived;
73
+ shared fee items reliably broke. This is why it only reproduced on **multi-kit orders that
74
+ share fee items**.
75
+ - **The code fix is preventive only.** Correcting the submit reads stops *new* corruption on
76
+ save; it does **not** heal already-persisted bad rows. Existing corrupted orders still render
77
+ fees under the wrong kit on edit because `pairSalesOrderBundlesFromApi` rebuilds the cart by
78
+ grouping on the (corrupted) `parentSalesOrderItem` identity from the API. See the Compass
79
+ detect-and-repair workflow (in `related`).
80
+ - **Still-stale consumers (not yet fixed, as of 2026-07-08):**
81
+ - `src/pages/OrderDetails/helpers/formatSalesOrderDataFromLocalStorage.ts:130` — `uuid:
82
+ bundle.uuid` (cosmetic: an `undefined` display field).
83
+ - `src/pages/OrderDetails/view/components/OrderItems.tsx:45` — React `key={bundle.uuid}`
84
+ (cosmetic: duplicate/`undefined` keys).
85
+ - `src/api/helpers/formatBundlesForSalesQuoteZuCartInEditMode.ts:82` still reads the old field
86
+ but is **dead code** (no imports).
87
+
88
+ ## Change history
89
+ - 2026-07-08 — Fixed edit-order bundle submission attributing kit items/fees to the wrong kit:
90
+ `syncSalesOrderItemsFromLocalStorageCartToApi.ts` read `bundle.uuid` (undefined) instead of
91
+ `bundle.bundleUuid` at 3 sites; corrected all three. Root cause traced to the 2026-03-18
92
+ `uuid → bundleUuid` producer rename (commit e5115167) that left stale consumers. Fix is
93
+ preventive (does not heal persisted rows) and lives on branch TRUE-80097, not yet merged.
94
+ Documented the `bundleUuid` identity contract and the two remaining cosmetic stale consumers.
95
+ (apeterson)
96
+ </content>
97
+ </invoke>
@@ -28,7 +28,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
28
28
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
29
29
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
30
30
  - **ai-bdr** (AI-BDR) — 6 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
31
- - **toga2-commerce** (TOGa Commerce) — 7 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
31
+ - **toga2-commerce** (TOGa Commerce) — 9 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
32
32
  - **toga25-supply** (TOGa 2.5 Supply) — 8 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
33
33
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
34
34
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
@@ -8,5 +8,6 @@
8
8
  | [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
9
9
  | [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
10
10
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
11
+ | [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
11
12
  | [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, library/app/client/compass.php |
12
13
  | [Compass Order Lifecycle & Data-Integrity Invariants](workflows/order-lifecycle-and-data-integrity.md) | 2.0 | End-to-end map of how a Compass order flows through the `Client_Compass` (2.0) database and the **expected raw-data shape** at each link/ASN/IF level. | |
@@ -14,13 +14,15 @@ project: _Underscore
14
14
  client: compass-usa
15
15
  type: profile
16
16
  status: active
17
- updated: 2026-07-06
18
- owners: [jcardinal, bala, tcox]
17
+ updated: 2026-07-08
18
+ owners: [jcardinal, bala, tcox, apeterson]
19
19
  files: []
20
20
  related:
21
21
  - features/asn-to-item-fulfillment.md
22
22
  - features/cost-centers.md
23
+ - workflows/cross-kit-bundle-corruption.md
23
24
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
25
+ - ../../2.0/apps/toga2-commerce/features/cart-bundle-submission-and-identity.md
24
26
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
25
27
  ---
26
28
 
@@ -49,6 +51,11 @@ separate, related client (see its own profile).
49
51
  **Known data issue:** many Compass MacBooks are categorized `APPLE LAPTOP` /
50
52
  `MAC & ACCESSORIES`, not `COMPUTERS`, so those kits do **not** qualify for expedited — a
51
53
  catalog-data normalization matter, not a code gap.
54
+ - **Cross-kit bundle corruption (edit-order):** a `toga2-commerce` submit bug attributed kit
55
+ line items and shared fees/warranties to the wrong kit; 55 Compass orders / 298 line items are
56
+ corrupted in `Client_Compass.SalesOrderItems` (Canada and Quad: zero). The code fix is
57
+ preventive; existing rows still need a DB data-fix. Detection query + remediation plan:
58
+ [Cross-Kit Bundle Corruption](workflows/cross-kit-bundle-corruption.md).
52
59
 
53
60
  ## Vendors & integrations
54
61
  - **Office Depot (ODP)** — vendor id 1. ASNs arrive via **cXML** (direct V2 API) and via the
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: Compass Cross-Kit Bundle Corruption — Detection & Repair
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: compass-usa
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-07-08
10
+ owners: ["apeterson"]
11
+ files:
12
+ - src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts
13
+ related:
14
+ - 2.0/apps/toga2-commerce/features/cart-bundle-submission-and-identity.md
15
+ - clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
16
+ - clients/compass-usa/profile.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle
22
+ (kit) line items and **fees/warranties** to the **wrong kit**, persisting corrupted rows in
23
+ `Client_Compass.SalesOrderItems`. Only **Compass** triggers it — it is the only tenant running
24
+ the multi-kit-plus-shared-fee edit flow. This doc is the read-only **detection** reference and
25
+ the **remediation** plan. The root cause is fixed (preventive), but **already-persisted rows are
26
+ not healed** and still need a DB data-fix.
27
+
28
+ Root cause and the field contract are in the toga2-commerce feature doc (see `related`); this
29
+ doc covers the **data signature, impact, and repair** in `Client_Compass`.
30
+
31
+ ## The corruption signature
32
+
33
+ A **flat kit child line** in `SalesOrderItems` where:
34
+ - `bundleItemId` **IS NOT NULL** (it belongs to a kit), and
35
+ - `bundleId` **IS NULL** (this excludes legitimate *nested* child-bundles, which set `bundleId`), and
36
+ - `parentSalesOrderItemId` points to a primary whose kit differs from the line's own kit —
37
+ i.e. the child line's `BundleItems.bundleId` ≠ its parent primary's `BundleItems.bundleId`.
38
+
39
+ The test joins `SalesOrderItems → BundleItems` **twice** (once for the child line via its own
40
+ `bundleItemId`, once for its parent via the parent's `bundleItemId`) and compares
41
+ `BundleItems.bundleId`. A mismatch, within the same order, is the corruption.
42
+
43
+ ## Detection query (read-only)
44
+
45
+ Run against `Client_Compass`, read-only (via the TOGA Database Integration MCP). Confirm exact
46
+ column names against the live schema before use:
47
+
48
+ ```sql
49
+ SELECT child.salesOrderId,
50
+ child.id AS childSalesOrderItemId,
51
+ biChild.bundleId AS childKitBundleId,
52
+ parent.id AS parentSalesOrderItemId,
53
+ biParent.bundleId AS parentKitBundleId
54
+ FROM SalesOrderItems child
55
+ JOIN BundleItems biChild ON biChild.id = child.bundleItemId
56
+ JOIN SalesOrderItems parent ON parent.id = child.parentSalesOrderItemId
57
+ JOIN BundleItems biParent ON biParent.id = parent.bundleItemId
58
+ WHERE child.bundleItemId IS NOT NULL
59
+ AND child.bundleId IS NULL -- exclude legitimate nested child-bundles
60
+ AND biChild.bundleId <> biParent.bundleId; -- child line attributed to the wrong kit
61
+ ```
62
+
63
+ ## Impact (verified on prod, read-only, 2026-07-08)
64
+
65
+ | Schema | Corrupted orders | Corrupted line items |
66
+ |---|---|---|
67
+ | `Client_Compass` | **55** | **298** |
68
+ | `Client_CompassCanada` | 0 | 0 |
69
+ | `Client_Quad` | 0 | 0 |
70
+
71
+ Compass Canada and Quad are **zero impact** — neither runs the multi-kit + shared-fee edit flow.
72
+ **3 of the 55** Compass orders contain **nested bundles** — **SA119455, SA119508, SA133211** —
73
+ and need more careful remediation than the other **52 flat** orders.
74
+
75
+ ## Remediation (NOT yet done — requires write access + review)
76
+
77
+ - **Fix:** repoint each stray child line's `parentSalesOrderItemId` to **its own kit's primary**
78
+ in the same order (i.e. the primary whose `BundleItems.bundleId` equals the child line's
79
+ `BundleItems.bundleId`).
80
+ - Handle the **3 nested-bundle orders separately** — do not batch them with the 52 flat orders.
81
+ - **Why the code fix alone is insufficient:** the preventive frontend fix stops new corruption on
82
+ save but does not touch existing rows. On edit, `pairSalesOrderBundlesFromApi` rebuilds the
83
+ cart by grouping API lines on the (still-corrupted) `parentSalesOrderItem` identity, so old
84
+ orders keep rendering fees under the wrong kit until the DB rows are repaired.
85
+
86
+ ## Open questions — timeline is NOT conclusively the 2026-03-18 regression
87
+
88
+ The corruption was initially attributed to the 2026-03-18 `uuid → bundleUuid` rename (commit
89
+ e5115167). But **10 of the 55** impacted orders have `SalesOrders.dtUpdated` **before**
90
+ 2026-03-18. Two unresolved possibilities:
91
+ 1. `SalesOrders.dtUpdated` (the order **header**) is not the corruption clock — the edit flow
92
+ writes `SalesOrderItems` without bumping the header timestamp; or
93
+ 2. An **earlier** bundle-matching defect produced the same signature — the submit file's history
94
+ shows *"Improve matching logic"* (Jan 2026) and *"Fix item duplications"* (Oct 2025) — i.e. a
95
+ **recurring** bundle-matching bug class, not one 4-month-old bug.
96
+
97
+ **Definitive check not yet run:** inspect `SalesOrderItems.dtUpdated` on the corrupted rows (not
98
+ the header) to date the corruption precisely.
99
+
100
+ ## Systems involved
101
+
102
+ - `toga2-commerce` edit-order submit path (`syncSalesOrderItemsFromLocalStorageCartToApi.ts`) — the cause.
103
+ - `Client_Compass.SalesOrderItems` / `Client_Compass.BundleItems` — where the corruption lives.
104
+ - TOGA Database Integration MCP (`toga_query` etc.) — read-only prod detection.
105
+
106
+ ## Change history
107
+ - 2026-07-08 — Documented the cross-kit bundle-attribution corruption: the signature, a read-only
108
+ detection query, prod impact (Compass 55 orders / 298 line items; Canada 0; Quad 0; 3
109
+ nested-bundle orders SA119455 / SA119508 / SA133211 needing separate handling), the
110
+ preventive-vs-remediation distinction, and the unresolved timeline. Root cause is the
111
+ toga2-commerce `bundleUuid` submit bug (see the feature doc). Remediation not yet performed.
112
+ (apeterson)
113
+ </content>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.290",
3
+ "version": "1.0.291",
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",