toga-ai 1.0.1180 → 1.0.1182

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.
@@ -6,10 +6,13 @@ project: Library
6
6
  client: pcmaticb2b
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-22
9
+ updated: 2026-10-28
10
10
  owners: [snaredla, mhammontree]
11
11
  files:
12
12
  - library/app/api/toga2.php
13
+ - togadesk/desk/includes/controllers/quickactions.php
14
+ - _underscore/Component/Api/Startech/Startech.php
15
+ - _underscore/Trait/Startech/Ticket.php
13
16
  - library/app/api/startechticket.php
14
17
  - library/app/apitransaction.php
15
18
  - worker/crons/toga2/startech/common_import_supporting_records.php
@@ -104,7 +107,8 @@ Imports Startech (Easeedesk V3) config into TOGA 2.0 — users, groups, devices,
104
107
  ## Gotchas
105
108
 
106
109
  - **Do not add a NULL early-return guard in this function.** NULL `c_escalateToStartech` means the ticket was created in TOGaDesk — it must still sync to TOGA 2.0. The Startech gate lives in `_Trait_Startech_Ticket::postPost`, not here.
107
- - **Open (2026-10-22): MSP tickets with creator/`ticket_affected_user` = 9665 fail to load in Startech's UI.** 9665 = Agilant Api fallback `DEFAULT_AFFECTED_USER_ID` in `_underscore/Component/Api/Startech/Startech.php`. Per-client override = `Parameters` key `STARTECH_DEFAULT_AFFECTED_USER_ID`; none of the 4 MSPs set it. Decision pending.
110
+ - **Default affected user is per client type (decided 2026-10-28, `_underscore` PR #807).** Startech will not load MSP tickets whose creator/`ticket_affected_user` is 9665. Retail (PC Matic B2B) keeps **9665** (Agilant Api); MSP uses **9696** (Toga Helpdesk). Code constants `RETAIL_/MSP_DEFAULT_AFFECTED_USER_ID` in `RETAIL_/MSP_ESCALATION_DEFAULTS['affectedUser']` (`_underscore/Component/Api/Startech/Startech.php`, used by `Trait/Startech/Ticket.php`); `Parameters` key `STARTECH_DEFAULT_AFFECTED_USER_ID` still overrides. Developer chose constants over a Parameters row; **do not change the PC Matic default**. 9696 still awaiting confirmation from Razvan (Startech).
111
+ - **"Escalate to Startech" on an unlinked ticket is invisible to the sync watermark.** Without a `referenceId`, `quickactions.php` `escalateToStartech` only sets `tickets.escalateToStartech=1` + adds `tickets_history`; `tickets.timestamp` (datetime, no auto-update) does not move, so `syncWithTogadesk` misses it. Fix (TRUE-82720): `$isFieldEditSyncEnabled` covers Elite **plus all Startech MSP clients** (`STARTECH_CLIENT_UUIDS` minus PC Matic B2B), so the latest `tickets_history` time counts. Applies to every MSP, not only IM Houston. The sync also sends `c_escalateToStartech=1` on create.
108
112
  - **`SUPPORT REQUEST` with a space** — `TOGA2_TICKET_TYPE_CODE__SUPPORT = 'SUPPORT REQUEST'`. Using `'SUPPORT'` silently breaks type matching.
109
113
  - **`$isPcmaticb2bUuid` flag** is used for PC Matic B2B-specific branching throughout `syncToga2TicketIntoTogadesk1Ticket()`.
110
114
  - **Sentry `IMPORT_SUPPORTING_RECORDS_<CLIENT>.PHP >>> StarTech auth error: ` with nothing after the colon = network failure, not bad credentials.** `App_Api_StarTechTicket::authenticate()` calls `execute(false)`, which returns false and keeps no cURL error text (10s `setTimeout` timeout, dropped connection, DNS), so response code and body are empty. A real refusal shows an HTTP code + body. These crons set `App_ApiTransaction::$isLoggingEnabled = false`, so there is no API log row either. A single event heals itself on the next 2-min run — no action. Decided 2026-10-16 (WORKER1-4DQ, IM Houston): **no login retry** for one-off blips. Only investigate if events repeat or show an HTTP code.
@@ -6,15 +6,15 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-08
9
+ updated: 2026-10-28
10
10
  owners: [bala, tcox]
11
11
  files:
12
12
  - _underscore/Model/Client/Users/DelegatorUser.php
13
13
  - _underscore/Model/Client/User.php
14
14
  - _underscore/Model/Client/Logs/Api.php
15
15
  - api2/Component/Api/V2/V2.php
16
- - dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql
17
- - dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql
16
+ - dbchanges2/Core/2026-10-23a - DelegatedUserRecordScript.sql
17
+ - dbchanges2/Client_Compass/2026-10-23a - OrderForAndViewAsAcl.sql
18
18
  related:
19
19
  - ../../api2/features/request-logging.md
20
20
  - ./acl-permission-chain.md
@@ -29,8 +29,12 @@ Server side of "Order For" (TRUE-81848): who may order for whom, where that is e
29
29
 
30
30
  A user may place an order **as** another user only when a `Users_DelegatorUsers` row says so.
31
31
  Three pieces: the junction table (who may order for whom), the gate
32
- (`_Model_Client_User::canUserOrderForOtherUser`, checked at both mint points), and the
33
- `originalUserId` audit column (who actually signed in).
32
+ (`_Model_Client_User::canUserOrderForOtherUser`), and the `originalUserId` audit column (who
33
+ actually signed in).
34
+
35
+ **Order For and View As are two different things with two endpoints** (since 2026-10-23, TRUE-81848):
36
+ `delegatedUser` = Order For, list-checked; `encryptedUser` = View As, unrestricted and Admin-only by
37
+ ACL. See §2.
34
38
 
35
39
  **The table ships EMPTY** — no admin screen, no import. Until rows exist every delegation is
36
40
  refused and every delegation dropdown is empty. That is intended, not a bug.
@@ -47,19 +51,36 @@ Core metadata (`Core/2026-09-18a`): record **362**, route `user-delegator-users`
47
51
  **2607** delegatorUserId, registered as an **inherent child of record 2 (Users)** so
48
52
  `userDelegatorUsers` is readable nested under `/users/{uuid}`.
49
53
 
50
- ### 2. The gate — checked TWICE
54
+ ### 2. Two mint endpoints — Order For is list-checked, View As is not
55
+
56
+ One endpoint could not be both. The list check lived **inside** `encryptedUser`, so it also refused
57
+ admin View As with **403 EZ-1** for anyone off the caller's list. Split 2026-10-23:
58
+
59
+ | Endpoint | `Core.RecordScripts` | Meaning | Delegation-list check |
60
+ |---|---|---|---|
61
+ | `GET /users/encryptedUser` | 16 | **View As** (read-only impersonation) | none — Admin role only, by ACL grant |
62
+ | `GET /users/delegatedUser` | **31** | **Order For** (order as someone else) | `canUserOrderForOtherUser` |
63
+
64
+ Both call one private builder, `buildEncryptedUserPair()`, in `_Model_Client_User`.
65
+
66
+ **The purpose rides INSIDE the ciphertext.** `/auth/delegator` receives only the `{client,user}`
67
+ pair, so the pair itself must say which it is — otherwise a View As pair passes as an Order For pair
68
+ or the reverse. `delegatedUser` prefixes the user uuid with the constant
69
+ `DELEGATION_PREFIX__ORDER_FOR = 'order-for:'` **before** encrypting; `V2.php` strips it after
70
+ decrypting and re-checks the delegation list **only** for an Order For pair. The stripped value must
71
+ match a uuid pattern or `readUuidFromEncryptedUserValue()` returns an empty string and the caller
72
+ fails closed.
51
73
 
52
- `_Model_Client_User::canUserOrderForOtherUser($orderingUserId, $orderedForUserId)` (L354), used by
53
- `getRequestingUser()` / `encryptedUser` (L398) and again by `/auth/delegator` in
54
- `api2/Component/Api/V2/V2.php`.
74
+ **The marker then rides in the JWT.** The encrypted pair never expires, so a one-time check at
75
+ switch time is not enough — a deleted `Users_DelegatorUsers` row must end the live session. On a
76
+ successful `/auth/delegator`, `$this->delegator->isOrderFor` is set and lands in the token;
77
+ `/auth/refresh` re-checks the list whenever that marker is **not literally `false`**. So a token
78
+ issued before this change (no marker) is treated as Order For and fails closed, and View As sessions
79
+ are never re-checked — View As is not on the list.
55
80
 
56
- - Check 1 = when the encrypted `{client,user}` pair is minted.
57
- - Check 2 = when `/auth/delegator` exchanges that pair for a token. **This second check is the real
58
- control**: the encrypted pair has no expiry and no MAC (`_String::encryptWithKey`, AES-256-CBC
59
- keyed on `Core.Parameters.API_SECRET_REFRESH_TOKEN`), so a captured pair could otherwise be
60
- replayed forever.
61
- - `/auth/refresh` re-checks an active delegation, so deleting the row or deactivating either
62
- account **ends the live session**, not just new switches.
81
+ Check order, end to end: mint (`delegatedUser` only) → `/auth/delegator` exchange (the real control;
82
+ the pair has no expiry and no MAC — `_String::encryptWithKey`, AES-256-CBC keyed on
83
+ `Core.Parameters.API_SECRET_REFRESH_TOKEN`) → every `/auth/refresh`.
63
84
 
64
85
  `V2.php` sets `$isDelegatorAuthRequest = true` as the **first statement** of `case 'delegator'`,
65
86
  which then falls through into `case 'encrypted'`; the flag is declared `false` (L609) so a forged or
@@ -74,6 +95,11 @@ log-write sites plus the CloudWatch entry.
74
95
 
75
96
  - `userId` = who the token is for · `originalUserId` = who actually signed in · **NULL = no
76
97
  delegation**. No row should ever have `originalUserId = userId`.
98
+ - **It is the only record of who really placed an Order For order.** `SalesOrders.c_orderSubmittedById`
99
+ is still NULL on real orders, so read the `POST /v2/sales-orders` row in `Logs_<Tenant>.Api`.
100
+ Confirmed 2026-10-23 on client-sandbox order **SA138906**: `userId` = the person ordered for,
101
+ `originalUserId` = the person who signed in. (The 2026-09-22 "never stamped on client-sandbox"
102
+ finding is closed.)
77
103
  - Verified on local order SA136321: `SalesOrders.createdByUserId` = 124256 (Dawn Anderson), log row
78
104
  `userId 124256 / originalUserId 127046` (Dev Team). Last 200 rows: 126 own-token (NULL) + 70
79
105
  delegated, zero self-referencing.
@@ -94,10 +120,30 @@ CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END
94
120
  `GET /v2/user-delegator-users` and list who every colleague may order for. `AclUser` is joined by
95
121
  `_Model_True::isPermitted` on the logged-in user. Chain rules: [ACL permission chain](./acl-permission-chain.md).
96
122
 
97
- **Role ids differ per tenant** — US Compass **7** = Compass Base, **8** = Manager; Compass Canada
98
- **8** = Compass Base, **9** = Manager. Canada also needed `Core.RecordScripts` **16**
99
- (`encryptedUser`) granted to its roles 8 and 9 — it shipped with role 1 only, so every normal Canada
100
- user got a 403 on a pick. US already had 16 for roles 3/7/8.
123
+ **Role ids differ per tenant** — US Compass **7** = Compass Base, **8** = Manager, **4** = Admin;
124
+ Compass Canada **8** = Compass Base, **9** = Manager, **5** = Admin.
125
+
126
+ **Script grants after the split** (`2026-10-23a - OrderForAndViewAsAcl.sql` per tenant). Script
127
+ **31** (`delegatedUser`, Order For) goes to the ordering roles; script **16** (`encryptedUser`, View
128
+ As) is narrowed to Admin:
129
+
130
+ | Tenant | 31 granted to | 16 added | 16 deleted from |
131
+ |---|---|---|---|
132
+ | `Client_Compass` | 7, 8 | 4 | 3, 7, 8 |
133
+ | `Client_CompassCanada` | 8, 9 | 5 | 1, 3, 8, 9 |
134
+
135
+ **⚠ The grant rows are now the ONLY control on script 16.** The list check used to sit inside the
136
+ function and covered every tenant whatever the grants said. Prod audit 2026-10-23: **31 of 37**
137
+ client databases grant 16 to role **1 (`Base`)** — the role every user holds (role 3 is `API` in
138
+ every client DB; Northwell grants 3, Prudential 1 and 3). Live impact is small today: only
139
+ `toga2-commerce` calls script 16, the button is shown only for the Compass US / Canada **ADMIN**
140
+ persona (`viewAs.isEnabled` in `HEADERFIELDS.json`), and prod `Api` logs for the three months to
141
+ 2026-10-23 show **zero** `encryptedUser` calls with an `apiId` in Prudential, Northwell and Quad and
142
+ zero API-key calls in Compass. Narrow a tenant's grants before enabling View As there.
143
+
144
+ **Compass Canada prod never ran `Client_CompassCanada/2026-09-18a`** — read 2026-10-23, prod grants
145
+ 16 to role 1 only while client-sandbox has 1, 8 and 9. The 2026-10-23a DELETE is written to cover
146
+ both: it clears sandbox and matches nothing extra in prod.
101
147
 
102
148
  **Dev-sandbox needs its own files** (`2026-09-21a … DevSandbox.sql`) because the pinned field ids
103
149
  collide there — field ids **2767/2768/2769/2770**, record 362 unchanged. See
@@ -107,11 +153,21 @@ collide there — field ids **2767/2768/2769/2770**, record 362 unchanged. See
107
153
 
108
154
  `toga2-commerce/src/pages/Cart/api/CartApi.ts` → `fetchDelegationList(ownUuid)` returns
109
155
  `DelegationOption[]` (`{uuid, name, email}`) — everyone that user may order for, one call, no
110
- paging. Two hard rules in
156
+ paging, sorted by name in the browser. `fetchUserTokens` takes the mint route as a required third
157
+ argument (`MINT_ROUTE__ORDER_FOR = "delegatedUser"` / `MINT_ROUTE__VIEW_AS = "encryptedUser"`), so
158
+ each feature picks its own endpoint. Nothing else changed in the front end — the `isOrderFor` marker
159
+ lives in the JWT and only the server reads it. Two hard rules in
111
160
  [header order-for dropdown](../../toga2-commerce/features/header-order-for-dropdown.md).
112
161
 
113
162
  ## Gotchas
114
163
 
164
+ - **⚠ Deploy `_underscore` and api2 together.** Version skew looks like a data problem:
165
+ `GET /v2/users/delegatedUser` returns 200 and the next `POST /v2/auth/delegator` returns **403
166
+ EZ-1** with the debug line `Invalid User`. New `_underscore` writes the `order-for:` prefix into
167
+ the ciphertext; old api2 never strips it and looks up a user whose uuid is `order-for:<uuid>`.
168
+ - **Never let a mint endpoint decide a meaning the exchange cannot see.** `/auth/delegator` gets
169
+ only the encrypted pair, so anything it must enforce has to be inside the ciphertext (the prefix)
170
+ or in the token it issues (`isOrderFor`) — not in the calling route.
115
171
  - **Rows are per signed-in user.** "No options found" in the Commerce pill means the signed-in
116
172
  user has no rows **as `userId`** — data, not code (the own-record `GET /v2/users/{uuid}` returns
117
173
  200 with an empty list). Seen on client-sandbox 2026-09-30: user 4273 had none; all 8 seeded
@@ -182,19 +238,9 @@ FROM Logs_Compass.Api ORDER BY id DESC LIMIT 20;
182
238
 
183
239
  ## Still open (next person: do not rediscover)
184
240
 
185
- 1. ~~The front end does not switch the token~~ — **done 2026-09-22.** Commerce now runs
186
- `encryptedUser` → `/auth/delegator` → `beginDelegation` → reload on every Order For pick; see
187
- [header order-for dropdown](../../toga2-commerce/features/header-order-for-dropdown.md). Two
188
- findings from that session are still open:
189
- - **`originalUserId` is not being stamped on client-sandbox.** Every `Logs_Compass.Api` row of a
190
- real Order For session (2026-09-22 11:10–11:13, `userId` 303631, including
191
- `POST /v2/sales-orders` at 11:12:36) has `originalUserId` NULL.
192
- - **The gate now blocks admin View As too.** `canUserOrderForOtherUser` guards
193
- `/users/encryptedUser` and `/auth/delegator` for everyone, so View As returns **403 EZ-1** for
194
- any target missing from the caller's `Users_DelegatorUsers` rows (api.beta, user `50160ccc`).
195
- On prod that means View As works only for listed people. Seen on beta and client-sandbox.
196
- No front-end toast by decision (tcox, 2026-09-30) — prod carries the full list. Whether View
197
- As needs its own permission path is still undecided.
241
+ 1. ~~The front end does not switch the token~~ (done 2026-09-22) and ~~the gate blocks admin View
242
+ As~~ — **settled 2026-10-23** by the two-endpoint split in §2. `originalUserId` is confirmed
243
+ stamping (§3).
198
244
  2. **`c_orderSubmittedById` must be stamped server side.** The column exists on `SalesOrders` in
199
245
  `Client_Compass`, `Client_CompassCanada`, `Client_Quad`, with `CustomRecordFields` id 40 on record
200
246
  14 and `_Model_Compass_SalesOrder` L51 as `FIELD_INTEGER`. It is a plain integer, not an FK, so
@@ -209,6 +255,11 @@ FROM Logs_Compass.Api ORDER BY id DESC LIMIT 20;
209
255
 
210
256
  ## Change history
211
257
 
258
+ - 2026-10-28 — Split into two record scripts: `encryptedUser` (16) = View As, no list check,
259
+ Admin-only by ACL; new `delegatedUser` (31) = Order For, list-checked. Purpose carried in the
260
+ ciphertext (`order-for:` prefix) and then in the JWT (`isOrderFor`, re-checked on refresh);
261
+ 2026-10-23a ACL files for both Compass tenants; prod audit of script-16 grants; `originalUserId`
262
+ confirmed stamping (SA138906). (bala)
212
263
  - 2026-10-08 — Retired the `/order-for` skill; moved its last facts here (JWT claims, the
213
264
  `empty()` gate trap, Core-cannot-name-api2, dead Core ACL rows, refusal check SQL). (jcardinal)
214
265
  - 2026-09-30 — "No options found" traced to a user with no rows as `userId`; added the diagnosis
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-18
10
- owners: ["kyalamarthi", "mhammontree", "tcox", "jcardinal"]
9
+ updated: 2026-10-28
10
+ owners: ["kyalamarthi", "mhammontree", "tcox", "jcardinal", "bala"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/Model/Team/Sprint.php
@@ -111,12 +111,13 @@ So for a scripted call, **authorization comes solely from `AclCustomRecordScript
111
111
  - **No bound params in `_Query`.** Sanitize with `\_Database::escape()` (strings) and `(int)` casts (numerics) for every value placed into the query text — query-string args are user input.
112
112
  - **Restore the query-cache flag.** `useQueryCache(false)` is a global toggle; capture the prior value and restore it, or you disable caching for the rest of the request.
113
113
  - **`aclDatabase` has no `TEAM`.** A `Team`-schema record registers as `'CORE'`; picking `'CLIENT'` sends the engine looking in the wrong cluster.
114
- - **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.** Concrete case (verified on production 2026-08-13): `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next free; prod max was 24"), and its sibling `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id 30** in production, so the grants pointed at nothing and a later file (`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that references a stale id inserts a row happily and fails **only** at request time as a missing-grant error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always verify the actual id in the target environment. **In a `Client/` grant file the Core lookup is not allowed (other cluster)**, so the file types the id as a literal with header SELECTs to run on Core; the operator replaces the literal per environment before running (approved manual step, jcardinal 2026-10-18).
114
+ - **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.** Concrete case (verified on production 2026-08-13): `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next free; prod max was 24"), and its sibling `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id 30** in production, so the grants pointed at nothing and a later file (`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that references a stale id inserts a row happily and fails **only** at request time as a missing-grant error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always verify the actual id in the target environment. **In a `Client/` grant file the Core lookup is not allowed (other cluster)**, so the file types the id as a literal with header SELECTs to run on Core; the operator replaces the literal per environment before running (approved manual step, jcardinal 2026-10-18). **✅ Better — stop the renumbering at the source: write the `id` as an explicit literal in the `Core` INSERT** rather than letting `AUTO_INCREMENT` pick it, so the same id exists in every environment and the client literal is correct everywhere. Confirm the id is free in **every** environment first — `Core/2026-10-23a - DelegatedUserRecordScript.sql` pinned **31** after checking that prod, client-sandbox and dev-sandbox each held 30 rows with max id 30. The client grant file still opens with a **STEP 0** header block: run the Core file first, read the printed id, replace the literal if it differs.
115
115
  - **⚠ `phpMethod` must not reuse a `_Model` method name.** `search` on `_Model_Core_Record` fatals ("Cannot make non static method … static") on every request that loads Records — `_Model` already has non-static `search()`. Global search uses `globalSearch` ([global search](../../_underscore/features/global-search.md)).
116
116
  - **Registration is per-environment and split.** A missing `Core.RecordScripts` row → the route isn't a script (falls through to normal CRUD / 404-shaped behavior); a missing `AclRecordScripts` grant → the caller's roles can't run it. Both rows must exist in every environment the API serves.
117
117
  - **The committed-tile definition is intentionally not the sprint-scoring definition.** `committedTile()` uses the **Power BI / current-state** definition of done/committed (`dtDone IS NOT NULL` + `workTypeNow`), which is *deliberately different* from TOGa Talos's canonical sprint scoring (`statusNow IN (STATUS_IN__DONE)` + `workTypeAtLock`). See [Team Sprint Management](../../worker2/features/team-sprint-management.md) before assuming a tile agrees with sprint scores.
118
118
 
119
119
  ## Change history
120
+ - 2026-10-28 — Pin a new `Core.RecordScripts` `id` as an explicit literal in the Core INSERT (checked free in every environment) so client grant literals hold; STEP 0 header in the client file. TRUE-81848 (bala)
120
121
  - 2026-10-18 — `phpMethod` vs `_Model` method-name collision; manual per-environment `RecordScripts` id in `Client/` grant files. TRUE-82907 (jcardinal)
121
122
  - 2026-09-25 — renamed product references from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
122
123
 
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-27
9
+ updated: 2026-10-28
10
10
  owners: [tcox, bala, apeterson, jcardinal, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -58,6 +58,12 @@ controller.
58
58
  (`V2.php` ~L3673–3682). **Prefer bare for ascending:** the query string is parsed *without*
59
59
  urldecoding (see Encoding rules), so a client that percent-encodes the `+` as `%2B` sends a
60
60
  field literally named `%2Bfield` and the sort is silently dropped. Bare has no such failure mode.
61
+ - **⚠ `sort` orders the MAIN record query only — a nested CHILD set has no sort at all.** The option
62
+ is handled at `V2.php:3836` and builds the one `ORDER BY` for the record query; an inherent child
63
+ collection (e.g. `userDelegatorUsers` on `GET /users/<uuid>`) carries no sort of its own, so MySQL
64
+ returns those rows in no guaranteed order and no query param can change it. **Order child rows in
65
+ the client.** Verified on client-sandbox 2026-10-23: `Users_DelegatorUsers` for one user with no
66
+ `ORDER BY` came back as ids 3, 2, 1.
61
67
  - **Multi-sort = comma list** (`sort=-dateIssued,uuid`; `parseOptionsSort` ~L10888 explodes on `,`;
62
68
  a blox `sort: [...]` array is sent comma-joined). **Offset paging (`page`) is only stable with a
63
69
  unique last key** — add `uuid` as the tie-breaker, or rows with equal sort values shuffle between
@@ -692,6 +698,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
692
698
  [V2 request logging](request-logging.md).
693
699
 
694
700
  ## Change history
701
+ - 2026-10-28 — `sort` orders only the main record query; nested child sets come back unordered, sort them client-side (TRUE-81848). (bala)
695
702
  - 2026-10-27 — `where` unsupported on PUT/PATCH; uuid-less PUT/PATCH with no identifying field now 400 `EV-1` instead of a full-table load + `EV-14` (TRUE-83276). (jcardinal)
696
703
  - 2026-10-24 — blox JS `null` in `where` → `= ''` → 1525 500, send `"null"`; dotted FK path fails in `fields` too; cross-cluster FK → filter client-side (TRUE-82916). (jcardinal)
697
704
  - 2026-10-23 — `depth` re-expands every child collection per row (skip-guard key mismatch) → 256 MB OOM on a 2,530-address contact (TRUE-83138). (bala)
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
9
+ updated: 2026-10-28
10
10
  owners: [tcox, bala]
11
11
  files:
12
12
  - src/hooks/useOrderForDelegation.ts
@@ -53,7 +53,7 @@ TRUE-81851 (pill) + TRUE-81852 (persistence) + the 2026-09-22 token switch + the
53
53
 
54
54
  On picking a person:
55
55
 
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.
56
+ 1. `CartApi.fetchUserTokens(uuid, "/auth/delegator", MINT_ROUTE__ORDER_FOR)` mints the target's pair. The third argument is the **mint route** and has **no default**, so every caller picks one: `MINT_ROUTE__ORDER_FOR = "delegatedUser"` (here) or `MINT_ROUTE__VIEW_AS = "encryptedUser"` ([View As](view-as-session-tokens.md)); it sets both the URL and the response key. Order For must use `delegatedUser` — `encryptedUser` does not run the `Users_DelegatorUsers` check and is Admin-only. It throws on a non-string/empty token and calls `clearPublicToken()`; the caller installs the pair.
57
57
  2. `beginDelegation(pair, ownerUuid)` parks the owner's pair (`src/api/sessionTokens.ts`); `setActiveTokens()` bumps the identity epoch.
58
58
  3. `setDelegation({targetUuid, ownerUuid})` writes the Order For marker.
59
59
  4. Load the target: `fetchUser`, `fetchUserPageSettings`, `fetchUserPersonaData` (query key `userPersonaDataOnOrderFor`), then `setUser`.
@@ -70,6 +70,7 @@ Reads `userDelegatorUsers.delegatorUser.{uuid,_name,email}` off `GET /users/{own
70
70
  - **Call it before the token switch** — on the target's token the owner's own record 403s.
71
71
  - **Never request `id`** — it 403s the whole call. Use `uuid`.
72
72
  - **No paging.** Search filters the list in the browser.
73
+ - **Sort in the browser.** api2 returns child rows in no order at all — the `sort` option orders only the main record query, never a child set like `userDelegatorUsers` (see [V2 REST query contract](../../api2/features/v2-rest-query-contract.md)). `fetchDelegationList` sorts the mapped list with `localeCompare` + `sensitivity: "base"`, falling back to the email when `_name` is blank so blank rows do not clump at the top.
73
74
  - Snapshotted in `useOrderForZu.delegationOptions` so it survives on the target's token.
74
75
  - **"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).
75
76
 
@@ -132,11 +133,11 @@ Zustand `persist`, localStorage `order-for-zu`. Holds the cart's pick `orderForU
132
133
  ## Open items
133
134
 
134
135
  - 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).
136
+ - `SalesOrders.c_orderSubmittedById` is still never set, so the only audit trail is `Logs_<Tenant>.Api.originalUserId`. It belongs in `_Model_Compass_SalesOrder::prePost()` from `$api->delegator->user`, not in the client. See [order-for delegation](../../_underscore/features/order-for-delegation.md).
137
137
 
138
138
  ## Change history
139
139
 
140
+ - 2026-10-28 — Order For mints through its own endpoint: `fetchUserTokens` takes a required mint-route argument (`MINT_ROUTE__ORDER_FOR` = `delegatedUser`, `MINT_ROUTE__VIEW_AS` = `encryptedUser`). Delegation list now sorted by name in the browser, because api2 cannot sort child rows. `originalUserId` confirmed stamping. (bala)
140
141
  - 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
142
  - 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
143
  - 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)
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-21
9
+ updated: 2026-10-28
10
10
  owners: [tcox, bala, apeterson]
11
11
  files:
12
12
  - src/api/sessionTokens.ts
@@ -42,7 +42,7 @@ All token plumbing lives in **`src/api/sessionTokens.ts`** (new). `src/api/apiBa
42
42
 
43
43
  ### Entering view-as (`NavIconList.tsx` onSuccess)
44
44
 
45
- 1. `fetchUserTokens(uuid, "/auth/delegator")` mints the delegated pair for the target user.
45
+ 1. `fetchUserTokens(uuid, "/auth/delegator", MINT_ROUTE__VIEW_AS)` mints the delegated pair for the target user. The third argument is the **mint route**, with no default: `MINT_ROUTE__VIEW_AS = "encryptedUser"` here, `MINT_ROUTE__ORDER_FOR = "delegatedUser"` for [Order For](header-order-for-dropdown.md). `encryptedUser` runs **no** delegation-list check and is restricted to the Admin role by its `AclRecordScripts` grants — so View As can reach anyone in the tenant, and only an admin can call it.
46
46
  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`.
47
47
  3. It **never overwrites an existing parked pair** — view-as is one level deep. `isViewAsEnabled` also hides the button when `hasSavedOriginalTokens()` is true.
48
48
  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("/"))`.
@@ -90,7 +90,7 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
90
90
  - **⚠ 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`.
91
91
  - **Never test "View As is on" with `Object.keys(returnOriginalUser).length > 0`.** Order For parks the same user; use `useIsViewAsActive`.
92
92
  - **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).
