toga-ai 1.0.940 → 1.0.942
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/_underscore/features/apirequest-json-content-type.md +12 -2
- package/knowledge/2.0/apps/_underscore/features/cli-script-bootstrap.md +11 -1
- package/knowledge/2.0/apps/_underscore/features/order-for-delegation.md +26 -5
- package/knowledge/2.0/apps/dbchanges2/architecture.md +2 -1
- 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/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/8x8-post-call-survey-import.md +86 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +6 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/pcmaticb2b/features/startech-ticket-sync.md +7 -2
- package/knowledge/clients/true/profile.md +4 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal", "ajean", "rgirish", "kyalamarthi"]
|
|
9
|
+
updated: 2026-10-05
|
|
10
|
+
owners: ["jcardinal", "ajean", "rgirish", "kyalamarthi", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/ApiRequest.php
|
|
13
13
|
- _underscore/Model/Client/Logs/Api.php
|
|
@@ -17,6 +17,7 @@ related:
|
|
|
17
17
|
- ../../worker2/features/creating-worker-actions.md
|
|
18
18
|
- ../../worker2/features/talos-transcript-ingestion.md
|
|
19
19
|
- togaiq-gateway-client.md
|
|
20
|
+
- ../../worker2/features/8x8-post-call-survey-import.md
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
`_ApiRequest`, the 2.0 outbound HTTP client — JSON encode/decode, retry, and api-logging traps; open before calling it or reading `Logs.Api`.
|
|
@@ -94,6 +95,14 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
|
|
|
94
95
|
**hardcodes** its token in committed source — flagged for a rotate-and-move-to-config ticket; 11
|
|
95
96
|
worker2 files use it). When adding a new component, decide deliberately: log and accept the header
|
|
96
97
|
exposure, strip/redact the auth header before the call is logged, or `setLogging(false)`.
|
|
98
|
+
Basic auth via `CURLOPT_USERPWD` is **not** logged (it is not a header); bearer headers and
|
|
99
|
+
request/response bodies **are**. Vendor calls carrying secrets or caller PII → `setLogging(false)`
|
|
100
|
+
(e.g. [8x8 survey import](../../worker2/features/8x8-post-call-survey-import.md)).
|
|
101
|
+
- **⚠ OPEN, not fixed: SSL peer verification is OFF by default.** `ApiRequest.php:238` sets
|
|
102
|
+
`CURLOPT_SSL_VERIFYPEER` to `0` whenever `sslVerifyPeer` is null — despite the "AUTO based on
|
|
103
|
+
https" comment, `!null` is true, so an unset value never verifies, https included. New components
|
|
104
|
+
must call `setSslVerifyPeer(true)`. Changing the default touches every caller — needs a `cto`
|
|
105
|
+
opinion. 1.0 twin: [library architecture](../../../1.0/apps/library/architecture.md).
|
|
97
106
|
- **Multipart/file upload ALREADY WORKS — do not add an `ENCODE__MULTIPART` case (2026-09-04).**
|
|
98
107
|
The request-encoding switch (`ApiRequest.php:205-228`) has **no `default:` case**, so
|
|
99
108
|
`payloadEncoding: null` passes the payload through **untouched** and sets no `Content-Type`; `:236`
|
|
@@ -153,6 +162,7 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
|
|
|
153
162
|
`dtStamp` (indexed) or it times out — `WHERE source = '…'` alone will not return.
|
|
154
163
|
|
|
155
164
|
## Change history
|
|
165
|
+
- 2026-10-05 — Added SSL-verify-off default (OPEN) and basic-auth-not-logged note, from TRUE-82185 (mhammontree)
|
|
156
166
|
- 2026-09-25 — renamed product reference from "TogaIQ" to "Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
|
|
157
167
|
|
|
158
168
|
## Related
|
|
@@ -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", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Database.php
|
|
@@ -66,6 +66,15 @@ For DB-**free** unit tests of interceptor logic, use the PHPUnit scaffold instea
|
|
|
66
66
|
anything touches `App_Framework`. `__APPROOT__/app/framework.php` shadows the library file for
|
|
67
67
|
*both* class names, so resolving `App_Framework` first loads the app file and the library
|
|
68
68
|
definition never arrives.
|
|
69
|
+
- **Locally, `registerClientDatabases()` can fail outright.** It reads `Clients` through the local
|
|
70
|
+
`Core` alias, which on a laptop may point at the 1.0 `Core` DB (no `Clients` table). Register the
|
|
71
|
+
client schema directly instead: `_Database::register(...)` with `database` = the real schema
|
|
72
|
+
(e.g. `Client_True`) and `alias` = `DB_CLIENT`. Windows MySQL reports schema names lower-case
|
|
73
|
+
(`client_true`) — compare case-insensitively.
|
|
74
|
+
- **⚠ The 2.0 debug error page prints `print_r($GLOBALS)`, including `$_SERVER` / env.** A script
|
|
75
|
+
that reads a secret from env vars must scrub them (`putenv`, `unset($_SERVER[...], $_ENV[...])`)
|
|
76
|
+
and keep the value out of global scope (inside a function) **before** the framework loads, or the
|
|
77
|
+
first error dumps the secret to the screen.
|
|
69
78
|
- Do not drive a local harness through `_Worker::runTask` — the dev config's
|
|
70
79
|
`[cloud] aws_worker_queue_url` points at the **production** SQS queue. Call the target method
|
|
71
80
|
in-process. (Also noted on [email-template-sending.md](./email-template-sending.md).)
|
|
@@ -79,4 +88,5 @@ For DB-**free** unit tests of interceptor logic, use the PHPUnit scaffold instea
|
|
|
79
88
|
— the work these traps surfaced in.
|
|
80
89
|
|
|
81
90
|
## Change history
|
|
91
|
+
- 2026-10-05 — Added local `registerClientDatabases` failure + debug-page `$GLOBALS` secret leak gotchas (TRUE-82185) (mhammontree)
|
|
82
92
|
- 2026-09-25 — Added lazy-CWD autoloader gotcha (found building the worker2 Codeception harness) (mhammontree)
|
|
@@ -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
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-05
|
|
10
10
|
owners: [jcardinal, mhammontree, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -189,6 +189,7 @@ When the executor processes a client, it applies that client's own folder **plus
|
|
|
189
189
|
|---|---|---|
|
|
190
190
|
| `netsuite` | `_modules/netsuite/` | Compass, Canon, Ncci, Sbasite, Spglobal, … (most clients) |
|
|
191
191
|
| `startech` | `_modules/startech/` | Pcmatic |
|
|
192
|
+
| `8x8` | `_modules/8x8/` | True (8x8 post-call survey tables, TRUE-82185) |
|
|
192
193
|
|
|
193
194
|
Module folders use the **same `YYYY-MM-DD` naming + ordering rules** as client folders.
|
|
194
195
|
|
|
@@ -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)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary |
|
|
4
4
|
|-----|---------|
|
|
5
5
|
| [Worker (worker2) Architecture](architecture.md) | How worker2 (Elastic Beanstalk Worker Tier background-job app) works overall — job pipeline, MySQL/SQS roles, Lambdas, routing, transactions; open for any worke |
|
|
6
|
+
| [8x8 Post-Call Survey (CSAT) Import (8x8/Surveys → _modules/8x8 tables)](features/8x8-post-call-survey-import.md) | worker2 imports 8x8 Contact Center post-call surveys (CSAT) from the 8x8 Analytics API into `Client_True` tables, hourly, with an import-run watermark; open bef |
|
|
6
7
|
| [Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)](features/alb-target-group-auto-registration.md) | Deploy-time postdeploy hook that self-registers a non-prod EB instance into the shared-ALB target group; open when a non-prod worker2/saml/api2 env is unreachab |
|
|
7
8
|
| [All-Client Email Queue Monitor (Monitor/Operations/EmailQueue)](features/all-client-email-queue-monitor.md) | Cross-client health check for the 2.0 outbound email queue (`Logs_<Client>.Email`); open when working on email-queue alerting or an all-client DB fan-out. |
|
|
8
9
|
| [API 2.0 Stability Monitor (Monitor/Operations/ApiStability)](features/api-stability-monitor.md) | End-to-end authenticated round-trip probe of the production 2.0 API; open when working on API liveness monitoring or a server-side call to the public-auth endpo |
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "8x8 Post-Call Survey (CSAT) Import (8x8/Surveys → _modules/8x8 tables)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-10-05
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/8x8/Surveys.php
|
|
13
|
+
- _underscore/Component/Api/8x8/8x8.php
|
|
14
|
+
- dbchanges2/Core/2026-10-02a - 8x8 partner and survey import cron.sql
|
|
15
|
+
- dbchanges2/_modules/8x8/2026-10-02a - PostCallSurveys.sql
|
|
16
|
+
related:
|
|
17
|
+
- ../../_underscore/features/apirequest-json-content-type.md
|
|
18
|
+
- ../../_underscore/features/cloud-secrets-manager.md
|
|
19
|
+
- ../../_underscore/features/cli-script-bootstrap.md
|
|
20
|
+
- ./creating-worker-actions.md
|
|
21
|
+
- ../../../clients/true/profile.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
worker2 imports 8x8 Contact Center post-call surveys (CSAT) from the 8x8 Analytics API into `Client_True` tables, hourly, with an import-run watermark; open before touching the 8x8 component, the survey tables, or 8x8 credentials.
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
**Status (2026-10-05): built on branch `TRUE-82185` in `_underscore`, `worker2`, `dbchanges2` — NOT committed, NOT deployed.** Ticket TRUE-82185; design from the 2026-09-29 "8x8 Integration" meeting.
|
|
29
|
+
|
|
30
|
+
Replaces the manual 8x8 CSV export + Excel pivots. Phase 1 = store only. Later phases link surveys to Desk tickets, Chat, call records. Power BI / Talos read the tables directly (no `Core.Records` registration yet).
|
|
31
|
+
|
|
32
|
+
One 8x8 tenant (`agilantsolutionsi01`, TOGA's call center) serves many end clients (Office Depot, Craftex, Walmart, PC Matic). **All surveys land in `Client_True`**; the end client is identified by `surveyName` / `queueName`. Routing to per-end-client DBs is deferred.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
**Credentials.** AWS Secrets Manager `partners/8x8/<clientIdentifier>` (us-east-1), JSON `{clientId, secret, region}`. Not in `Core.PartnerApiIdentities.partnerApiPassword` — that column is a STRING RecordField on record 305, readable through the records API.
|
|
37
|
+
|
|
38
|
+
**Component `_Component_Api_8x8`** (`_underscore/Component/Api/8x8/8x8.php`):
|
|
39
|
+
1. Token: `POST https://api.8x8.com/oauth/v2/token`, client_credentials, Basic `clientId:secret`, lives ~1799s.
|
|
40
|
+
2. Create report: `POST /analytics/cc/v8/historical-metrics/detailed-reports`, type `detailed-reports-survey`; returns `DONE` immediately.
|
|
41
|
+
3. Page data: `GET /detailed-reports/{id}/data?size=1000`; next-page cursor is the `Last-Document-Id` **response header**.
|
|
42
|
+
4. Region path prefix: us `''`, `eu`, `au`, `ca`.
|
|
43
|
+
5. `Logs.Api` logging OFF on every call (bearer token + caller PII); `setSslVerifyPeer(true)`. A 204 / empty body = empty page. Exception messages carry response shape only, never values.
|
|
44
|
+
|
|
45
|
+
**Worker `_Worker_8x8_Surveys`** (`worker2/Worker/8x8/Surveys.php`):
|
|
46
|
+
1. `8x8/Surveys/Dispatch` (cron `15 * * * *`, 300s) queues one `8x8/Surveys/Import` per client with an 8x8 row in `Core.PartnerApiIdentities`.
|
|
47
|
+
2. `Import(int $clientId, ?string $windowStart, ?string $backfillStart)` — one client per job, 1-day windows, 180s budget, then re-queues itself via `runTask` with `windowStart`.
|
|
48
|
+
3. Start = last SUCCESS `dtWindowEnd` − 48h overlap; with no SUCCESS row, `backfillStart` is required (else throws).
|
|
49
|
+
4. Upsert on `(source, externalResponseKey)` via raw escaped `INSERT … ON DUPLICATE KEY UPDATE` — `_Model` has no upsert and `save()` cascades to parents. Agents/answers are delete+reinsert **only** when the survey row changed (affected rows 0 = unchanged → skip).
|
|
50
|
+
5. Fail loud: a bad record throws, the window is marked FAILED, the watermark does not advance.
|
|
51
|
+
6. RUNNING guard also treats a SUCCESS completed within 300s as running (closes the chain hand-off gap).
|
|
52
|
+
7. Read host disabled for the job (`_Database::setIsReadHostEnabled(false)`) — see Gotchas.
|
|
53
|
+
|
|
54
|
+
**Schema.**
|
|
55
|
+
- `Core`: `PartnerApiIdentities.clientApiId` now NULL-able; `Partners` row `8x8`; identity row clientIdentifier `True` → partnerIdentifier `agilantsolutionsi01`; `CronJobs` `8x8/Surveys/Dispatch` inserted **INACTIVE**.
|
|
56
|
+
- Module `8x8` (`_modules/8x8/`, listed in `Client_True/_modules.txt`): `PostCallSurveys`, `PostCallSurveyAgents`, `PostCallSurveyAnswers`, `PostCallSurveyImports` (the import-run table is the watermark, not `MAX(dtSurveyStarted)`).
|
|
57
|
+
- Survey times stored **UTC** (8x8 returns local time + offset). Vendor values (`surveyType`, `questionType`, `answerType`) are VARCHAR, not ENUM; our `status` is ENUM.
|
|
58
|
+
|
|
59
|
+
**8x8 data shape (probed 2026-10-02).**
|
|
60
|
+
- One row = one survey. `surveyId` is the survey **template** (1201 OD Customer Survey, 2164 Craftex CSAT, 1221 Walmart Customers, 1161 PC Matic Survey); there is no response id. Unique key = `surveyId` + highest `interactionId` (ids start with a hex timestamp; 1–2 per row on transfer).
|
|
61
|
+
- Answers nested in `items[key=results].value[]`; values like `StayOnCall`, `Scale`, `VoiceComment`, `Valid`, `Recorded`. `surveyIsDeleted` is the string `"false"`. Durations in ms. `voiceRecordingUuid` is per answer (`callerVoiceInput-<uuid>`).
|
|
62
|
+
- Volume ~110 surveys/day.
|
|
63
|
+
|
|
64
|
+
**Deploy order** (human steps):
|
|
65
|
+
1. Run the Core SQL (cron stays inactive).
|
|
66
|
+
2. Run the module SQL on `Client_True`.
|
|
67
|
+
3. Verify the `PartnerApiIdentities` row for `True`.
|
|
68
|
+
4. devops grants the worker2 EB role `secretsmanager:GetSecretValue` on `partners/8x8/*`.
|
|
69
|
+
5. Deploy `_underscore` (Rohan's weekly window), then worker2.
|
|
70
|
+
6. Run `8x8/Surveys/Import` by hand with `backfillStart`.
|
|
71
|
+
7. Verify SUCCESS rows in `PostCallSurveyImports`, including one empty window.
|
|
72
|
+
8. Set `CronJobs.isActive = 1`.
|
|
73
|
+
|
|
74
|
+
**Local verification.** `test/@Mark/TRUE-82185/upsert_local_test.php` feeds a saved real page through private `upsertSurvey` (ReflectionMethod) into local `Client_True`: 25 inserted; re-run 25 unchanged, 0 answers rewritten. Full end-to-end through the worker not yet run (needs AWS secret access).
|
|
75
|
+
|
|
76
|
+
## Gotchas
|
|
77
|
+
|
|
78
|
+
- **Omit the report `timezone` param.** The tenant rejects `"UTC"` ("Timezone UTC is not valid or configured for this tenant"); convert to UTC in PHP.
|
|
79
|
+
- **The 1.0 VCC API has no survey data.** `vcc-na20.8x8.com/api` (basic auth tenant+token; `library/app/api/8x8.php`, `worker/crons/togadesk/send_uma_8x8_daily_report.php`) gives queue stats only. Surveys need the Analytics API (CC API v7+).
|
|
80
|
+
- **8x8 developer reference pages render in JavaScript** — WebFetch gets no schemas. Confirm fields with a real call.
|
|
81
|
+
- **Read-after-write in the job:** SELECTs go to the read host (a separate connection) and cannot see the job's uncommitted insert. Disable the read host for the job — same fix as `_Email` (TRUE-80448). See [api-payload-interceptors](../../api2/features/api-payload-interceptors.md).
|
|
82
|
+
- **DB errors: store/rethrow only `errno` + error text.** The raw `_Query` message repeats the whole INSERT, with caller PII, into `WorkerJobs.output` / `Logs.Issue`.
|
|
83
|
+
- **8x8 report storage:** each run creates reports on 8x8's side (~72/day). Not cleaned up unless 8x8 complains (decided 2026-10-02).
|
|
84
|
+
|
|
85
|
+
## Change history
|
|
86
|
+
- 2026-10-05 — Created: Phase 1 survey import built on `TRUE-82185` (uncommitted, not deployed); single-tenant → `Client_True` decision, Secrets Manager creds, import-run watermark (mhammontree)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-05
|
|
10
10
|
owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
@@ -513,10 +513,15 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
|
|
|
513
513
|
failure indefinitely. The dry run must exercise the **same helpers** as the real path — a separate
|
|
514
514
|
test harness that reimplements them proves nothing. See
|
|
515
515
|
[netsuite-opportunity-sync](./netsuite-opportunity-sync.md#gotchas--known-issues) for what it caught.
|
|
516
|
+
- **Read-after-write inside an action needs the read host OFF.** `_Query` sends SELECTs to the read
|
|
517
|
+
host — a separate connection that cannot see the job's own uncommitted insert (not replication
|
|
518
|
+
lag). Call `_Database::setIsReadHostEnabled(false)` for the job, as `_Email` (TRUE-80448) and
|
|
519
|
+
[8x8 survey import](./8x8-post-call-survey-import.md) do.
|
|
516
520
|
- See [architecture.md](../architecture.md) for the always-HTTP-200 rule and the
|
|
517
521
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
518
522
|
|
|
519
523
|
## Change history
|
|
524
|
+
- 2026-10-05 — Added read-host read-after-write gotcha for worker actions (TRUE-82185) (mhammontree)
|
|
520
525
|
- 2026-09-17 — Added the **half-written `WorkerJobs` row** tell: a row with `dtStarted` but no
|
|
521
526
|
`dtCompleted`/`isSuccess`/`output` means the **action class is not on the box**, not a hung job.
|
|
522
527
|
`Controller/Index.php` (~L322) calls `method_exists($className, 'initialize')` one line **above**
|
package/knowledge/INDEX.md
CHANGED
|
@@ -20,7 +20,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
20
20
|
## 2.0 framework
|
|
21
21
|
|
|
22
22
|
- **_underscore** (_Underscore) _(framework core)_ — 94 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
23
|
-
- **worker2** (Worker) —
|
|
23
|
+
- **worker2** (Worker) — 82 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
24
24
|
- **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
25
25
|
- **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
26
26
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -5,8 +5,8 @@ project: TOGa
|
|
|
5
5
|
client: pcmaticb2b
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
9
|
-
owners: [snaredla, jcardinal]
|
|
8
|
+
updated: 2026-10-05
|
|
9
|
+
owners: [snaredla, jcardinal, mhammontree]
|
|
10
10
|
files:
|
|
11
11
|
- worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php
|
|
12
12
|
- worker/crons/toga2/startech/common_import_supporting_records.php
|
|
@@ -177,6 +177,8 @@ const TOGADESK_TICKET_TYPE__TOGADESK_PCS = 'TOGaDesk-PCS';
|
|
|
177
177
|
- **Webhook must set `c_escalateToStartech=0`** — without this, the `postPost` gate evaluates `!null = true` and would skip Startech for webhook-originated tickets on any subsequent PUT.
|
|
178
178
|
- **ticketType uses `c_startechTicketTypeId` dynamically** — stored on `TicketTypes`, not hardcoded in the worker.
|
|
179
179
|
|
|
180
|
+
- **⚠ OPEN (found 2026-10-05, not fixed): Startech credentials are exposed in two places.** (1) `_underscore/Component/Api/Startech/Startech.php` hardcodes `STARTECH_API_USERNAME` / `STARTECH_API_PASSWORD` class constants in committed source — rotate, then move to Secrets Manager. (2) `Core.PartnerApiIdentities.partnerApiPassword` holds the Startech password in plaintext and is a STRING RecordField on record 305, so the records API can read it. New partners use Secrets Manager instead (see [8x8 survey import](../../../2.0/apps/worker2/features/8x8-post-call-survey-import.md)).
|
|
181
|
+
|
|
180
182
|
### Sync prerequisites & gotchas (2026-07-06)
|
|
181
183
|
- **The entitlement gate blocks the 2.0 create.** A TOGaDesk ticket only syncs up to TOGA 2.0 (and can then reach Startech) if its email maps to an existing 2.0 contact **with an active entitlement**. `_Model_Pcmaticb2b_Ticket::prePost` rejects the create otherwise, so the desk ticket never gets a `referenceId` — and the escalate handler shows "Ticket is not linked to TOGa 2.0".
|
|
182
184
|
- **These prePost rejections are now HTTP 400, not 500 (fixed 2026-08-25).** `_Model_Pcmaticb2b_Ticket::prePost()` (the 2.0 pre-interceptor for `POST /v2/tickets`) previously threw a **plain `Exception`** for its four client-actionable rejections — *"Ticket requires a contact…"*, *"Contact not found for uuid=…"*, *"Contact not found for email=…"*, *"No active entitlement found for contact"* — each producing **HTTP 500 (EO-1)** plus a `Logs.Issue` and a Sentry event (the "requires a contact" one was prod `Logs.Issue` ref `FG`, 215 occurrences). All four now throw **`_Exception_Validation`**, which `Controller/Index.php` maps to **HTTP 400** — the client sees it in the response envelope with no Issue/Sentry/5xx-health impact. A client-input / business rejection belongs at 400, not 500. (See the 2.0 backend-php standard, *Choosing an exception class is a ROUTING decision*.)
|
|
@@ -195,3 +197,6 @@ const TOGADESK_TICKET_TYPE__TOGADESK_PCS = 'TOGaDesk-PCS';
|
|
|
195
197
|
- [Startech webhook handler (worker2)](../../../2.0/apps/worker2/features/startech-webhook-handler.md)
|
|
196
198
|
- [Library Startech pcmaticb2b sync](../../../1.0/apps/library/features/startech-pcmaticb2b-sync.md)
|
|
197
199
|
- [Startech entitlement provisioning](startech-entitlement-provisioning.md)
|
|
200
|
+
|
|
201
|
+
## Change history
|
|
202
|
+
- 2026-10-05 — Recorded open Startech credential exposure (hardcoded constants + plaintext `partnerApiPassword` RecordField) (mhammontree)
|
|
@@ -16,7 +16,7 @@ project: _Underscore
|
|
|
16
16
|
client: true
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-10-
|
|
19
|
+
updated: 2026-10-05
|
|
20
20
|
owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -24,6 +24,7 @@ related:
|
|
|
24
24
|
- ../../2.0/apps/saml/features/gateway-environments-and-redirect-safety.md
|
|
25
25
|
- ../../2.0/apps/_underscore/features/core-domains-app-host-registry.md
|
|
26
26
|
- ../../1.0/apps/togadesk/features/staff-sso-login.md
|
|
27
|
+
- ../../2.0/apps/worker2/features/8x8-post-call-survey-import.md
|
|
27
28
|
---
|
|
28
29
|
|
|
29
30
|
"True" — TOGa's internal/staff tenant (`Client_True`, identifier `True`, default email SSO mapper); open for its internal apps (Tools, sprint dashboard, togatech site) and its Talos-pricing internal-tenant role.
|
|
@@ -44,6 +45,8 @@ The **Tools** app (1.0; repo `tools`) authenticates True users via **SSO** throu
|
|
|
44
45
|
|
|
45
46
|
**TOGa Desk** (1.0; repo `togadesk`) staff sign in via the same True SSO (TRUE-78064, prod only, not deployed as of 2026-10-02): True user email → one AD People row → existing `TOGaDeskSupport.people` row; no auto-create. Domain row id 261 (repointed from an unused devteam seed row) `uuid d1e526b9-e2fb-4462-935f-ed449e8f5986`, `clientId 1`, `appId 18`, env 1, `https://togadesk.togatech.com/desk/` — live in prod Core 2026-10-02. See [TOGa Desk staff SSO](../../1.0/apps/togadesk/features/staff-sso-login.md).
|
|
46
47
|
|
|
48
|
+
**8x8 post-call surveys (CSAT)** for every end client served by TOGA's one 8x8 call-center tenant (Office Depot, Craftex, Walmart, PC Matic) are stored in `Client_True` (module `8x8` in `Client_True/_modules.txt`); the end client is read from `surveyName` / `queueName`. Built on TRUE-82185, not deployed as of 2026-10-05. See [8x8 survey import](../../2.0/apps/worker2/features/8x8-post-call-survey-import.md).
|
|
49
|
+
|
|
47
50
|
## ⚠ "Base" maps to Super User in Client_True
|
|
48
51
|
|
|
49
52
|
In `Client_True` the role **named `Base`** maps to **`coreRoleId` 3 = Super User**. Every other
|
package/package.json
CHANGED