toga-ai 1.0.289 → 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>
@@ -5,9 +5,12 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-07-02
8
+ updated: 2026-07-08
9
9
  owners: [mhammontree]
10
- files: []
10
+ files:
11
+ - _underscore/Query.php
12
+ - _underscore/Database.php
13
+ - _underscore/Environment.php
11
14
  related:
12
15
  - ../apps/_underscore/architecture.md
13
16
  - ../apps/_underscore/features/per-client-database-connections.md
@@ -27,12 +30,22 @@ From a 2.0 app root (e.g. `worker2`): `chdir` into the app, require Composer aut
27
30
  `_underscore.php`, and set `ENVIRONMENT` before bootstrap:
28
31
 
29
32
  ```php
33
+ putenv('ENVIRONMENT=dev-markhammontree-laptop'); // REQUIRED, before requiring _underscore.php
34
+ // e.g. dev-<yourname>-laptop — matches your Config/<ENVIRONMENT>.ini
30
35
  chdir('/path/to/worker2');
31
36
  require 'vendor/autoload.php';
32
37
  require '_underscore.php'; // boots Loader/Config/etc.
33
- // ENVIRONMENT must be set (env var / server) so _Config picks the right INI
34
38
  ```
35
39
 
40
+ - **`ENVIRONMENT` is mandatory.** The bootstrap uses it to select `worker2/Config/<ENVIRONMENT>.ini`.
41
+ If it is unset, `_underscore.php` throws *"The environment variable 'ENVIRONMENT' is required and
42
+ has not been set"* (`_underscore/Environment.php:12`). Set it via `putenv(...)` in the script
43
+ before the `require`, or export it in the shell.
44
+ - **On Windows the shell is PowerShell** — set env vars as `$env:VAR='x'; php script.php`, **not**
45
+ the bash `VAR=x php script.php` prefix. The bash-style prefix silently does nothing in
46
+ PowerShell, so the script falls back to its defaults (a confusing "it ran but used the wrong
47
+ config" failure).
48
+
36
49
  ## You MUST pin the client DB explicitly
37
50
 
38
51
  `_underscore::DB_CLIENT` is a **logical alias**, not a schema name — there is no database literally
@@ -51,8 +64,41 @@ Without this, any `Model/Client/*` query resolves the unmapped alias and fails.
51
64
  schema you have locally/on the target env (internal testing uses `Client_True` = TOGA Technology).
52
65
  Remember the per-client **logs** connection trap too (see the per-client-database-connections doc).
53
66
 
67
+ ## Commit before you read your own writes (lazy-transaction trap)
68
+
69
+ Standalone scripts run under `_underscore`'s **lazy-transaction model with a separate read
70
+ connection**, so a write is invisible to a later read — and to the code under test — until it is
71
+ committed. Concretely:
72
+
73
+ - `_Database::register(..., alias: _underscore::DB_CLIENT)` sets up a pending transaction for that
74
+ DB. The **first** write (via `_Query`) flips autocommit **OFF** and opens the transaction
75
+ (`_underscore/Query.php` ~303–304, keyed by the database value passed to `_Query`).
76
+ - INSERTs therefore sit **uncommitted**. A subsequent `SELECT` on the read connection returns
77
+ nothing, so a read-back-by-marker returns `null` and dependent FK inserts fail (classic symptom:
78
+ `saleItemId = 0` FK violation).
79
+
80
+ **Pattern:** after seeding base rows, call
81
+ `_Database::transactionCommit(_underscore::DB_CLIENT)` (`_underscore/Database.php` ~165–212). This
82
+ commits **and** restores `autocommit(true)`, so all later writes — and reads of them — behave
83
+ normally. Order your script:
84
+
85
+ 1. seed base rows →
86
+ 2. **commit** →
87
+ 3. read ids back →
88
+ 4. insert dependent rows →
89
+ 5. assert →
90
+ 6. cleanup — and **commit the cleanup too**, so the `DELETE`s persist rather than being rolled
91
+ back when the script exits.
92
+
93
+ This is the same lazy-transaction write-drop family noted in the `_underscore` architecture doc;
94
+ it bites the standalone-script/verification case specifically. Demonstrated working in
95
+ `test/@Mark/Rate/verify_wholehome_per_address_guard.php`.
96
+
54
97
  ## Rules
55
98
 
56
99
  - Do not add or assume a PHPUnit suite for 2.0 backend work; write a `test`-repo script instead.
100
+ - Always set `ENVIRONMENT` before bootstrap (PowerShell: `$env:ENVIRONMENT='...'`, or `putenv`).
57
101
  - Always pin the concrete `Client_<Id>` schema to `DB_CLIENT` (and logs/archive if the code logs).
102
+ - **Commit seeded rows before you read them back** — the read connection cannot see uncommitted
103
+ writes; commit cleanup too so it persists.
58
104
  - Keep scripts in your per-dev `test` folder; never point them at production databases.
@@ -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>
@@ -8,5 +8,5 @@
8
8
  | [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
9
9
  | [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
10
10
  | [Rate Service-Purchase Confirmation Emails (Tech / Warranty)](features/service-purchase-emails.md) | 2.0 | When a Rate customer purchases a service, a confirmation email is sent. | _underscore/Model/Rate/Entitlement.php, worker2/Worker/Notification/EmailTemplate.php, dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql |
11
- | [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | A customer may hold **one active Whole Home Warranty (WH) per validated address, globally** (across all borrowers). | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Address.php, dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql |
11
+ | [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | A customer may hold **one active Whole Home Warranty (WH) per validated address, globally** (across all borrowers). | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Address.php, dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql, test/@Mark/Rate/verify_wholehome_per_address_guard.php |
12
12
  | [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
@@ -6,12 +6,13 @@ project: _Underscore
6
6
  client: rate
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-07
9
+ updated: 2026-07-08
10
10
  owners: [mhammontree]
11
11
  files:
12
12
  - _underscore/Model/Rate/Entitlement.php
13
13
  - _underscore/Model/Client/Address.php
14
14
  - dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql
15
+ - test/@Mark/Rate/verify_wholehome_per_address_guard.php
15
16
  related:
16
17
  - clients/rate/profile.md
17
18
  - clients/rate/features/aig-contract-creation.md
@@ -113,6 +114,17 @@ suggested-vs-entered when `success && address1`, and a "couldn't verify" toaster
113
114
  [address-validation feature](../../../2.0/apps/_underscore/features/address-validation.md) for the
114
115
  endpoint mechanics.
115
116
 
117
+ ## Verification
118
+
119
+ A self-contained backend verification script lives at
120
+ `test/@Mark/Rate/verify_wholehome_per_address_guard.php` (run via PHP CLI with `ENVIRONMENT`
121
+ set, pinned to the real migrated **`Client_Rate`** schema — see the
122
+ [2.0 backend-testing standard](../../../2.0/standards/backend-testing.md)). It applies
123
+ `c_serviceAddressId` idempotently, seeds a token-tagged scratch WH graph (2 addresses / 2 items /
124
+ 2 active subscriptions / 2 entitlements), then exercises the pure helpers, the dedup matrix (via
125
+ reflection on the private methods), and the `prePost` branches, cleaning up in a `finally`. The
126
+ live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/18 pass**.
127
+
116
128
  ## Gotchas / known issues
117
129
 
118
130
  - **TOCTOU on the uniqueness check (accepted).** The check-then-persist dedup is **not**
@@ -131,6 +143,9 @@ endpoint mechanics.
131
143
 
132
144
  ## Change history
133
145
 
146
+ - 2026-07-08 — Added a backend verification script
147
+ (`test/@Mark/Rate/verify_wholehome_per_address_guard.php`, 18/18 pass against the migrated
148
+ `Client_Rate` schema) and a Verification pointer. (mhammontree)
134
149
  - 2026-07-07 — Built the WH per-address purchase guard (TRUE-79533): `prePost` on
135
150
  `_Model_Rate_Entitlement` hard-blocks invalid addresses and second active WH at the same
136
151
  normalized address (global), adopts the carrier-normalized address onto the payload, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.289",
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",