93
- - **⚠ 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.
93
+ - **⚠ Deploy `_underscore` and api2 together.** Mixed versions break the mint→exchange handshake with a 403 EZ-1 `Invalid User` on `/auth/delegator`; see [order-for delegation](../../_underscore/features/order-for-delegation.md).
94
94
  - **⚠ View As on a target with no `Users_Roles` row strands the admin.** Every call as the target 403s EZ-1 (`/v2/users/encryptedUser`, `/v2/pages/meta`, `/v2/core-parameters`, bundles), the banner shows the **admin's own** name, return does not work, and clearing cookies does not help. Not a token bug. Diagnose: `Logs_<Tenant>.Api` rows with `responseCode = 403`, `responsePayload.audience.user` = target, app TOGa Commerce; then check `Client_<Tenant>.Users_Roles` for that user id. First case: Compass user 311024, created by the PEOPLE import a day earlier (TRUE-82976). Cause + fix: [PEOPLE-file lifecycle](../../../../clients/compass-usa/features/people-file-user-lifecycle.md#gotchas).
95
95
  - **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.
96
96
 
@@ -98,12 +98,13 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
98
98
 
99
99
  - 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).
100
100
  - `SENTRY_AUTH_TOKEN` is committed in 25 tracked `.env*` files — treat it as compromised and rotate it. (Never store the value in this KB.)
