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-
|
|
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.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
- `
|
|
39
|
-
|
|
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
|
-
- **
|
|
77
|
-
(order-for user + supervisor
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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-
|
|
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