toga-ai 1.0.939 → 1.0.941
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/1.0/apps/library/features/netsuite-closed-order-rule.md +10 -3
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +3 -1
- package/knowledge/2.0/apps/_underscore/features/order-for-delegation.md +26 -5
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga2-commerce/features/header-order-for-dropdown.md +55 -39
- package/knowledge/2.0/apps/toga2-commerce/features/view-as-session-tokens.md +14 -9
- package/knowledge/2.0/apps/toga2-commerce/workflows/cypress-testing.md +5 -2
- package/knowledge/clients/compass-usa/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["rgirish"]
|
|
9
|
+
updated: 2026-10-05
|
|
10
|
+
owners: ["rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
13
|
- library/app/api/netsuite/rest.php
|
|
@@ -35,7 +35,7 @@ When the NetSuite → TOGa Supply sync drops (skips + deletes) an order (all cli
|
|
|
35
35
|
- Every other case imports: some lines open (e.g. 9 open + 1 closed), all closed but shipped/invoiced/has a PO, all open, zero item lines.
|
|
36
36
|
- **Why the related-records part:** NetSuite had 132 SOs since 2025 with every line closed but real shipments (Staples 43, GroWrk 21, ODP Veyer 14, Smoke Stacks 11, NYCDOE 5, Endeavor 5, Elite 2 — e.g. Elite SO 269622 → shipment 304666). Deleting them drops real stock movements.
|
|
37
37
|
- Replaces 2026-09-29 "Closed status + related records = keep" and 2026-09-23 "delete every Closed".
|
|
38
|
-
- Status:
|
|
38
|
+
- Status: deployed; per-client SO scope fix deployed 2026-10-05.
|
|
39
39
|
|
|
40
40
|
## How it works
|
|
41
41
|
- **Item line** = `mainline = 'F'`, `item > 0`, not ShipItem/TaxItem/TaxGroup/Discount/EndGroup/BeginGroup/Subtotal/Description/Markup/Payment. One home: `App_Api_Netsuite_Rest::SALES_ORDER_ITEM_LINE_FILTER`.
|
|
@@ -44,6 +44,7 @@ When the NetSuite → TOGa Supply sync drops (skips + deletes) an order (all cli
|
|
|
44
44
|
- `deleteClosedOrderFromNetsuite()` **KEEP guard**: re-checks live and keeps (logs `KEEP`, returns false) if `hasOpenLines` OR `hasRelatedTransactions`. Live re-check is needed because the bulk tool `delete_closed_netsuite_orders.php` (dry run unless `--execute`) passes an order with no lines.
|
|
45
45
|
- The delete removes the SalesOrder/TransferOrder with its lines, invoices, fulfillments (units, tracking bridges) and bridge rows. Records on the other side of a bridge (PO, other SO, TrackingNumber, Unit) are never deleted — only the link row. Fails loud; a half-done delete is finished by the next run.
|
|
46
46
|
- **Line field `isOpen`** on `SalesOrderItems` (after `preferredVendorId`) and `PurchaseOrderItems` (after `vendorItemId`): `TINYINT UNSIGNED NOT NULL DEFAULT 1`; model `[self::FIELD_BOOLEAN, self::FIELDOPT_DEFAULT => true]`. Core.RecordFields 2634 (record 15) / 2635 (record 18), BOOLEAN. Client ACL: API role read+write, Base role read, roles by name. The sync writes 0/1 from the NS line (`property_exists` + `(int)` change check). Old rows stay 1 until their order re-syncs.
|
|
47
|
+
- **Per-client SO scope:** private `closedOrderSalesOrderScope()` returns `[joins, extraEquals]` for `readAllForClosedOrderDelete()` — Compass `SalesOrders.locationId=615286` + `customerId=3`; Prudential `Customers` join + `Customers.uuid = CLIENT_UUID_PRUDENTIAL_CUSTOMER`; other clients unscoped. Same scope as SO sync (~L1012), invoice sync (~L3879), ~L6941.
|
|
47
48
|
- Backfill shipped orders skipped under old rules: re-run their fulfillments with `process_netsuite_records.php` ([re-processing workflow](../../worker/workflows/reprocessing-one-netsuite-order.md)).
|
|
48
49
|
|
|
49
50
|
## Gotchas
|
|
@@ -51,8 +52,14 @@ When the NetSuite → TOGa Supply sync drops (skips + deletes) an order (all cli
|
|
|
51
52
|
- **`Client/2026-09-30a` ALTER is not guarded** — a second run fails on duplicate column. The INSERTs are guarded. Deploy order + per-DB check: [verifying a migration ran](../../../2.0/apps/dbchanges2/workflows/verifying-a-migration-ran.md).
|
|
52
53
|
- **The delete reads every route in `CLOSED_ORDER_DELETE_ROUTES` (toga2.php) — the client's API role (roleId 3) needs read on ALL of them:** record permission + an expression on each logic group + field permission (at least `uuid`). One gap throws in the SO loop and **SALES_ORDERS freezes**. Find gaps with `App_Api_Toga2::checkClosedOrderDeleteAccess()` (run by `delete_closed_netsuite_orders.php`; lists every gap in one run). ACL layers: [acl-permission-chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
|
|
53
54
|
- Compass, 2026-09-29: records 312/313 had logic groups with no expression; 257 and 254 had no `AclRecordPermissions`, then no `AclFieldPermissions` (`EZ-2` on `uuid`). Fix: `dbchanges2/Client_Compass/2026-09-29a - TransferOrderAclExpressions.sql`, then reset `NETSUITE_EXECUTION_MODE_SALES_ORDERS` to `864000-IDLE`.
|
|
55
|
+
- **⚠ Compass dual-SO trap — SO lookup by NetSuite id MUST use the per-client scope.** Compass has 2 `SalesOrders` per `c_netsuiteInternalSalesOrderId`: Office Depot order (`customerId=1`, stamped by cron 5) + NetSuite-synced order (`customerId=3`, `locationId=615286`). 2026-10-02 17:00 → 2026-10-05 SALES_ORDERS froze at `1-RUNNING` (cursor `2026-10-02 17:56:01`, `Logs.Issue` 940, throw at toga2.php:688 `send()`): closing NS SO 7566413 (TOGa 292235) also tried to delete Office Depot SO 481506511001; its fulfillment line `ItemFulfillmentItems` 335334 DELETE hit MySQL 1451 `fk_ItemFulfillmentItems_upstreamItemFulfillmentItemId` (downstream 335333 points up to it) → api2 `EV-11` → whole section aborted each run. Fixed by the scope above. 4th hit of this trap (SO sync, IF sync, invoice sync, closed-order delete) — apply the scope on every new lookup.
|
|
56
|
+
- Damage: 1 `ItemFulfillmentItemUnits` row on Office Depot fulfillment F116294 deleted 2026-10-02 16:59:53; not recoverable (no audit row for api deletes). Only other closed-order delete (2026-09-30, SO 285524 / NS 7354035) was correct.
|
|
57
|
+
- **OPEN (dev decision):** after dropping 292235, mirror fulfillments of the NS-deleted shipment remain on higher tiers — F116294 (id 91650, Office Depot SO) and 91651 (Compass customer order); Compass order may still show shipped. Not touched — never touch the Compass customer order.
|
|
58
|
+
- **OPEN low-risk:** the TransferOrder read in `deleteClosedOrderFromNetsuite()` and `retireStaleSalesOrderForTransferOrder` (~L5698) are still unscoped by NS id; for Compass they throw (fail loud), not delete.
|
|
59
|
+
- **Diagnosis tips:** `Logs_Compass.Api` has no audit for deletes; toga-db MCP rejects any SQL containing the word `DELETE` — filter `method LIKE 'DEL%'`; unbounded `LIKE` on `Api.responsePayload` over days times out — use tight `dtStamp`/`id` ranges.
|
|
54
60
|
- Adding a route to `CLOSED_ORDER_DELETE_ROUTES` = a new ACL need on every client. Run the access check fleet-wide before deploy.
|
|
55
61
|
|
|
56
62
|
## Change history
|
|
57
63
|
- 2026-09-30 — Created: Closed SO with related records kept; only Closed with none deleted. KEEP guard + Compass ACL gotcha. (rgirish)
|
|
58
64
|
- 2026-09-30 — Rule v3: header status dropped; drop only when every item line `isclosed = 'T'` AND no related records. Added line `isOpen` field. (rgirish)
|
|
65
|
+
- 2026-10-05 — Per-client SO scope on the delete lookup (`closedOrderSalesOrderScope()`) after the Compass dual-SO freeze (Issue 940). Open items: higher-tier mirror fulfillments, unscoped TO lookups. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-05
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -1966,6 +1966,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
1966
1966
|
`customerId=3`; Prudential adds the `Customers` join + `CLIENT_UUID_PRUDENTIAL_CUSTOMER`; no other client
|
|
1967
1967
|
changed, no else/default, and the guard still throws for a genuinely-missing SO. PHP 7.2-safe
|
|
1968
1968
|
(`$options['join'] ?? []`), signature unchanged.
|
|
1969
|
+
- Same trap hit the closed-order delete 2026-10-05 (SALES_ORDERS freeze, Issue 940) — 4th path; see [closed-order rule](../../library/features/netsuite-closed-order-rule.md). Every SO-by-NS-id lookup needs this scope.
|
|
1969
1970
|
- **⚠ OPEN follow-up (own ticket): six OTHER clients also keep two SalesOrders per NetSuite id, and the
|
|
1970
1971
|
fix does NOT scope them.** A read-only sweep found the same dual-SO pattern in **Growrk (2789), Canon
|
|
1971
1972
|
(2890), Endeavorhealth (4828), Spglobal (2893), Browardsheriff (160), Masonite (17)** (Nychh/Elite/Quad/Sbasite
|
|
@@ -2078,6 +2079,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
2078
2079
|
[NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
|
|
2079
2080
|
|
|
2080
2081
|
## Change history
|
|
2082
|
+
- 2026-10-05 — Closed-order delete now per-client SO scoped (Compass dual-SO freeze, Issue 940); details in [closed-order rule](../../library/features/netsuite-closed-order-rule.md). Library only. (jcardinal)
|
|
2081
2083
|
- 2026-10-01 — `Items.description` now from the NetSuite item record, refreshed every touch ([doc](../../library/features/netsuite-item-description-sync.md); library + worker deploy together). Gotchas: invoice billing an SO older than the SO cursor (Adyen); receipt lines never rebuilt on an existing receipt (Elite). (rgirish)
|
|
2082
2084
|
- 2026-10-01 — NYCHH diagnosis traps recorded: the wrapper is gated to **once a day** by
|
|
2083
2085
|
`cronInitialization(86400)` despite its `*/5` schedule entry, and the live cursors/modes live in
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-05
|
|
10
10
|
owners: [bala, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Users/DelegatorUser.php
|
|
@@ -112,8 +112,25 @@ paging. Two hard rules in
|
|
|
112
112
|
|
|
113
113
|
## Gotchas
|
|
114
114
|
|
|
115
|
-
- **
|
|
116
|
-
|
|
115
|
+
- **Rows are per signed-in user.** "No options found" in the Commerce pill means the signed-in
|
|
116
|
+
user has no rows **as `userId`** — data, not code (the own-record `GET /v2/users/{uuid}` returns
|
|
117
|
+
200 with an empty list). Seen on client-sandbox 2026-09-30: user 4273 had none; all 8 seeded
|
|
118
|
+
rows belonged to Dev Team (127046). Diagnose with per-user counts of rows as `userId` vs as
|
|
119
|
+
`delegatorUserId`. Re-runnable copy of one user's list to another (no UI or import exists):
|
|
120
|
+
|
|
121
|
+
```sql
|
|
122
|
+
INSERT INTO Users_DelegatorUsers (uuid, userId, delegatorUserId)
|
|
123
|
+
SELECT UUID(), 4273, d.delegatorUserId
|
|
124
|
+
FROM Users_DelegatorUsers d
|
|
125
|
+
WHERE d.userId = 127046
|
|
126
|
+
AND d.delegatorUserId <> 4273
|
|
127
|
+
AND NOT EXISTS (
|
|
128
|
+
SELECT 1 FROM Users_DelegatorUsers x
|
|
129
|
+
WHERE x.userId = 4273 AND x.delegatorUserId = d.delegatorUserId
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The TOGa DB MCP tool is read-only (SELECT/SHOW/DESCRIBE); a developer runs the INSERT.
|
|
117
134
|
- **Never let the `isDelegatorAuthRequest` flag default to true** — the encrypted pair is
|
|
118
135
|
unauthenticated, replayable input.
|
|
119
136
|
- **Outbound logs never carry `userId` or `originalUserId`.** `_underscore/ApiRequest.php` and
|
|
@@ -151,8 +168,9 @@ paging. Two hard rules in
|
|
|
151
168
|
- **The gate now blocks admin View As too.** `canUserOrderForOtherUser` guards
|
|
152
169
|
`/users/encryptedUser` and `/auth/delegator` for everyone, so View As returns **403 EZ-1** for
|
|
153
170
|
any target missing from the caller's `Users_DelegatorUsers` rows (api.beta, user `50160ccc`).
|
|
154
|
-
On prod that means View As works only for listed people.
|
|
155
|
-
|
|
171
|
+
On prod that means View As works only for listed people. Seen on beta and client-sandbox.
|
|
172
|
+
No front-end toast by decision (tcox, 2026-09-30) — prod carries the full list. Whether View
|
|
173
|
+
As needs its own permission path is still undecided.
|
|
156
174
|
2. **`c_orderSubmittedById` must be stamped server side.** The column exists on `SalesOrders` in
|
|
157
175
|
`Client_Compass`, `Client_CompassCanada`, `Client_Quad`, with `CustomRecordFields` id 40 on record
|
|
158
176
|
14 and `_Model_Compass_SalesOrder` L51 as `FIELD_INTEGER`. It is a plain integer, not an FK, so
|
|
@@ -167,6 +185,9 @@ paging. Two hard rules in
|
|
|
167
185
|
|
|
168
186
|
## Change history
|
|
169
187
|
|
|
188
|
+
- 2026-09-30 — "No options found" traced to a user with no rows as `userId`; added the diagnosis
|
|
189
|
+
and the re-runnable copy-list seed; recorded the no-toast decision for the View As 403. (tcox)
|
|
190
|
+
|
|
170
191
|
- 2026-09-28 — Recorded the `/auth/delegator` public-token 500 at `V2.php` L903 (no `user` guard,
|
|
171
192
|
`Logs.Issue` T2) and the decision to leave it unfixed. (bala)
|
|
172
193
|
- 2026-09-22 — Front end now switches the token, so the gate is live: recorded that
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | Expedited shipping options are config-gated to show only when the cart holds a computer kit; open when touching cart shipping-method options or the gate config. |
|
|
15
15
|
| [Filter / Search-Results Page & the Two Search Entry Points](features/filter-search-results-page.md) | Two search entry points (Home in-place vs Header→/filter) render the same card via different code paths and FIELDS files; open for any search-results or kit-car |
|
|
16
16
|
| [Force Logout on Deployment (DeploymentGuard) — commerce](features/force-logout-on-deployment.md) | Signs every user out when a deploy bumps a Core parameter, after showing a notice; open for surprise logouts, "why wasn't I logged out?", a repeating "new versi |
|
|
17
|
-
| ["Order For" — header pill, delegated token switch, and the cart
|
|
17
|
+
| ["Order For" — header pill (a mode), delegated token switch, and the cart lock](features/header-order-for-dropdown.md) | Header pill that puts the session into "Order For" mode (a delegated token) so a buyer orders **as** someone else; open before touching the pill, `useOrderForZu |
|
|
18
18
|
| [Inactive-item purchase gating (standalone lines only — kits are exempt by design)](features/inactive-item-purchase-gating.md) | `Items.isActive = 0` blocks standalone purchase but means something different on kit components, so the gate keys on purchase CONTEXT not the flag; open when to |
|
|
19
19
|
| [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | How the hostname resolves the tenant and fans out to API base URL, theme (CSS variables), and fields; open when adding a tenant or debugging wrong-theme/wrong-A |
|
|
20
20
|
| [Order-submit sync sequencing (useSubmitOrder) — why these calls must not run in parallel](features/order-submit-sync-sequencing.md) | The item sync must finish before the header sync on order submit, or concurrent line deletes race a depth-3 response into an uncaught api2 500; open when touchi |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Order For" — header pill, delegated token switch, and the cart
|
|
2
|
+
title: "Order For" — header pill (a mode), delegated token switch, and the cart lock
|
|
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-10-05
|
|
10
10
|
owners: [tcox, bala]
|
|
11
11
|
files:
|
|
12
12
|
- src/hooks/useOrderForDelegation.ts
|
|
@@ -15,10 +15,15 @@ files:
|
|
|
15
15
|
- src/stores/useOrderForZu.ts
|
|
16
16
|
- src/components/OrderForDropdown/OrderForDropdown.tsx
|
|
17
17
|
- src/components/OrderForDropdown/useOrderForDropdownViewModel.ts
|
|
18
|
+
- src/components/OrderForDropdown/types.ts
|
|
18
19
|
- src/pages/Cart/api/CartApi.ts
|
|
19
20
|
- src/pages/Cart/viewModel/useCartViewModel.ts
|
|
20
21
|
- src/pages/Cart/CartPage.tsx
|
|
22
|
+
- src/contexts/AuthContext.tsx
|
|
23
|
+
- src/lib/authStorage.ts
|
|
21
24
|
- src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/ADMIN/HEADERFIELDS.json
|
|
25
|
+
- src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/MANAGER/HEADERFIELDS.json
|
|
26
|
+
- src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/SUPERUSER/HEADERFIELDS.json
|
|
22
27
|
- cypress/e2e/homePage/orderForDropdown.cy.ts
|
|
23
28
|
related:
|
|
24
29
|
- 2.0/apps/toga2-commerce/architecture.md
|
|
@@ -30,13 +35,15 @@ related:
|
|
|
30
35
|
- 2.0/apps/toga2-commerce/workflows/cypress-testing.md
|
|
31
36
|
---
|
|
32
37
|
|
|
33
|
-
Header pill that
|
|
38
|
+
Header pill that puts the session into "Order For" mode (a delegated token) so a buyer orders **as** someone else; open before touching the pill, `useOrderForZu`, the delegation hooks, or the cart `orderFor` field.
|
|
34
39
|
|
|
35
40
|
## Summary
|
|
36
41
|
|
|
37
|
-
TRUE-81851 (pill) + TRUE-81852 (persistence) + the 2026-09-22 token switch. Picking a person in the header
|
|
42
|
+
TRUE-81851 (pill) + TRUE-81852 (persistence) + the 2026-09-22 token switch + the 2026-10-05 mode redesign. Picking a person in the header pill mints that person's token pair and reloads the app as them — the same mechanism as [View As](view-as-session-tokens.md), with a different meaning (you may order; you are not read-only).
|
|
38
43
|
|
|
39
|
-
**
|
|
44
|
+
**The pill is a MODE, not a mirror of the cart** (decided 2026-10-05, Matt's review). It reads only the delegation state (`useOrderForDelegation.isDelegationActive` + the user store). The cart's "User" field, its remembered pick (`useOrderForZu.orderForUser`) and its selected user never feed the pill, and the pill never writes them — except the cart lock in §4.
|
|
45
|
+
|
|
46
|
+
**Rejected: browser-side "catalog scoping".** Only a real token switch makes api2's `Users_DelegatorUsers` gate, the target's catalog and prices, and `originalUserId` logging apply. Server side: [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
40
47
|
|
|
41
48
|
**Per-role, per-tenant gated.** Copy and the on/off switch live in `HEADERFIELDS.json` under an `orderFor` key, Compass USA only: `isEnabled` true for ADMIN / MANAGER / SUPERUSER, false for USER. Compass Canada and Quad have no key, so the component renders nothing.
|
|
42
49
|
|
|
@@ -46,15 +53,15 @@ TRUE-81851 (pill) + TRUE-81852 (persistence) + the 2026-09-22 token switch. Pick
|
|
|
46
53
|
|
|
47
54
|
On picking a person:
|
|
48
55
|
|
|
49
|
-
1. `GET /users/encryptedUser?uuid=<target
|
|
50
|
-
2. `beginDelegation(pair, ownerUuid)` parks the owner's pair (`src/api/sessionTokens.ts`).
|
|
56
|
+
1. `GET /users/encryptedUser?uuid=<target>`, then `CartApi.fetchUserTokens` (`POST /auth/delegator`) returns the target's pair. It throws on a non-string/empty token and calls `clearPublicToken()`; the caller installs the pair.
|
|
57
|
+
2. `beginDelegation(pair, ownerUuid)` parks the owner's pair (`src/api/sessionTokens.ts`); `setActiveTokens()` bumps the identity epoch.
|
|
51
58
|
3. `setDelegation({targetUuid, ownerUuid})` writes the Order For marker.
|
|
52
59
|
4. Load the target: `fetchUser`, `fetchUserPageSettings`, `fetchUserPersonaData` (query key `userPersonaDataOnOrderFor`), then `setUser`.
|
|
53
|
-
5. Clear the
|
|
60
|
+
5. Clear the cart, **await** `clearPersistedQueryCache(queryClient)` (`src/lib/queryCacheRestoration.ts` — never `queryClient.clear()` + `removeItem("commerce")`), `navigate("/")`, `window.location.reload()`.
|
|
54
61
|
|
|
55
|
-
**"
|
|
62
|
+
**Leaving the mode** = the pill's "x" (`exitMode` → `returnToMyself`), which restores the parked pair through the shared `useReturnToOriginalUser` hook.
|
|
56
63
|
|
|
57
|
-
The hook also **repairs a half-finished switch** on load (a reload
|
|
64
|
+
The hook also **repairs a half-finished switch** on load (a reload between step 3 and step 4): restore the owner pair, or end the session. That repair reads the stores **live** — the header mounts the hook twice (desktop + mobile), and a stale second copy would roll back again, find no parked pair, and log the user out. `endSession` uses `void logout().then(() => navigate("/"))` because logout is async (it clears the persisted cache first).
|
|
58
65
|
|
|
59
66
|
### 2. The delegation list (`fetchDelegationList` in `src/pages/Cart/api/CartApi.ts`)
|
|
60
67
|
|
|
@@ -62,13 +69,13 @@ Reads `userDelegatorUsers.delegatorUser.{uuid,_name,email}` off `GET /users/{own
|
|
|
62
69
|
|
|
63
70
|
- **Call it before the token switch** — on the target's token the owner's own record 403s.
|
|
64
71
|
- **Never request `id`** — it 403s the whole call. Use `uuid`.
|
|
65
|
-
- **No paging.**
|
|
66
|
-
-
|
|
67
|
-
- `Users_DelegatorUsers`
|
|
72
|
+
- **No paging.** Search filters the list in the browser.
|
|
73
|
+
- Snapshotted in `useOrderForZu.delegationOptions` so it survives on the target's token.
|
|
74
|
+
- **"No options found" = no `Users_DelegatorUsers` rows for the signed-in user** (data, not code; the own-record GET returns 200 with an empty list). Diagnose and seed per [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
68
75
|
|
|
69
76
|
### 3. Telling Order For apart from View As (`src/stores/useOrderForZu.ts`)
|
|
70
77
|
|
|
71
|
-
Both park the original user in `useReturnOriginalUserFromViewAsStore` and share **one** parked-token slot, so they cannot nest. Order For is identified by its own marker `delegation {targetUuid, ownerUuid}` in `useOrderForZu` (localStorage `order-for-zu`), separate from the cart's pick `orderForUser
|
|
78
|
+
Both park the original user in `useReturnOriginalUserFromViewAsStore` and share **one** parked-token slot, so they cannot nest. Order For is identified by its own marker `delegation {targetUuid, ownerUuid}` in `useOrderForZu` (localStorage `order-for-zu`), separate from the cart's pick `orderForUser`.
|
|
72
79
|
|
|
73
80
|
Helpers (store) and their hooks (`src/hooks/useIsViewAsActive.ts`):
|
|
74
81
|
|
|
@@ -78,54 +85,63 @@ Helpers (store) and their hooks (`src/hooks/useIsViewAsActive.ts`):
|
|
|
78
85
|
| `isOrderForSwitchPending(delegation, currentUuid, originalUuid)` | marker written, user store still the owner — mid-switch |
|
|
79
86
|
| `useIsViewAsActive` | parked user present **and** not Order For active **and** not switch pending |
|
|
80
87
|
|
|
81
|
-
Consumers: `CartPage.tsx` (checkout gate `isViewAsOn`), `AuthLayout.tsx` (banner), `NavIconList.tsx` (
|
|
88
|
+
Consumers: `CartPage.tsx` (checkout gate `isViewAsOn`), `AuthLayout.tsx` (banner), `NavIconList.tsx` (cart-clear mount effect runs for real View As only; starting View As calls `endDelegation()`).
|
|
82
89
|
|
|
83
|
-
### 4. The cart while delegated
|
|
90
|
+
### 4. The cart while delegated — the only header-to-cart link
|
|
84
91
|
|
|
85
|
-
`
|
|
92
|
+
`useCartViewModel.ts` forces the `CARTPAGEFIELDS` `orderFor` input (`valueKey: "orderFor"`) to `isEditable: false` while delegated: the "User" field is read-only text holding the picked person. `CartPage` re-seeds that field with the current user after **Clear cart**. **Clear cart clears only the cart's own pick** — it never leaves the mode.
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
The cart owns the order-for cascade (contact, manager, cost center, notification emails) — see [cart-notification-emails](cart-notification-emails.md). The header must not duplicate it or write `useSelectedUserZu`.
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
Scope decision: My Settings, My Shipping Addresses and Get Support stay read-only in both modes.
|
|
90
97
|
|
|
91
|
-
### 5. The pill
|
|
98
|
+
### 5. The pill (`src/components/OrderForDropdown/`)
|
|
92
99
|
|
|
93
|
-
MVVM split
|
|
100
|
+
MVVM split, rendered from `Header.tsx`, hidden in edit-order mode; mobile copy inside `SlideMenu`. The view model sets `currentOrderForUser = delegated ? user : null` and `isOrderingForSomeoneElse = isDelegationActive` (shared `runSwitch` helper for pick and exit).
|
|
94
101
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
102
|
+
- **Default:** icon + "Order for" on one line, no name. It **never** says "Myself".
|
|
103
|
+
- **In the mode:** "Order for" with the person's name under it, plus an **"x"** button at the right end (FontAwesome regular `xmark` via `getFontAwesomeIcon`) that leaves the mode.
|
|
104
|
+
- Structure: a wrapper `div` carries the outline/glow; the trigger button and the "x" button sit inside it as siblings — a button cannot nest a button.
|
|
105
|
+
- Teal `#00B9A8` outline + inset glow **only on hover and while open**. No permanent "in mode" style yet (needs design); the mode shows by the name, the "x" and `data-active="yes"`.
|
|
106
|
+
- Menu: search, then the list. **The signed-in person is never listed** (the old "Name (Myself)" row is gone). Check mark on the current pick, `aria` listbox/option roles, Escape closes and returns focus, outside click closes.
|
|
107
|
+
- Copy keys under `orderFor`: `isEnabled`, `label`, `searchPlaceholder`, `loadingMessage`, `noOptionsFoundMessage`, `clearLabel` (screen-reader name of the "x", default "Clear order for"). `myselfLabel` is still in the JSON but unused by the header.
|
|
108
|
+
- Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name` (mode only), `order-for-clear` (mode only), `order-for-menu`, `order-for-search`, `order-for-option`; mobile variants add `-mobile`.
|
|
100
109
|
|
|
101
110
|
### 6. `useOrderForZu` — persisted, owner-scoped
|
|
102
111
|
|
|
103
|
-
Zustand `persist`, localStorage `order-for-zu`. Holds `orderForUser {uuid, name, email, ownerUuid}`, `delegation` and `delegationOptions`. Read the pick through `getOrderForUserFor(pick, ownerUuid)`
|
|
112
|
+
Zustand `persist`, localStorage `order-for-zu`. Holds the cart's pick `orderForUser {uuid, name, email, ownerUuid}`, `delegation` and `delegationOptions`. Read the cart pick through `getOrderForUserFor(pick, ownerUuid)` (owner match + real uuid). `clearOrderForUser()` drops the cart pick only; `endDelegation()` drops everything (return to self, View As start, and `AuthContext.resetClientState` on sign-in).
|
|
113
|
+
|
|
114
|
+
`originalAuthTokens` and `order-for-zu` are in `AUTH_STORAGE_KEYS` (`src/lib/authStorage.ts`), so every sign-in / sign-out / 401 logout path drops the parked pair and the marker.
|
|
104
115
|
|
|
105
116
|
## Gotchas
|
|
106
117
|
|
|
107
|
-
- **
|
|
118
|
+
- **The header reads only the delegation state.** Do not wire the cart's pick or selected user into the pill (either direction) — that was the old mirror design, rejected 2026-10-05.
|
|
119
|
+
- **Never test "View As is on" with `Object.keys(returnOriginalUser).length > 0`.** Order For parks the same user; use `useIsViewAsActive`.
|
|
108
120
|
- **Order For and View As cannot nest** — one `originalAuthTokens` slot, and `beginDelegation` refuses to overwrite it.
|
|
109
|
-
- **The marker is written before the target is loaded**, so there is a gap where the parked user exists but the user store is still the owner.
|
|
110
|
-
- **Order of operations on return matters.** In `useReturnToOriginalUser`, `setUser(owner)`
|
|
111
|
-
- **
|
|
112
|
-
- **The `ownerUuid` guard is what makes persistence safe — never drop it.** A pick with no owner check is inherited by the next login or by a delegated target.
|
|
121
|
+
- **The marker is written before the target is loaded**, so there is a gap where the parked user exists but the user store is still the owner. `isOrderForSwitchPending` covers it; any new "am I delegated?" check must handle it.
|
|
122
|
+
- **Order of operations on return matters.** In `useReturnToOriginalUser`, `setUser(owner)` → `clearOriginalUser` → `endDelegation` must run right after `restoreOriginalTokens` and **before any await**.
|
|
123
|
+
- **The `ownerUuid` guard is what makes persistence safe — never drop it.**
|
|
113
124
|
- **A missing `orderFor` key renders nothing, by design** (`useAssignClientFields` has no fallback). Adding a tenant means adding the key to every role file for that tenant.
|
|
114
|
-
- **Delegation failures are silent.** `onError` only logs to Sentry — no toast (developer's call).
|
|
125
|
+
- **Delegation failures are silent.** `onError` only logs to Sentry — no toast (developer's call).
|
|
126
|
+
- **"The fix is not there" after a merge is usually a branch that never had the commits.** Check `git branch --contains <sha>` / `git log A..B` before calling it a regression (the cart lock "stopped working" on `_sandbox-client` because the feature was never merged there).
|
|
127
|
+
- **Merge of `_feature-OrderForDropDown` into `_sandbox-client` (6d84f565):** sandbox wins on the session layer (TRUE-81991, see [view-as-session-tokens](view-as-session-tokens.md)), feature wins on Order For. The old store API (`pendingOrderForUser`, `getPendingOrderForUserFor`, `clearPendingOrderForUser`, `fetchUserAccessToken`) no longer exists — do not bring it back from a stale branch.
|
|
115
128
|
- **`_production` and `_sandbox-client` have diverged in `EditCart.tsx`**, so a stashed patch will not apply cleanly. Move work with `git diff stash@{0}^1 stash@{0} > patch`, `git apply --check --3way patch`, then `git apply --3way --exclude=<conflicting file> patch` and hand-port the rest.
|
|
116
129
|
- **Tailwind: `max-h-300px` is not a token** — use `max-h-[300px]`.
|
|
117
|
-
- **`.env.development` points at `https://api.beta.togahub.com/v2` by default.** client-sandbox is `https://api.client.sandbox.togahub.com/v2` — check the
|
|
130
|
+
- **`.env.development` points at `https://api.beta.togahub.com/v2` by default.** client-sandbox is `https://api.client.sandbox.togahub.com/v2` — check `authority` in the API response JSON before reading the wrong environment's logs.
|
|
118
131
|
|
|
119
132
|
## Open items
|
|
120
133
|
|
|
121
|
-
-
|
|
122
|
-
- `
|
|
134
|
+
- No permanent "in mode" pill style — needs a design.
|
|
135
|
+
- `SalesOrders.c_orderSubmittedById` is still never set. It belongs in `_Model_Compass_SalesOrder::prePost()` from `$api->delegator->user`, not in the client.
|
|
136
|
+
- `Logs_Compass.Api.originalUserId` was **NULL** for every row of an Order For session on client-sandbox (2026-09-22). See [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
123
137
|
|
|
124
138
|
## Change history
|
|
125
139
|
|
|
126
|
-
- 2026-
|
|
127
|
-
- 2026-09-
|
|
128
|
-
- 2026-09-
|
|
140
|
+
- 2026-10-05 — Pill redesigned as a mode (Matt's review): default "Order for" with no name and never "Myself"; in the mode the name shows under the label plus an "x" that returns to self; the signed-in person is no longer listed; the header no longer reads or writes the cart's pick; Clear cart clears only the cart pick. Added `clearLabel` and test id `order-for-clear`. (tcox)
|
|
141
|
+
- 2026-09-30 — Merged into `_sandbox-client` (6d84f565) on top of the TRUE-81991 session layer: `originalAuthTokens` + `order-for-zu` added to `AUTH_STORAGE_KEYS`; `fetchUserTokens` returns the pair; `resetClientState` calls `endDelegation()`; switch and return use `clearPersistedQueryCache`. Old pending-pick store API removed. (tcox)
|
|
142
|
+
- 2026-09-22 — Order For now switches the token (encryptedUser, /auth/delegator, beginDelegation, reload); added the `delegation` marker, `useIsViewAsActive` / `useIsOrderForDelegationActive`, the read-only cart `orderFor` field while delegated, the shared `useReturnToOriginalUser`, and the no-paging list. (tcox)
|
|
143
|
+
- 2026-09-21 — `Users_DelegatorUsers`, route `user-delegator-users` and the enforced `/auth/delegator` exist (TRUE-81848); added `fetchDelegationList`. (bala)
|
|
144
|
+
- 2026-09-17 — Pick persisted to `order-for-zu` with an `ownerUuid` guard (TRUE-81852). (tcox)
|
|
129
145
|
- 2026-09-17 — Header order-for pill added (TRUE-81851). (tcox)
|
|
130
146
|
|
|
131
147
|
## Related
|
|
@@ -6,12 +6,15 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-05
|
|
10
10
|
owners: [tcox, bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- src/api/sessionTokens.ts
|
|
13
13
|
- src/api/apiBaseUrl.ts
|
|
14
14
|
- src/api/axiosInstance.ts
|
|
15
|
+
- src/lib/session.ts
|
|
16
|
+
- src/lib/authStorage.ts
|
|
17
|
+
- src/lib/queryCacheRestoration.ts
|
|
15
18
|
- src/contexts/AuthContext.tsx
|
|
16
19
|
- src/hooks/useReturnToOriginalUser.ts
|
|
17
20
|
- src/hooks/useIsViewAsActive.ts
|
|
@@ -41,7 +44,7 @@ All token plumbing lives in **`src/api/sessionTokens.ts`** (new). `src/api/apiBa
|
|
|
41
44
|
1. `fetchUserTokens(uuid, "/auth/delegator")` mints the delegated pair for the target user.
|
|
42
45
|
2. `beginDelegation(delegatedPair, adminUuid)` parks the admin's current pair under localStorage key **`originalAuthTokens`** as `{ v: 1, access, refresh, userUuid, apiHost, savedAt }`, then writes the delegated pair into `accessToken` / `refreshToken`.
|
|
43
46
|
3. It **never overwrites an existing parked pair** — view-as is one level deep. `isViewAsEnabled` also hides the button when `hasSavedOriginalTokens()` is true.
|
|
44
|
-
4. On any error entering view-as, it rolls back: `restoreOriginalTokens` if a pair is parked, plus `clearOriginalUser()`.
|
|
47
|
+
4. On any error entering view-as, it rolls back: restores the admin's label language (switched before the persona fetch), `restoreOriginalTokens` if a pair is parked, plus `clearOriginalUser()`. If the parked pair cannot be restored it logs out: `useAuth().logout().then(() => navigate("/"))`.
|
|
45
48
|
5. It also calls `endDelegation()` so an old Order For marker cannot make the new session read as Order For, and its mount effect that clears the cart is gated on `useIsViewAsActive` — otherwise it wiped an Order For cart on every load.
|
|
46
49
|
|
|
47
50
|
### Which mode is running — `useIsViewAsActive`
|
|
@@ -57,17 +60,17 @@ Moved out of `AuthLayout` so View As and Order For return through one copy. `set
|
|
|
57
60
|
1. Checks `userUuid` **and** `apiHost` match, and age ≤ `PARKED_PAIR_MAX_AGE_MS` (**12h**). It **always removes** the parked key, pass or fail.
|
|
58
61
|
2. On success: puts the admin pair back; if the access JWT is expired (`isAccessTokenExpired` decodes `exp`), calls `refreshAuthTokens()` once **before** the cache clear.
|
|
59
62
|
3. On failure: logs a Sentry warning, clears the view-as store, logs out, navigates to `/`. There is **no** `/auth/encrypted` fallback.
|
|
60
|
-
4.
|
|
63
|
+
4. Calls `clearPersistedQueryCache(queryClient)` (`src/lib/queryCacheRestoration.ts`: waits for a pending restore, cancels in-flight queries, empties memory + `localStorage["commerce"]`) so the impersonated user's rows cannot rehydrate under the admin. The no-parked-pair path logs out with `void logout().then(() => navigate("/"))` — logout is async.
|
|
61
64
|
|
|
62
65
|
### Where else the parked pair is cleared
|
|
63
66
|
|
|
64
|
-
`
|
|
67
|
+
`originalAuthTokens` (and Order For's `order-for-zu`) are in `AUTH_STORAGE_KEYS` (`src/lib/authStorage.ts`); `clearAuthStorage` runs on sign-in (`resetClientState`), sign-out and the 401 `performLogout`. `fetchPublicToken` clears it too; module load sweeps it when there is no `accessToken`. `hasSession()` (`src/lib/session.ts`) is keyed on localStorage `user`.
|
|
65
68
|
|
|
66
69
|
### Refresh safety (`axiosInstance.ts`)
|
|
67
70
|
|
|
68
|
-
The 401 refresh is **single-flight** (one module-level promise shared by concurrent 401s) and **identity-bound
|
|
71
|
+
The 401 refresh (`performTokenRefresh`) is **single-flight** (one module-level promise shared by concurrent 401s) and **identity-bound** by an **identity epoch** (`getIdentityEpoch` / `bumpIdentityEpoch` in `src/lib/session.ts`) plus a refresh-token comparison. The epoch is bumped on sign-in, sign-out and every token swap (`sessionTokens.setActiveTokens()`). The token comparison alone was not enough: an older admin refresh finishing late restored the **higher-privilege** admin token under the delegated identity. See [SSO & session gating](./sso-redirect-and-session-gating.md).
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
If the epoch or refresh token changed while the refresh was in flight, it **stores nothing and returns the current `localStorage.accessToken`** (whoever is current now). A stale current token just gets its own 401 and a fresh refresh.
|
|
71
74
|
|
|
72
75
|
### Decisions (CSO review — SHIP WITH FIXES)
|
|
73
76
|
|
|
@@ -81,22 +84,24 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
|
|
|
81
84
|
|
|
82
85
|
- **Never re-mint the admin's tokens on return.** The old `/auth/encrypted` path was a privileged call made from the browser. Restore the parked pair, or log out — those are the only two outcomes.
|
|
83
86
|
- **Always remove `originalAuthTokens`, even on a failed restore.** Leaving a stale parked pair blocks the next view-as (`beginDelegation` refuses to overwrite) and keeps an old admin refresh token on disk.
|
|
84
|
-
- **Clear `
|
|
85
|
-
-
|
|
87
|
+
- **Clear the persisted cache on every identity switch with `clearPersistedQueryCache`**, never `queryClient.clear()` + `removeItem("commerce")` — a pending restore can write the old rows back. Without a clear, the impersonated user's data rehydrates under the admin (see the 24h persisted-cache gotcha in [architecture](../architecture.md)).
|
|
88
|
+
- **⚠ A refresh that lost the identity race must never return `null`.** Every 401 waiting on the shared refresh then calls `performLogout` (`hasSession()` is still true), so a delegated token expiring just before return-to-self logged the owner out (found by cso, 2026-09-30). Return the current access token, write nothing. Bump the epoch on every identity change.
|
|
86
89
|
- **⚠ Optional-chain the whole token path on the swap.** `response?.data.tokens.access` wrote the literal string `"undefined"` on a malformed reply — truthy, so every later call sent `Bearer undefined`.
|
|
87
90
|
- **Never test "View As is on" with `Object.keys(returnOriginalUser).length > 0`.** Order For parks the same user; use `useIsViewAsActive`.
|
|
88
91
|
- **Clear the delegated state before any await on return.** `setUser(owner)` / `clearOriginalUser` / `endDelegation` must land before the token refresh or any refetch, or an expired parked token's refresh opens a window where the "parked pair gone while delegated" guard logs the user out (and the banner and cart-clear flicker).
|
|
89
|
-
- **⚠ Admin View As is now gated by the Order For delegation list.** `canUserOrderForOtherUser` guards `/users/encryptedUser` and `/auth/delegator` for everyone, so View As returns **403 EZ-1** for any target not on the signed-in user's `Users_DelegatorUsers` rows (
|
|
92
|
+
- **⚠ Admin View As is now gated by the Order For delegation list.** `canUserOrderForOtherUser` guards `/users/encryptedUser` and `/auth/delegator` for everyone, so View As returns **403 EZ-1** for any target not on the signed-in user's `Users_DelegatorUsers` rows (beta and client-sandbox). The UI shows nothing — `onError` only logs to Sentry; no toast by decision (tcox), since prod carries the full list.
|
|
90
93
|
- **A view-as test must stub `/v2/pages/meta?page=home-page&app=commerce`.** Unstubbed, the real call fails and triggers the enter-view-as rollback, so the spec sees a return-to-admin it did not ask for.
|
|
91
94
|
|
|
92
95
|
## Open items (out of ticket, not fixed)
|
|
93
96
|
|
|
97
|
+
- On a 401 logout the in-memory zustand stores are not reset, so a store write in the ≤1.5s before the redirect can write `zu-cart` / `order-for-zu` back. Low risk: login runs `resetClientState` (cso NIT, 2026-09-30).
|
|
94
98
|
- `SENTRY_AUTH_TOKEN` is committed in 25 tracked `.env*` files — treat it as compromised and rotate it. (Never store the value in this KB.)
|
|
95
99
|
- ~~Confirm api2 authorizes `/auth/delegator` **server-side**~~ — **answered 2026-09-21:** it now re-checks `_Model_Client_User::canUserOrderForOtherUser` at mint time (and `/auth/refresh` re-checks it), so a captured encrypted pair cannot be replayed. See [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
96
100
|
- ~~The parked-token slot is shared with "Order For" — decide precedence~~ — **settled 2026-09-22:** one slot, no nesting; the mode is told apart by the Order For marker, not the slot.
|
|
97
101
|
|
|
98
102
|
## Change history
|
|
99
103
|
|
|
104
|
+
- 2026-09-30 — Order For merged onto the TRUE-81991 session layer on `_sandbox-client`: parked pair cleared via `AUTH_STORAGE_KEYS`; return and switch use `clearPersistedQueryCache`; a refresh that lost the identity race now returns the current token instead of `null` (stopped a false logout on return); View As `onError` restores the admin's language and logs out if the parked pair is gone. (tcox)
|
|
100
105
|
- 2026-09-22 — Refresh identity binding upgraded to an epoch bumped on the view-as swap; `Bearer undefined` swap bug fixed. TRUE-81991 (`e546db3d`). (apeterson)
|
|
101
106
|
- 2026-09-22 — Return moved to the shared `useReturnToOriginalUser` hook (state cleared before any await); mode now tested with `useIsViewAsActive` because Order For parks the same user; View As start calls `endDelegation()` and its cart-clear effect is gated; recorded the 403 EZ-1 when the target is not on the delegation list. (tcox)
|
|
102
107
|
- 2026-09-21 — `/auth/delegator` is now gated server side by the Order For delegation check; noted that Order For will share the single parked-token slot. (bala)
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-05
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/cypress/e2e/cartPage/cartV2.cy.ts
|
|
@@ -101,7 +101,7 @@ The cart UI was instrumented with `data-testid` attributes so specs anchor on **
|
|
|
101
101
|
|
|
102
102
|
### Other active specs
|
|
103
103
|
|
|
104
|
-
- `cypress/e2e/homePage/orderForDropdown.cy.ts` — the header order-for pill, COMPASS only, **5 tests**. Now covers the delegated **token switch**: stubs `users/{admin}?*userDelegatorUsers*`, `encryptedUser`, `POST auth/delegator`, the target-user fetch by regex (`delay: 1500`), `pages/meta` and bundles. Ids `order-for
|
|
104
|
+
- `cypress/e2e/homePage/orderForDropdown.cy.ts` — the header order-for pill, COMPASS only, **5 tests**. Now covers the delegated **token switch**: stubs `users/{admin}?*userDelegatorUsers*`, `encryptedUser`, `POST auth/delegator`, the target-user fetch by regex (`delay: 1500`), `pages/meta` and bundles. Ids `order-for-*`; `order-for-current-name` and `order-for-clear` (the "x") exist **only in the mode**, so assert `not.exist` for them in the default state (see [header-order-for-dropdown](../features/header-order-for-dropdown.md)).
|
|
105
105
|
|
|
106
106
|
**Testing a token switch — four rules learned here:**
|
|
107
107
|
|
|
@@ -142,9 +142,12 @@ On prod base `aedd1f7a`, Compass tenant: **13 failures** — `getSupportV2` (4),
|
|
|
142
142
|
- **ESLint fails repo-wide in a fresh checkout** because `.eslintrc.cjs` references `eslint-plugin-react-compiler`, which is not in `package.json`, so `npm run lint` fails on a clean install. Check changed files with `tsc` plus `npx eslint --no-eslintrc --plugin react-hooks --plugin @typescript-eslint <files>`. This is a local-environment issue, not a spec problem.
|
|
143
143
|
- **`npm install` rewrites `package-lock.json` on a newer npm** (e.g. tanstack 5.101.4 → 5.103.2). Do not ship the lock file in a feature commit unless the bump is intended.
|
|
144
144
|
- **Never start a second Vite dev server in a repo someone else is running.** It corrupts `node_modules/.vite` and breaks their browser. Recovery: stop every server, delete `node_modules/.vite`, restart one.
|
|
145
|
+
- **First run after a source edit often fails one test** with "Expected to find element ... but never found it" while Vite re-bundles on the running dev server. Rerun before debugging; a real failure repeats.
|
|
145
146
|
|
|
146
147
|
## Change history
|
|
147
148
|
|
|
149
|
+
- 2026-10-05 — `orderForDropdown.cy.ts` follows the pill-as-mode redesign (`order-for-clear`, mode-only ids); added the first-run-after-edit Vite flake. (tcox)
|
|
150
|
+
|
|
148
151
|
- 2026-09-22 — `orderForDropdown.cy.ts` now covers the delegated token switch (5 tests); added the four token-switch testing rules (reload race, non-retrying "never appears" check, 403 not 401, catch-all intercept ordering) plus the package-lock drift and second-Vite-server warnings. (tcox)
|
|
149
152
|
- 2026-09-17 — Added `viewAsUser.cy.ts`; orderForDropdown now green; recorded the `ELECTRON_RUN_AS_NODE` failure, the intercept pitfalls, and the 13 pre-existing red specs on base `aedd1f7a`. (tcox)
|
|
150
153
|
- 2026-09-17 — Created with the V2 convention set and the `cartV2.cy.ts` cart harness (slice 1). (tcox)
|
|
@@ -21,7 +21,7 @@ project: _Underscore
|
|
|
21
21
|
client: compass-usa
|
|
22
22
|
type: profile
|
|
23
23
|
status: active
|
|
24
|
-
updated: 2026-
|
|
24
|
+
updated: 2026-10-05
|
|
25
25
|
owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean, rgirish]
|
|
26
26
|
files: []
|
|
27
27
|
related:
|
|
@@ -219,6 +219,7 @@ separate, related client (see its own profile).
|
|
|
219
219
|
the ⚠ rule that **deactivation is raw bulk SQL, so no model hook ever fires on it**.
|
|
220
220
|
|
|
221
221
|
## Notes
|
|
222
|
+
- **⚠ SALES_ORDERS froze 2026-10-02 → 10-05 (Issue 940): closed-order delete matched the Office Depot twin SO** (dual-SO trap). Fixed in library with the per-client scope; open: higher-tier mirror fulfillments 91650/91651. Details: [closed-order rule](../../1.0/apps/library/features/netsuite-closed-order-rule.md).
|
|
222
223
|
- **⚠ SALES_ORDERS froze 2026-09-29 on ACL gaps the closed-order delete hit** (transfer-orders 312/313, entitlement-sales-orders 257, sales-order-payments 254). Fixed by `dbchanges2/Client_Compass/2026-09-29a - TransferOrderAclExpressions.sql`, then mode reset to `864000-IDLE`. Details: [closed-order rule](../../1.0/apps/library/features/netsuite-closed-order-rule.md).
|
|
223
224
|
- **⚠ The 1.0 NetSuite sync links item fulfillments ONLY to `customerId=3` / `locationId=615286`
|
|
224
225
|
SalesOrders (2026-09-16).** This Compass-specific hardcoded scope filter lives in
|
package/package.json
CHANGED