101
- - **`GET /users/encryptedUser` permission gap (found 2026-09-25, deferred).** The RecordScript (`Core.RecordScripts` id 16; `_underscore/Model/Client/User.php` ~line 353, `@TODO` with no check) was granted in prod to ordinary roles across many clients (e.g. Compass Base + Manager), likely from `dbchanges2/Core/HISTORIC/2025/2025-01-13 - 55324 - GetEncryptedUser.sql`. Fix = a real permission check in code, not only pulling grants. The `canUserOrderForOtherUser` gate (Gotchas) covers it on beta / client-sandbox — confirm it is live in prod before closing this item.
101
+ - **`GET /users/encryptedUser` permission gap (found 2026-09-25, partly closed 2026-10-23).** Script 16 has no in-code permission check, so its **ACL grants are the whole control**. Both Compass tenants are now narrowed to Admin, but **31 of 37** prod client databases still grant 16 to role 1 (`Base`) — narrow a tenant before enabling View As there. Grant table + prod audit: [order-for delegation §ACL](../../_underscore/features/order-for-delegation.md).
102
102
  - ~~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).
103
103
  - ~~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.
104
104
 
105
105
  ## Change history
106
106
 
107
+ - 2026-10-28 — View As mints through `encryptedUser` again, now with no delegation-list check and Admin-only ACL grants; `fetchUserTokens` takes a required mint-route argument (`MINT_ROUTE__VIEW_AS`). Order For moved to its own `delegatedUser` endpoint. (bala)
107
108
  - 2026-10-21 — Gotcha: View As on a role-less target 403s everything and strands the admin; how to diagnose (TRUE-82976). (bala)
