toga-ai 1.0.832 → 1.0.833
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.
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +5 -4
- package/knowledge/2.0/apps/toga2-commerce/architecture.md +34 -98
- package/knowledge/2.0/apps/toga2-commerce/features/cart-notification-emails.md +58 -142
- package/knowledge/2.0/apps/toga2-commerce/features/client-fields.md +121 -393
- package/knowledge/2.0/apps/toga2-commerce/features/header-order-for-dropdown.md +106 -0
- package/knowledge/2.0/apps/toga2-commerce/workflows/cypress-testing.md +68 -144
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/profile.md +15 -2
- package/package.json +1 -1
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary |
|
|
4
4
|
|-----|---------|
|
|
5
|
-
| [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) |
|
|
5
|
+
| [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) | Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM pages, state, API, multi-tenant resolution); open when working anywhere in this |
|
|
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 — auto-add/remove lifecycle + duplicate prevention](features/cart-notification-emails.md) |
|
|
8
|
+
| [Cart Notification Emails — auto-add/remove lifecycle + duplicate prevention](features/cart-notification-emails.md) | Cart Notifications section: CC-email duplicate prevention + auto-add/remove of order-for-user & delegate-manager emails; open when touching notification emails |
|
|
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 |
|
|
12
12
|
| [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | **"Move item X to the front of category Y" is a DATA change, not a code change.** The order of item tiles on a storefront category page is driven by exactly one |
|
|
13
|
-
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) |
|
|
13
|
+
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Per-tenant/language/role JSON config that drives almost all commerce UI text, fields, and login fetches (Layer A global + Layer B FIELDS registry); open before |
|
|
14
14
|
| [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh |
|
|
15
15
|
| [Filter / Search-Results Page & the Two Search Entry Points](features/filter-search-results-page.md) | The storefront has **two distinct search entry points that render the same card component through completely different code paths and different FIELDS files**. |
|
|
16
|
+
| [Header "Order for <Name>" dropdown + the pending-pick hand-off to the cart](features/header-order-for-dropdown.md) | Header pill that lets a buyer pick who they are ordering for from any page, and the in-memory store that hands that pick to the cart's order-for cascade; open b |
|
|
16
17
|
| [Inactive-item purchase gating (standalone lines only — kits are exempt by design)](features/inactive-item-purchase-gating.md) | An item with **`Items.isActive = 0`** must not be viewable, addable to a cart, or orderable **as a standalone line** on the storefront — but the **same flag is |
|
|
17
18
|
| [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. |
|
|
18
19
|
| [Order-submit sync sequencing (useSubmitOrder) — why these calls must not run in parallel](features/order-submit-sync-sequencing.md) | Submitting an order from the cart fires **two independent sync routines** — one for the sales-order header (`syncSalesOrderData`) and one for the line items (`s |
|
|
19
20
|
| [Config-Driven Shipping Cost Waiver (Standard Ground free for computer kits)](features/shipping-cost-waiver-gating.md) | On the toga2-commerce **Cart** page, a shipping option's **cost** can be waived by config using the same `PrimaryItemShippingRule` vocabulary that drives expedi |
|
|
20
21
|
| [Storefront persona gating — the Home page's kit / category / item counts, and the persona switcher](features/storefront-persona-gating-and-home-counts.md) | What a storefront user sees on the Home page is decided **in the front end**, from `user._personaIds`, not by the API's ACL. |
|
|
21
22
|
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. |
|
|
22
|
-
| [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) |
|
|
23
|
+
| [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) | Cypress e2e conventions + the Cart harness (cartV2.cy.ts) for toga2-commerce; read before adding any spec here, and note it doubles as the config-cart refactor |
|
|
23
24
|
| [Diagnosing ERR_HTTP2_PROTOCOL_ERROR (one client fails, everyone else is fine)](workflows/http2-protocol-error-diagnosis.md) | When a Chromium browser (Chrome / Edge) shows **`ERR_HTTP2_PROTOCOL_ERROR`** loading a `*.togacommerce.com` tenant for **one client/network but works for the TO |
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: ["apeterson", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- src/main.tsx
|
|
@@ -29,22 +29,15 @@ related:
|
|
|
29
29
|
- 2.0/apps/toga2-commerce/features/filter-search-results-page.md
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
+
Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM pages, state, API, multi-tenant resolution); open when working anywhere in this repo.
|
|
33
|
+
|
|
32
34
|
## Summary
|
|
33
35
|
|
|
34
|
-
`toga2-commerce` (npm package
|
|
35
|
-
customer-facing **B2B commerce storefront** of the 2.0 platform. It is a **React 18 + TypeScript +
|
|
36
|
-
Vite 5 single-page app** styled with **Tailwind**, and it is the React front end in the 2.0
|
|
37
|
-
"decoupled" architecture: it talks to **`api2`** (the TOGa v2 JSON API) for all data, and renders
|
|
38
|
-
nothing server-side.
|
|
36
|
+
`toga2-commerce` (npm package **`commerce2-react`**, product **TOGa Commerce**) is the customer-facing **B2B commerce storefront** of the 2.0 platform: a **React 18 + TypeScript + Vite 5 SPA** styled with **Tailwind**. It is the React front end in the 2.0 "decoupled" architecture — talks to **`api2`** (the TOGa v2 JSON API) for all data, renders nothing server-side. It is a 2.0 *app with no PHP*: registry `dependsOn` is `api2`; it consumes `_underscore`/`api2` over HTTP but contains no framework PHP classes.
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
**COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from
|
|
42
|
-
the **hostname** and drives three independent systems — the API base URL, the theme, and the
|
|
43
|
-
per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and
|
|
44
|
-
[client-fields](features/client-fields.md) for those subsystems in depth.
|
|
38
|
+
**Multi-tenant:** one codebase serves several clients (**COMPASS**, **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from the **hostname** and drives three independent systems — API base URL, theme, per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and [client-fields](features/client-fields.md).
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
> `_underscore`/`api2` backend over HTTP but contains no framework PHP classes itself.
|
|
40
|
+
**Critical rules:** Tenant is derived from hostname only (`hostname.toUpperCase().split(".")[0]`) — wrong host = wrong tenant, and `localhost` falls back (fields→COMPASS, theme→DEFAULT). The single `QueryClient` uses a **24h default `staleTime`** and persists the whole cache to **`localStorage["commerce"]`**, so catalog AND config/copy (FIELDS) can be up to 24h stale for a returning user — verify any data/copy change after `localStorage.removeItem('commerce')` or logout, never on a warm browser. `AuthLayout` never remounts, so `refetchOnMount` fires once per session — route-change refreshes must be wired explicitly. The registry French language key is **`fr-CA`**, not `fr` — branch on `startsWith("fr")`. api2 returns the standard `{success,data,errors}` envelope; do not break the shape.
|
|
48
41
|
|
|
49
42
|
## Tech stack (verified versions, package.json)
|
|
50
43
|
|
|
@@ -67,10 +60,8 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
|
67
60
|
|
|
68
61
|
## Bootstrap & provider stack
|
|
69
62
|
|
|
70
|
-
- `src/main.tsx` — initializes **Sentry** (browser tracing + replay)
|
|
71
|
-
|
|
72
|
-
- `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a
|
|
73
|
-
`createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
|
|
63
|
+
- `src/main.tsx` — initializes **Sentry** (browser tracing + replay), mounts `<App/>` in React `StrictMode`, imports global CSS.
|
|
64
|
+
- `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a `createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
|
|
74
65
|
|
|
75
66
|
```
|
|
76
67
|
PersistQueryClientProvider (client=queryClient, persister → localStorage "commerce")
|
|
@@ -81,13 +72,8 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
|
81
72
|
+ ReactQueryDevtools (initialIsOpen=false)
|
|
82
73
|
```
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
outside `/cart`, and subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
|
|
87
|
-
|
|
88
|
-
> The cache is **persisted**: query results survive refreshes via `PersistQueryClientProvider`.
|
|
89
|
-
> Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate
|
|
90
|
-
> deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
|
|
75
|
+
- `App` runs a mount effect managing **edit-order mode**: reads a `localStorage.synced` flag and calls `exitEditOrderModeGlobalSyncReset()` when an edit-order session was abandoned outside `/cart`; subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
|
|
76
|
+
- The cache is **persisted**: query results survive refreshes. Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
|
|
91
77
|
|
|
92
78
|
## Routing (`src/routes.tsx`, createBrowserRouter)
|
|
93
79
|
|
|
@@ -97,17 +83,11 @@ Public:
|
|
|
97
83
|
- `/reset-password` → `ResetPasswordPage`
|
|
98
84
|
|
|
99
85
|
Authenticated (wrapped by `PrivateRoute` → `AuthLayout`):
|
|
100
|
-
- `/home`, `/bundle-view`, `/filter`, `/cart`, `/get-support`, `/account`, `/item-view`,
|
|
101
|
-
`/order-details`, `/privacy-policy`, `/terms-conditions`; unmatched → `Navigate to="/home"`.
|
|
86
|
+
- `/home`, `/bundle-view`, `/filter`, `/cart`, `/get-support`, `/account`, `/item-view`, `/order-details`, `/privacy-policy`, `/terms-conditions`; unmatched → `Navigate to="/home"`.
|
|
102
87
|
|
|
103
88
|
Mechanics:
|
|
104
|
-
- **`PrivateRoute`** gates on `useAuth().isAuthenticated` + `useAuthenticationFlow().isLoading`;
|
|
105
|
-
|
|
106
|
-
- **`AuthLayout`** wraps every authenticated route and **stays mounted across route changes**
|
|
107
|
-
(header, footer, nav, notifications live here). Its `errorElement` renders `<ErrorMessage errorType={500}/>`.
|
|
108
|
-
⚠️ Because it never remounts, React Query `refetchOnMount` only fires **once per session** for
|
|
109
|
-
queries owned by AuthLayout — route-change refreshes must be wired explicitly (e.g. the
|
|
110
|
-
notifications query invalidates on route change keyed on `user?.uuid`).
|
|
89
|
+
- **`PrivateRoute`** gates on `useAuth().isAuthenticated` + `useAuthenticationFlow().isLoading`; shows an `AuthLoading` spinner while loading, redirects to `/login` if unauthenticated.
|
|
90
|
+
- **`AuthLayout`** wraps every authenticated route and **stays mounted across route changes** (header, footer, nav, notifications live here). Its `errorElement` renders `<ErrorMessage errorType={500}/>`. Because it never remounts, `refetchOnMount` fires **once per session** for queries owned by AuthLayout — route-change refreshes must be wired explicitly (e.g. the notifications query invalidates on route change keyed on `user?.uuid`).
|
|
111
91
|
- Routes are **statically imported** (no `React.lazy`); Vite handles chunking at build time.
|
|
112
92
|
|
|
113
93
|
## Page architecture — MVVM convention
|
|
@@ -128,10 +108,7 @@ src/pages/<Page>/
|
|
|
128
108
|
└── helpers/ # pure compute/format helpers
|
|
129
109
|
```
|
|
130
110
|
|
|
131
|
-
Pages: `Account, BundleView, Cart, Filter, GetSupport, Home, ItemsView, Login, OrderDetails,
|
|
132
|
-
PrivacyPolicy, ResetPassword, TermsConditions`. The **ViewModel hook is the seam**: it calls
|
|
133
|
-
`useAssignClientFields(fieldKey, language, user)` to load the right tenant/role/language content,
|
|
134
|
-
runs the page's queries, and returns ready-to-render data to the View. (See client-fields doc.)
|
|
111
|
+
Pages: `Account, BundleView, Cart, Filter, GetSupport, Home, ItemsView, Login, OrderDetails, PrivacyPolicy, ResetPassword, TermsConditions`. The **ViewModel hook is the seam**: it calls `useAssignClientFields(fieldKey, language, user)` to load the right tenant/role/language content, runs the page's queries, and returns ready-to-render data to the View. (See client-fields doc.)
|
|
135
112
|
|
|
136
113
|
## State management
|
|
137
114
|
|
|
@@ -147,6 +124,7 @@ Most use the `persist` middleware (localStorage). Verified files:
|
|
|
147
124
|
| `useEditOrderZu` | "Edit existing order" mode flag + original snapshot |
|
|
148
125
|
| `useEditOrderUuidStore` | UUID of the order being edited |
|
|
149
126
|
| `useSelectedUserZu` | "Order for" user selected in cart |
|
|
127
|
+
| `useOrderForZu` | Header "order for" **pending pick** handed to the cart cascade — **in memory only, never `persist`ed** |
|
|
150
128
|
| `useReturnOriginalUserFromViewAsStore` | "View as another user" return-to-self tracking |
|
|
151
129
|
| `useEmailOptionsStore` | Notification CC email options (see cart-notification-emails) |
|
|
152
130
|
| `useBundleBuilderZu` | Bundle-builder wizard selections |
|
|
@@ -159,59 +137,35 @@ Most use the `persist` middleware (localStorage). Verified files:
|
|
|
159
137
|
- Cache persisted to localStorage (`"commerce"`); rehydrated on load.
|
|
160
138
|
- Page queries are typically `enabled` on `isAuthenticated && user?.uuid && !settingsModalEnabled`.
|
|
161
139
|
|
|
162
|
-
**Two-layer state model:** React Query owns *server* state (catalog, orders, user); Zustand owns
|
|
163
|
-
*local* state (cart, selected user, edit mode, tenant/language). The cart is synced to the backend
|
|
164
|
-
sales order via `src/api/syncSalesOrder*.ts` helpers.
|
|
140
|
+
**Two-layer state model:** React Query owns *server* state (catalog, orders, user); Zustand owns *local* state (cart, selected user, edit mode, tenant/language). The cart is synced to the backend sales order via `src/api/syncSalesOrder*.ts` helpers.
|
|
165
141
|
|
|
166
142
|
## API layer (`src/api/`)
|
|
167
143
|
|
|
168
|
-
`src/api/axiosInstance.ts` is the shared client.
|
|
169
|
-
- **Base URL by tenant:** `host = "VITE_API_" + window.location.hostname.toUpperCase().split(".")[0]`;
|
|
170
|
-
`baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So `compass.togacommerce` →
|
|
171
|
-
`VITE_API_COMPASS`, falling back to `VITE_API`. `timeout: 180000`.
|
|
144
|
+
`src/api/axiosInstance.ts` is the shared client. Verified behaviors:
|
|
145
|
+
- **Base URL by tenant:** `host = "VITE_API_" + window.location.hostname.toUpperCase().split(".")[0]`; `baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So `compass.togacommerce` → `VITE_API_COMPASS`, falling back to `VITE_API`. `timeout: 180000`.
|
|
172
146
|
- **Transaction id:** a request interceptor stamps every call with `params.transactionId = uuidv4()`.
|
|
173
|
-
- **Auth token:** a second request interceptor attaches `Authorization: Bearer <token>` — using the
|
|
174
|
-
stored `accessToken` if a `user` exists in localStorage, otherwise fetching a **public token** via
|
|
175
|
-
`POST {baseURL}/auth/public`.
|
|
147
|
+
- **Auth token:** a second request interceptor attaches `Authorization: Bearer <token>` — using the stored `accessToken` if a `user` exists in localStorage, otherwise fetching a **public token** via `POST {baseURL}/auth/public`.
|
|
176
148
|
- **1 ms delay interceptor:** a third request interceptor `await delay(1)` before sending.
|
|
177
|
-
- **401 handling:** response interceptor attempts `POST {baseURL}/auth/refresh` (Bearer refresh
|
|
178
|
-
token) once (`_retry`), retries the original request on success, else calls `performLogout()`
|
|
179
|
-
(clears all auth + cart + `commerce` cache keys and redirects to `/`).
|
|
149
|
+
- **401 handling:** response interceptor attempts `POST {baseURL}/auth/refresh` (Bearer refresh token) once (`_retry`), retries the original request on success, else calls `performLogout()` (clears all auth + cart + `commerce` cache keys and redirects to `/`).
|
|
180
150
|
- Failures are reported to **Sentry** with `errorType: "API"` tags.
|
|
181
151
|
|
|
182
|
-
The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors`; see api2 arch);
|
|
183
|
-
generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `saveData`, `updateData`,
|
|
184
|
-
`deleteData`, optional `?depth=-1` for nested responses).
|
|
152
|
+
The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors`; see api2 arch); generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `saveData`, `updateData`, `deleteData`, optional `?depth=-1` for nested responses).
|
|
185
153
|
|
|
186
154
|
## Authentication & roles
|
|
187
155
|
|
|
188
|
-
- `src/contexts/AuthContext.tsx` derives `host` from the hostname (`hostname.toUpperCase().split(".")[0]`),
|
|
189
|
-
|
|
190
|
-
(`determineShouldShowPersonaSwitcher` — QUAD special-cased), then runs
|
|
191
|
-
`getLoginSettings(...)` which writes `useFieldsStore.fieldKey = host` and the language, and
|
|
192
|
-
hydrates `useUserStore`.
|
|
193
|
-
- `src/hooks/useAuthenticationFlow.ts` orchestrates the post-login fetch sequence
|
|
194
|
-
(user data → persona → contact → settings).
|
|
156
|
+
- `src/contexts/AuthContext.tsx` derives `host` from the hostname (`hostname.toUpperCase().split(".")[0]`), loads `getClientLoginFields(host)` (tenant config), decides persona-switcher visibility (`determineShouldShowPersonaSwitcher` — QUAD special-cased), then runs `getLoginSettings(...)` which writes `useFieldsStore.fieldKey = host` and the language, and hydrates `useUserStore`.
|
|
157
|
+
- `src/hooks/useAuthenticationFlow.ts` orchestrates the post-login fetch sequence (user data → persona → contact → settings).
|
|
195
158
|
- **Role flags** on the user object are tenant-specific:
|
|
196
|
-
- COMPASS / COMPASSCANADA → `ADMIN` (`_isAdmin`), `SUPERUSER` (`_isSuperAdmin`),
|
|
197
|
-
`MANAGER` (`_isSupervisor`), else `USER`.
|
|
159
|
+
- COMPASS / COMPASSCANADA → `ADMIN` (`_isAdmin`), `SUPERUSER` (`_isSuperAdmin`), `MANAGER` (`_isSupervisor`), else `USER`.
|
|
198
160
|
- QUAD → `GLOBALADMIN` (`_isGlobalAdmin`), `BUYER` (`_isBuyer`), `ITSHOPPER` (`_isItShopper`), else `USER`.
|
|
199
|
-
- **Personas:** `user._personaIds` is a colon-delimited string; `getLoginSettings` has special
|
|
200
|
-
handling collapsing persona-24 cases for non-QUAD tenants.
|
|
161
|
+
- **Personas:** `user._personaIds` is a colon-delimited string; `getLoginSettings` has special handling collapsing persona-24 cases for non-QUAD tenants.
|
|
201
162
|
|
|
202
163
|
## Build & deploy
|
|
203
164
|
|
|
204
|
-
- **Per-tenant dev/build scripts** (`package.json`): `compass`, `compasscanada`, `quad`,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- `vite.config.ts`: React plugin + Sentry Vite plugin (sourcemap upload); dev `server.allowedHosts`
|
|
209
|
-
= `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`; PostCSS via
|
|
210
|
-
`postcss.config.cjs`; `optimizeDeps.include: ["react-router-dom"]`.
|
|
211
|
-
- Local dev requires `*.togacommerce` hostnames to resolve to localhost (hosts file) so tenant
|
|
212
|
-
resolution works.
|
|
213
|
-
- Deployment runs on **AWS Amplify** — see [amplify-build-and-deploy](workflows/amplify-build-and-deploy.md).
|
|
214
|
-
A `Dockerfile`/`docker-compose.yml` also exist for containerized dev.
|
|
165
|
+
- **Per-tenant dev/build scripts** (`package.json`): `compass`, `compasscanada`, `quad`, `togacommerce` run `vite --mode development --host <tenant>.togacommerce`; production builds are `build` / `buildAlpha` / `buildBeta` / `buildGamma` / `buildQcSecurity` (each installs the matching `@agilant/toga-blox` npm channel, then `tsc` + `vite build --mode <env>`).
|
|
166
|
+
- `vite.config.ts`: React plugin + Sentry Vite plugin (sourcemap upload); dev `server.allowedHosts` = `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`; PostCSS via `postcss.config.cjs`; `optimizeDeps.include: ["react-router-dom"]`.
|
|
167
|
+
- Local dev requires `*.togacommerce` hostnames to resolve to localhost (hosts file) so tenant resolution works.
|
|
168
|
+
- Deployment runs on **AWS Amplify** — see [amplify-build-and-deploy](workflows/amplify-build-and-deploy.md). A `Dockerfile`/`docker-compose.yml` also exist for containerized dev.
|
|
215
169
|
|
|
216
170
|
## Directory map (`src/`)
|
|
217
171
|
|
|
@@ -234,26 +188,8 @@ generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `save
|
|
|
234
188
|
|
|
235
189
|
## Gotchas
|
|
236
190
|
|
|
237
|
-
- **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit
|
|
238
|
-
|
|
239
|
-
- **
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`localStorage["commerce"]` and sees stale copy for up to 24h after a build that contains the new
|
|
243
|
-
JSON. Verify any FIELDS change after `localStorage.removeItem('commerce')` or a logout, never on a
|
|
244
|
-
warm browser. A build-version `buster` in `persistOptions` would fix this at deploy time but
|
|
245
|
-
invalidates every persisted query app-wide — **open team decision, not implemented.**
|
|
246
|
-
- **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the
|
|
247
|
-
first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS,
|
|
248
|
-
theme → DEFAULT). Use the per-tenant dev scripts.
|
|
249
|
-
- **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase
|
|
250
|
-
(`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as
|
|
251
|
-
`en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"`
|
|
252
|
-
comparison silently never matches. See the client-fields doc.
|
|
253
|
-
|
|
254
|
-
## Change history
|
|
255
|
-
- 2026-07-28 — Gotchas: recorded that the 24h persisted React Query cache also masks `FIELDS`
|
|
256
|
-
config/copy deploys (verify after clearing `localStorage["commerce"]`; `persistOptions.buster` is
|
|
257
|
-
an open team decision), and corrected the runtime language key to `fr-CA` requiring
|
|
258
|
-
`startsWith("fr")` (tcox)
|
|
259
|
-
</content>
|
|
191
|
+
- **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit invalidation for data that must refresh on navigation.
|
|
192
|
+
- **24h `staleTime` + persisted cache** → users can see stale catalog/pricing; invalidate on the events that should bust it. **This also masks config/copy deploys:** `FIELDS` is served *through* the `["clientFields", …]` query, so a returning user rehydrates the old labels from `localStorage["commerce"]` and sees stale copy for up to 24h after a build that contains the new JSON. Verify any FIELDS change after `localStorage.removeItem('commerce')` or a logout, never on a warm browser. A build-version `buster` in `persistOptions` would fix this at deploy time but invalidates every persisted query app-wide — **open team decision, not implemented.**
|
|
193
|
+
- **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS, theme → DEFAULT). Use the per-tenant dev scripts.
|
|
194
|
+
- **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase (`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as `en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"` comparison silently never matches. See the client-fields doc.
|
|
195
|
+
- **Tailwind: not every `<n>px` name is a token.** `max-h-300px` does not exist (300px is only on the height/width scales) — write `max-h-[300px]`. `w-345px`, `h-300px`, `border-1` and `shadow-subMenu` do exist. A non-existent class silently renders unstyled.
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: ["bala", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- src/pages/Cart/CartPage.tsx
|
|
@@ -19,84 +19,47 @@ files:
|
|
|
19
19
|
related:
|
|
20
20
|
- 2.0/apps/toga2-commerce/features/cart-page-config-architecture.md
|
|
21
21
|
- 2.0/apps/toga2-commerce/features/client-fields.md
|
|
22
|
+
- 2.0/apps/toga2-commerce/features/header-order-for-dropdown.md
|
|
22
23
|
- 2.0/apps/toga2-commerce/workflows/cypress-testing.md
|
|
23
24
|
---
|
|
24
25
|
|
|
26
|
+
Cart Notifications section: CC-email duplicate prevention + auto-add/remove of order-for-user & delegate-manager emails; open when touching notification emails on the cart.
|
|
27
|
+
|
|
25
28
|
## Summary
|
|
26
|
-
|
|
27
|
-
here:
|
|
28
|
-
|
|
29
|
-
1. **Duplicate prevention.** Adding the same address twice (often with different casing) used to
|
|
30
|
-
|
|
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.
|
|
39
|
-
|
|
40
|
-
## Key files / entry points
|
|
41
|
-
- `CartPage.tsx` — `handleAddEmail`: validates the email regex, then on success calls
|
|
42
|
-
`addEmailOption` (UI options list) and `addEmail` (the cart sales-quote store that builds the
|
|
43
|
-
`salesOrderEmailAddresses` payload). Normalizes the input once with `.trim()`.
|
|
44
|
-
- `CartForm.tsx` — owns the duplicate UX. `handleAddEmailWithDuplicateCheck` wraps the passed-in
|
|
45
|
-
`handleAddEmail`: it case-insensitively checks the existing `emails` list and, on a match, shows
|
|
46
|
-
the message instead of adding. The **Add Email** button calls this wrapper.
|
|
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.
|
|
29
|
+
|
|
30
|
+
On the cart "Notifications" section a user can add CC email addresses to an order. Two jobs live here:
|
|
31
|
+
|
|
32
|
+
1. **Duplicate prevention.** Adding the same address twice (often with different casing) used to slip through to the API and blow up on submit with a MySQL 1062 duplicate-key error on the `SalesOrderEmailAddresses.salesOrderId_emailAddress` unique index (collation `utf8mb4_0900_ai_ci` is case-insensitive). The frontend blocks duplicates case-insensitively and shows a sapphire info message instead of letting the error reach the user.
|
|
33
|
+
2. **Auto-add/remove lifecycle.** Picking an **order-for user** or a **Delegate Manager** auto-adds their notification emails. **Changing the pick must remove the previous person's auto-added emails.** Until 2026-09-10 it did not: the old person and their manager stayed in the list, still checked, and still in the submit payload — so notifications went to the wrong people unless the buyer noticed and unchecked them.
|
|
60
34
|
|
|
61
35
|
## How it works
|
|
62
36
|
|
|
37
|
+
### Key files / entry points
|
|
38
|
+
- `CartPage.tsx` — `handleAddEmail`: validates the email regex, then on success calls `addEmailOption` (UI options list) and `addEmail` (the cart sales-quote store that builds the `salesOrderEmailAddresses` payload). Normalizes the input once with `.trim()`.
|
|
39
|
+
- `CartForm.tsx` — owns the duplicate UX. `handleAddEmailWithDuplicateCheck` wraps the passed-in `handleAddEmail`: it case-insensitively checks the existing `emails` list and, on a match, shows the message instead of adding. The **Add Email** button calls this wrapper.
|
|
40
|
+
- `CartPage.tsx` — also owns the **Delegate Manager** lifecycle: `advancedSelectConfig.delegateManager.onSelect` / `.onClearInput`, the `lastDelegateManagerEmailRef` (useRef), the `removeDelegateManagerEmail` guard, the `watch("hasDelegateManager")` and `watch("delegateManager")` effects, and `handleClearUser`.
|
|
41
|
+
- `EditCart.tsx` — cart-checkout mode. The order-for change effect (`onSuccess` of the user fetch) owns add **and** remove of the order-for user's emails, plus the local `ensureEmailSelected` helper and the `orderForUserChanged` flag.
|
|
42
|
+
- `EditOrder.tsx` — edit-order mode. Still has the **original add-only** order-for pattern.
|
|
43
|
+
- `useEmailOptionsStore.ts` — `addEmailOption` de-dupes the checkbox list; `removeEmailOption` removes case-insensitively; `selectEmailOption(email)` marks an option checked (idempotent — never toggles).
|
|
44
|
+
- `useCartSalesQuoteZu.ts` — `addEmail` de-dupes and `removeEmail` removes from the actual `salesOrderEmailAddresses` payload, both case-insensitively.
|
|
45
|
+
|
|
63
46
|
### Duplicate prevention
|
|
64
|
-
1. **
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
an already-present address is silently dropped rather than duplicated.
|
|
70
|
-
3. **Display:** the message renders via the shared `InfoBanner` component (the same one used for the
|
|
71
|
-
"view as" / "edit order" banners), styled `text-sapphire-700 text-xs`, no icon. It is NOT a
|
|
72
|
-
toaster and NOT a red `setError` validation — it is a sapphire info message.
|
|
73
|
-
4. The message text comes from the `notifications.duplicateEmailAddressError.label` field config,
|
|
74
|
-
present in every `CARTPAGE.ts` (all client/role/language variants, English + French).
|
|
75
|
-
5. A `useEffect` clears the message when the input, selected user (`orderForUser?.uuid`), or email
|
|
76
|
-
list (`emails.length`) changes.
|
|
47
|
+
1. **Manual add:** clicking Add Email runs `handleAddEmailWithDuplicateCheck` in `CartForm`. It compares `(e.email ?? "").trim().toLowerCase()` against the normalized input. On a match it sets `duplicateEmailMessage` and returns (does not add).
|
|
48
|
+
2. **Auto-add on user select:** when the order user is chosen, their email (and manager's) is auto-added via `addEmailOption`. Both stores de-dupe case-insensitively, so a case-variant of an already-present address is silently dropped rather than duplicated.
|
|
49
|
+
3. **Display:** the message renders via the shared `InfoBanner` component (the same one used for the "view as" / "edit order" banners), styled `text-sapphire-700 text-xs`, no icon. It is NOT a toaster and NOT a red `setError` validation — it is a sapphire info message.
|
|
50
|
+
4. The message text comes from the `notifications.duplicateEmailAddressError.label` field config, present in every `CARTPAGE.ts` (all client/role/language variants, English + French).
|
|
51
|
+
5. A `useEffect` clears the message when the input, selected user (`orderForUser?.uuid`), or email list (`emails.length`) changes.
|
|
77
52
|
|
|
78
53
|
### Order-for user — add AND remove (`EditCart.tsx`, cart-checkout mode only)
|
|
79
|
-
Auto-added set per user: **their email**, `supervisorUser.email`, and
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
54
|
+
Auto-added set per user: **their email**, `supervisorUser.email`, and `c_supportedByUserId.email`.
|
|
55
|
+
|
|
56
|
+
1. On success of the order-for user fetch, read the **previous** user from `useSelectedUserZu.getState().selectedUser` — the store, **not** the render closure (the closure is stale inside the query callback).
|
|
57
|
+
2. If the previous uuid differs from the new one (`orderForUserChanged`), remove that person's three auto-added addresses from **both** `useEmailOptionsStore` and `useCartSalesQuoteZu.salesOrder.salesOrderEmailAddresses`. **Never remove the logged-in user's own address** even if it matches one of the three.
|
|
58
|
+
3. Re-seed the new user's addresses through the local **`ensureEmailSelected(email)`** helper: `addEmailOption` → `selectEmailOption` → `addEmail`. Idempotent, so re-running it is safe.
|
|
59
|
+
4. The seeding block runs when `salesOrderEmailAddresses` is empty **or** when `orderForUserChanged`. Before the fix it ran only on empty, so the second pick's emails were added **un-checked** and never reached the payload.
|
|
94
60
|
|
|
95
61
|
### 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)"`.
|
|
62
|
+
"Associate" in the Compass UI = Delegate Manager. Its email is tracked by **`lastDelegateManagerEmailRef`** (a `useRef`), because the form value cannot be used (see Gotchas). The email is parsed out of the option label, which has the shape `"First Last (email)"`.
|
|
100
63
|
|
|
101
64
|
| Action | What happens |
|
|
102
65
|
|---|---|
|
|
@@ -106,80 +69,33 @@ Gotchas). The email is parsed out of the option label, which has the shape
|
|
|
106
69
|
| Clear the order-for user | `handleClearUser` also un-checks the box, empties the field, and clears the ref |
|
|
107
70
|
| 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
71
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
lifecycle above.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Frontend only. The payload list maps to the `SalesOrderEmailAddresses` table (api2/backend), which
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
the
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
- **
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
148
|
-
- Normalize with `(x ?? "").trim().toLowerCase()`, not `x?.trim().toLowerCase()` — both are
|
|
149
|
-
nullish-safe (optional chaining short-circuits the whole chain, it does not throw), but `(x ?? "")`
|
|
150
|
-
guarantees a string and avoids an `undefined === undefined` match edge.
|
|
151
|
-
- Adding the label to only some `CARTPAGE.ts` files leaves other client/role/language users with a
|
|
152
|
-
blank message.
|
|
72
|
+
**A failed order-for user fetch runs the same `handleClearUser()`** (plus an error toast) instead of only reporting to Sentry, so the ref, the emails and the field are cleared on that path too. Same doc for the other new entry point: the header pill sets `orderFor` through `useOrderForZu`, then this identical cascade runs — see [header-order-for-dropdown](header-order-for-dropdown.md). Both are branch `TRUE-81851`, not yet merged.
|
|
73
|
+
|
|
74
|
+
**`removeDelegateManagerEmail` never removes an address that is also** the order-for user's, their supervisor's, their support tech's, or the logged-in user's — those are owned by the order-for lifecycle above.
|
|
75
|
+
|
|
76
|
+
### Data model
|
|
77
|
+
Frontend only. The payload list maps to the `SalesOrderEmailAddresses` table (api2/backend), which has a case-insensitive unique index on `(salesOrderId, emailAddress)`.
|
|
78
|
+
|
|
79
|
+
### Client variations
|
|
80
|
+
The de-dupe mechanism is uniform across clients. Only the message label text differs per client config: English `"This email has already been added"`; French (Compass Canada) `"Cette adresse e-mail a déjà été ajoutée"`. The label must exist in every `CARTPAGE.ts` variant or the message resolves to `undefined` and renders blank.
|
|
81
|
+
|
|
82
|
+
## Gotchas
|
|
83
|
+
|
|
84
|
+
- The real source of the 1062 was `useCartSalesQuoteZu.addEmail` comparing with `===` (case-sensitive). Both stores must compare case-insensitively; fixing only the UI list is not enough because `addEmail` builds the payload.
|
|
85
|
+
- **Removes must be case-insensitive too, not just adds.** Both stores' *adds* were already case-insensitive while their *removes* used exact match, so a hand-typed `Bob@X.com` survived a remove of `bob@x.com` and shipped in the payload. `removeEmailOption` and `removeEmail` are now case-insensitive.
|
|
86
|
+
- **`handleEmailChange` TOGGLES — never use it to seed.** Re-running it on an already-checked address **un-checks** it. Use the idempotent `ensureEmailSelected` / `selectEmailOption` path for any auto-seeding.
|
|
87
|
+
- **`CartFormSection` overwrites the form value BEFORE your handler runs.** It calls `field.onChange(value)` and *then* `config.onSelect(value, valueKey)` (same for `field.onChange(null)` before `config.onClearInput`). So `formMethods.getValues("delegateManager")` inside those handlers already holds the **new** value — you **cannot** read the previous pick from the form. Keep the previous value in a `useRef`.
|
|
88
|
+
- **Read the previous order-for user from the store, not the closure.** Inside the user-fetch `onSuccess` the render closure's `selectedUser` is stale; use `useSelectedUserZu.getState().selectedUser`.
|
|
89
|
+
- **`delegateManager` defaults to a truthy object** (`{uuid:"",name:""}`) now that it is a typed form field, so `if (formValues.delegateManager)` is always true. Check `formValues.delegateManager?.uuid` instead (3 call sites: `EditCart.tsx` ×2, `EditOrder.tsx` ×1).
|
|
90
|
+
- **`EditOrder.tsx` (edit-order mode) still has the add-only order-for pattern** — the same wrong-recipient bug very likely exists there. Left alone deliberately: edit-order has different manager rules (`assignedTo`). Fix it as its own task.
|
|
91
|
+
- Normalize with `(x ?? "").trim().toLowerCase()`, not `x?.trim().toLowerCase()` — both are nullish-safe (optional chaining short-circuits the whole chain, it does not throw), but `(x ?? "")` guarantees a string and avoids an `undefined === undefined` match edge.
|
|
92
|
+
- Adding the label to only some `CARTPAGE.ts` files leaves other client/role/language users with a blank message.
|
|
153
93
|
- Do not use a toaster or `setError` (red) for this — product wants the sapphire info style.
|
|
154
|
-
- **e2e coverage is branch-dependent — check before you rely on it.** On `_production`,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
165
|
-
|
|
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)
|
|
178
|
-
- 2026-07-27 — No behavior change. This duplicate-prevention UX (email auto-population +
|
|
179
|
-
case-insensitive duplicate banner) is now covered by cart e2e slice 1 (`cartV2.cy.ts`); linked
|
|
180
|
-
the [cypress-testing](../workflows/cypress-testing.md) workflow doc. (tcox)
|
|
181
|
-
- 2026-06-18 — Initial: case-insensitive de-dupe in both cart stores, sapphire `InfoBanner` message in
|
|
182
|
-
CartForm, `duplicateEmailAddressError` label added to all CARTPAGE.ts variants (bala)
|
|
183
|
-
|
|
184
|
-
## Related docs
|
|
94
|
+
- **e2e coverage is branch-dependent — check before you rely on it.** On `_production`, `cypress/e2e/cartPage/cartV2.cy.ts` covers both the auto-population (order-for user + supervisor emails, protected rows unremovable) and the case-insensitive duplicate block surfacing the *"This email has already been added"* banner — the exact parity behaviors the config-cart refactor spike silently lost, so keep it green (see [cypress-testing](../workflows/cypress-testing.md)). But on `#sprint86` that file **does not exist** and every spec in `cypress/e2e/cartPage/cart.cy.ts` is **commented out**, so there is no net for this flow there. Verify manually on such a branch.
|
|
95
|
+
- **ESLint cannot run in this repo checkout** (pre-existing): `.eslintrc.cjs` references `eslint-plugin-react-compiler`, which is not installed. Type-check with `npx tsc --noEmit -p tsconfig.app.json` instead.
|
|
96
|
+
|
|
97
|
+
## Related
|
|
98
|
+
|
|
99
|
+
- [cart-page-config-architecture](cart-page-config-architecture.md)
|
|
100
|
+
- [client-fields](client-fields.md)
|
|
185
101
|
- [cypress-testing](../workflows/cypress-testing.md) — e2e coverage that pins this behavior.
|