toga-ai 1.0.793 → 1.0.794

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.
@@ -5,7 +5,7 @@
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 |
6
6
  | [Bundle item visibility & selectability (the three flags, and why they are enforced nowhere but the client)](features/bundle-item-visibility-and-selectability.md) | Which components of a kit a shopper can see and choose is decided **entirely in the browser**. |
7
7
  | [Cart Bundle Submission & the bundleUuid Identity Contract](features/cart-bundle-submission-and-identity.md) | How cart **bundles** (kits) are turned into `SalesOrderItems` when a cart is submitted or an existing order is edited, and the **identity-field contract** every |
8
- | [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. |
8
+ | [Cart Notification Emails — auto-add/remove lifecycle + duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. |
9
9
  | [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | The Cart summary section (subtotal / shipping / tax / total) is **data-driven** from `cartData`. |
10
10
  | [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`. |
11
11
  | [Catalog cache freshness — the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP |
@@ -1,16 +1,18 @@
1
1
  ---
2
- title: Cart Notification Emails — duplicate prevention
2
+ title: Cart Notification Emails — auto-add/remove lifecycle + duplicate prevention
3
3
  framework: "2.0"
4
4
  repo: toga2-commerce
5
5
  project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-27
9
+ updated: 2026-09-10
10
10
  owners: ["bala", "tcox"]
11
11
  files:
12
12
  - src/pages/Cart/CartPage.tsx
13
13
  - src/pages/Cart/view/cartForm/CartForm.tsx
14
+ - src/pages/Cart/view/cartForm/EditCart.tsx
15
+ - src/pages/Cart/view/cartForm/EditOrder.tsx
14
16
  - src/stores/useEmailOptionsStore.ts
15
17
  - src/stores/useCartSalesQuoteZu.ts
16
18
  - src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts
@@ -21,12 +23,19 @@ related:
21
23
  ---
22
24
 
23
25
  ## Summary
24
- On the cart "Notifications" section a user can add CC email addresses to an order. Adding the
25
- same address twice (often with different casing) used to slip through to the API and blow up on
26
- submit with a MySQL 1062 duplicate-key error on the `SalesOrderEmailAddresses.salesOrderId_emailAddress`
27
- unique index (whose collation `utf8mb4_0900_ai_ci` is case-insensitive). This feature blocks
28
- duplicates case-insensitively in the frontend and shows a sapphire info message instead of letting
29
- the error reach the user.
26
+ On the cart "Notifications" section a user can add CC email addresses to an order. Two jobs live
27
+ here:
28
+
29
+ 1. **Duplicate prevention.** Adding the same address twice (often with different casing) used to
30
+ slip through to the API and blow up on submit with a MySQL 1062 duplicate-key error on the
31
+ `SalesOrderEmailAddresses.salesOrderId_emailAddress` unique index (collation
32
+ `utf8mb4_0900_ai_ci` is case-insensitive). The frontend blocks duplicates case-insensitively and
33
+ shows a sapphire info message instead of letting the error reach the user.
34
+ 2. **Auto-add/remove lifecycle.** Picking an **order-for user** or a **Delegate Manager**
35
+ auto-adds their notification emails. **Changing the pick must remove the previous person's
36
+ auto-added emails.** Until 2026-09-10 it did not: the old person and their manager stayed in the
37
+ list, still checked, and still in the submit payload — so notifications went to the wrong
38
+ people unless the buyer noticed and unchecked them.
30
39
 
31
40
  ## Key files / entry points
32
41
  - `CartPage.tsx` — `handleAddEmail`: validates the email regex, then on success calls
@@ -35,10 +44,23 @@ the error reach the user.
35
44
  - `CartForm.tsx` — owns the duplicate UX. `handleAddEmailWithDuplicateCheck` wraps the passed-in
36
45
  `handleAddEmail`: it case-insensitively checks the existing `emails` list and, on a match, shows
37
46
  the message instead of adding. The **Add Email** button calls this wrapper.
38
- - `useEmailOptionsStore.ts` — `addEmailOption` de-dupes the checkbox list.
39
- - `useCartSalesQuoteZu.ts` — `addEmail` de-dupes the actual `salesOrderEmailAddresses` payload.
47
+ - `CartPage.tsx` — also owns the **Delegate Manager** lifecycle:
48
+ `advancedSelectConfig.delegateManager.onSelect` / `.onClearInput`, the
49
+ `lastDelegateManagerEmailRef` (useRef), the `removeDelegateManagerEmail` guard, the
50
+ `watch("hasDelegateManager")` and `watch("delegateManager")` effects, and `handleClearUser`.
51
+ - `EditCart.tsx` — cart-checkout mode. The order-for change effect (`onSuccess` of the user fetch)
52
+ owns add **and** remove of the order-for user's emails, plus the local `ensureEmailSelected`
53
+ helper and the `orderForUserChanged` flag.
54
+ - `EditOrder.tsx` — edit-order mode. Still has the **original add-only** order-for pattern.
55
+ - `useEmailOptionsStore.ts` — `addEmailOption` de-dupes the checkbox list; `removeEmailOption`
56
+ removes case-insensitively; `selectEmailOption(email)` marks an option checked (idempotent —
57
+ never toggles).
58
+ - `useCartSalesQuoteZu.ts` — `addEmail` de-dupes and `removeEmail` removes from the actual
59
+ `salesOrderEmailAddresses` payload, both case-insensitively.
40
60
 
41
61
  ## How it works
62
+
63
+ ### Duplicate prevention
42
64
  1. **Before (manual add):** clicking Add Email runs `handleAddEmailWithDuplicateCheck` in
43
65
  `CartForm`. It compares `(e.email ?? "").trim().toLowerCase()` against the normalized input. On a
44
66
  match it sets `duplicateEmailMessage` and returns (does not add).
@@ -53,6 +75,41 @@ the error reach the user.
53
75
  5. A `useEffect` clears the message when the input, selected user (`orderForUser?.uuid`), or email
54
76
  list (`emails.length`) changes.
55
77
 
78
+ ### Order-for user — add AND remove (`EditCart.tsx`, cart-checkout mode only)
79
+ Auto-added set per user: **their email**, `supervisorUser.email`, and
80
+ `c_supportedByUserId.email`.
81
+
82
+ 1. On success of the order-for user fetch, read the **previous** user from
83
+ `useSelectedUserZu.getState().selectedUser` — the store, **not** the render closure (the closure
84
+ is stale inside the query callback).
85
+ 2. If the previous uuid differs from the new one (`orderForUserChanged`), remove that person's three
86
+ auto-added addresses from **both** `useEmailOptionsStore` and
87
+ `useCartSalesQuoteZu.salesOrder.salesOrderEmailAddresses`. **Never remove the logged-in user's
88
+ own address** even if it matches one of the three.
89
+ 3. Re-seed the new user's addresses through the local **`ensureEmailSelected(email)`** helper:
90
+ `addEmailOption` → `selectEmailOption` → `addEmail`. Idempotent, so re-running it is safe.
91
+ 4. The seeding block runs when `salesOrderEmailAddresses` is empty **or** when
92
+ `orderForUserChanged`. Before the fix it ran only on empty, so the second pick's emails were
93
+ added **un-checked** and never reached the payload.
94
+
95
+ ### Delegate Manager email lifecycle (`CartPage.tsx`, cart-checkout mode)
96
+ "Associate" in the Compass UI = Delegate Manager. Its email is tracked by
97
+ **`lastDelegateManagerEmailRef`** (a `useRef`), because the form value cannot be used (see
98
+ Gotchas). The email is parsed out of the option label, which has the shape
99
+ `"First Last (email)"`.
100
+
101
+ | Action | What happens |
102
+ |---|---|
103
+ | Pick a manager | `onSelect` removes the *previous* ref email if different, adds the new one checked, sets the ref |
104
+ | Click the field's **X** | `onClearInput` removes the ref email and clears the ref |
105
+ | Un-check "has delegate manager" | the `watch("hasDelegateManager")` effect removes the ref email and empties the field |
106
+ | Clear the order-for user | `handleClearUser` also un-checks the box, empties the field, and clears the ref |
107
+ | Restore a saved cart | the `watch("delegateManager")` effect seeds the ref; a restored name has no `"(email)"`, so it falls back to `salesOrder.approvalUser.email` |
108
+
109
+ **`removeDelegateManagerEmail` never removes an address that is also** the order-for user's, their
110
+ supervisor's, their support tech's, or the logged-in user's — those are owned by the order-for
111
+ lifecycle above.
112
+
56
113
  ## Data model
57
114
  Frontend only. The payload list maps to the `SalesOrderEmailAddresses` table (api2/backend), which
58
115
  has a case-insensitive unique index on `(salesOrderId, emailAddress)`.
@@ -67,19 +124,57 @@ the message resolves to `undefined` and renders blank.
67
124
  - The real source of the 1062 was `useCartSalesQuoteZu.addEmail` comparing with `===`
68
125
  (case-sensitive). Both stores must compare case-insensitively; fixing only the UI list is not
69
126
  enough because `addEmail` builds the payload.
127
+ - **Removes must be case-insensitive too, not just adds.** Both stores' *adds* were already
128
+ case-insensitive while their *removes* used exact match, so a hand-typed `Bob@X.com` survived a
129
+ remove of `bob@x.com` and shipped in the payload. `removeEmailOption` and `removeEmail` are now
130
+ case-insensitive.
131
+ - **`handleEmailChange` TOGGLES — never use it to seed.** Re-running it on an already-checked
132
+ address **un-checks** it. Use the idempotent `ensureEmailSelected` /
133
+ `selectEmailOption` path for any auto-seeding.
134
+ - **`CartFormSection` overwrites the form value BEFORE your handler runs.** It calls
135
+ `field.onChange(value)` and *then* `config.onSelect(value, valueKey)` (same for
136
+ `field.onChange(null)` before `config.onClearInput`). So
137
+ `formMethods.getValues("delegateManager")` inside those handlers already holds the **new** value
138
+ — you **cannot** read the previous pick from the form. Keep the previous value in a `useRef`.
139
+ - **Read the previous order-for user from the store, not the closure.** Inside the user-fetch
140
+ `onSuccess` the render closure's `selectedUser` is stale; use
141
+ `useSelectedUserZu.getState().selectedUser`.
142
+ - **`delegateManager` defaults to a truthy object** (`{uuid:"",name:""}`) now that it is a typed
143
+ form field, so `if (formValues.delegateManager)` is always true. Check
144
+ `formValues.delegateManager?.uuid` instead (3 call sites: `EditCart.tsx` ×2, `EditOrder.tsx` ×1).
145
+ - **`EditOrder.tsx` (edit-order mode) still has the add-only order-for pattern** — the same
146
+ wrong-recipient bug very likely exists there. Left alone deliberately: edit-order has different
147
+ manager rules (`assignedTo`). Fix it as its own task.
70
148
  - Normalize with `(x ?? "").trim().toLowerCase()`, not `x?.trim().toLowerCase()` — both are
71
149
  nullish-safe (optional chaining short-circuits the whole chain, it does not throw), but `(x ?? "")`
72
150
  guarantees a string and avoids an `undefined === undefined` match edge.
73
151
  - Adding the label to only some `CARTPAGE.ts` files leaves other client/role/language users with a
74
152
  blank message.
75
153
  - Do not use a toaster or `setError` (red) for this — product wants the sapphire info style.
76
- - **Now pinned by e2e.** `cypress/e2e/cartPage/cartV2.cy.ts` covers both the auto-population
77
- (order-for user + supervisor emails, protected rows unremovable) and the case-insensitive
78
- duplicate block surfacing the *"This email has already been added"* banner. These are exactly
79
- the parity behaviors the config-cart refactor spike silently lost, so keep the spec green when
80
- touching this flow (see [cypress-testing](../workflows/cypress-testing.md)).
154
+ - **e2e coverage is branch-dependent — check before you rely on it.** On `_production`,
155
+ `cypress/e2e/cartPage/cartV2.cy.ts` covers both the auto-population (order-for user + supervisor
156
+ emails, protected rows unremovable) and the case-insensitive duplicate block surfacing the
157
+ *"This email has already been added"* banner — the exact parity behaviors the config-cart
158
+ refactor spike silently lost, so keep it green (see
159
+ [cypress-testing](../workflows/cypress-testing.md)). But on `#sprint86` that file **does not
160
+ exist** and every spec in `cypress/e2e/cartPage/cart.cy.ts` is **commented out**, so there is no
161
+ net for this flow there. Verify manually on such a branch.
162
+ - **ESLint cannot run in this repo checkout** (pre-existing): `.eslintrc.cjs` references
163
+ `eslint-plugin-react-compiler`, which is not installed. Type-check with
164
+ `npx tsc --noEmit -p tsconfig.app.json` instead.
81
165
 
82
166
  ## Change history
167
+ - 2026-09-10 — FIXED: changing the order-for user or the Delegate Manager left the previous
168
+ person's auto-added emails checked and in the submit payload, so notifications went to the wrong
169
+ people (reported by a Compass USA user on `compass.togacommerce.com`; fix is client-neutral).
170
+ Order-for path (`EditCart.tsx`) now removes the prior user's three auto-added addresses and
171
+ re-seeds via the idempotent `ensureEmailSelected` (replacing the toggling `handleEmailChange`),
172
+ and seeds on `orderForUserChanged` as well as on empty. Delegate Manager path (`CartPage.tsx`)
173
+ gained `lastDelegateManagerEmailRef` + `removeDelegateManagerEmail` covering pick / re-pick / X /
174
+ un-check / clear-user / restore. Stores: added `selectEmailOption`, made `removeEmailOption` and
175
+ `removeEmail` case-insensitive. `hasDelegateManager` / `delegateManager` are now typed form fields
176
+ with defaults, so delegate checks moved to `?.uuid`. Edit-order mode not fixed (different manager
177
+ rules). (tcox)
83
178
  - 2026-07-27 — No behavior change. This duplicate-prevention UX (email auto-population +
84
179
  case-insensitive duplicate banner) is now covered by cart e2e slice 1 (`cartV2.cy.ts`); linked
85
180
  the [cypress-testing](../workflows/cypress-testing.md) workflow doc. (tcox)
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-27
9
+ updated: 2026-09-10
10
10
  owners: ["apeterson", "tcox"]
11
11
  files:
12
12
  - src/pages/Cart/CartPage.tsx
@@ -85,6 +85,17 @@ based on `inEditMode`. `CartFormRenderer.tsx` exists only on the `implement-conf
85
85
  Two mode-change `reset()` effects (~lines 267, 282) re-seed from that same union shape. This is
86
86
  wrong for per-client field sets: it registers fields a client never renders and bakes one client's
87
87
  default into all of them, and risks RHF controlled/uncontrolled warnings.
88
+ As of 2026-09-10 `hasDelegateManager` (`false`) and `delegateManager` (`{uuid:"",name:""}`) are
89
+ part of that union and are typed in `CartFormFieldNames` — previously they were undefined and
90
+ untyped. Because the default object is **truthy**, any check on the field must test
91
+ `delegateManager?.uuid`, not the object.
92
+ - **`CartFormSection` runs `field.onChange` BEFORE the config handler.** For an `advancedSelect` it
93
+ calls `field.onChange(value)` then `config?.onSelect(value, valueKey)` (and
94
+ `field.onChange(null)` then `config?.onClearInput(valueKey)`). So a config handler can never read
95
+ the field's **previous** value from the form — it is already overwritten. Handlers that need the
96
+ prior pick must keep it in a `useRef` (see
97
+ [cart-notification-emails](cart-notification-emails.md) for the Delegate Manager case). The
98
+ planned `hydrateCartConfig` (Phase 5) must preserve this ordering contract or document a change.
88
99
  - **Client differences are hardcoded, not config.** Examples observed: COMPASS renders
89
100
  `DuplicateKitGuardrail`, QUAD does not; COMPASS seeds the cart form (cost center / manager / emails)
90
101
  from the order-for user, QUAD does not. These are per-client conditionals in cart code today.
@@ -285,6 +296,11 @@ extension** of toga2.5's philosophy, not a literal copy.
285
296
  edit-order mode?
286
297
 
287
298
  ## Change history
299
+ - 2026-09-10 — Two gotchas added from cart-notification work: `hasDelegateManager` /
300
+ `delegateManager` are now typed form fields in the union `defaultValues` (truthy default → check
301
+ `?.uuid`), and documented that `CartFormSection` calls `field.onChange` **before** the
302
+ `advancedSelect` config's `onSelect`/`onClearInput`, so a handler cannot read the field's previous
303
+ value. No refactor progress. (tcox)
288
304
  - 2026-07-27 — Phase 0 oracle is now partly real: cart e2e slice 1 (`cartV2.cy.ts`, 12 tests)
289
305
  landed and pins the exact parity behaviors the prior spike lost (notification-email
290
306
  auto-population, duplicate-email surfacing) plus render/gating/clear/empty flows. Corrected the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.793",
3
+ "version": "1.0.794",
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",