108
109
  - 2026-10-07 — Open item: `/users/encryptedUser` permission gap found 2026-09-25 (prod grants to ordinary roles); confirm the delegation gate is live in prod. (jcardinal)
109
110
  - 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)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: pcmaticb2b
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-22
9
+ updated: 2026-10-28
10
10
  owners: [snaredla, jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Startech.php
@@ -62,8 +62,7 @@ Both filters use `fields` to request only `id`/`uuid`. Filtering on `ticketTypeI
62
62
 
63
63
  ## Gotchas
64
64
 
65
- - **Race:** `create_ticket` + `status_change` arriving together both miss the lookup and POST; the second fails on duplicate `Tickets.number`. Harmless (first one wins); not fixed (2026-10-22).
66
-
65
+ - **Race (fixed 2026-10-28):** Startech sends `create_ticket` + `status_change` within a second; both jobs miss the lookup and POST, the second fails 400 EV-10 (1062 duplicate `Tickets.number`). The random 3-7s sleep does **not** prevent it. Fix: POST with throw off; on failure re-GET by `c_startechTicketId`; found → PUT the same payload, else throw the original response. Keep this fallback — do not rely on the sleep.
67
66
  - `c_escalateToStartech: 0` must be in the payload. If omitted, the interceptor still skips Startech (null is also falsy), but the field stays NULL in TOGA 2.0 — making webhook tickets indistinguishable from TOGaDesk-created ones.
68
67
  - `ticketType` is resolved via `c_startechTicketTypeId` on `TicketTypes` — not a hardcoded string. Startech type IDs: SR=6, PCS=54, API=201.
69
68
  - **⚠ The nested `contact` sends the Internal customer** (`Contacts.customerId` NOT NULL — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md)). `Tickets.contactId` is `MATCH_UPSERT` and `customerId` is an identifier, so the match uses `c_startechUserId` AND customerId. A contact whose customer is not the Internal one does not match and **gets a DUPLICATE contact** (not an overwrite). Open item (2026-10-16): 3 Pcmaticb2b contacts with a real customer + `c_startechUserId` will be duplicated on their next ticket. Proposed fix, not taken: look up the contact by `c_startechUserId` first and send `{uuid}`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1180",
3
+ "version": "1.0.1182",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",