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.
- package/knowledge/1.0/apps/library/features/startech-pcmaticb2b-sync.md +6 -2
- package/knowledge/2.0/apps/_underscore/features/order-for-delegation.md +85 -34
- package/knowledge/2.0/apps/api2/features/record-scripts.md +4 -3
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +8 -1
- package/knowledge/2.0/apps/toga2-commerce/features/header-order-for-dropdown.md +5 -4
- package/knowledge/2.0/apps/toga2-commerce/features/view-as-session-tokens.md +5 -4
- package/knowledge/2.0/apps/worker2/features/startech-webhook-handler.md +2 -3
- package/package.json +1 -1
|
@@ -6,10 +6,13 @@ project: Library
|
|
|
6
6
|
client: pcmaticb2b
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
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
|
-
- **
|
|
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-
|
|
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/
|
|
17
|
-
- dbchanges2/
|
|
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
|
|
33
|
-
|
|
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.
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
98
|
-
**8** = Compass Base, **9** = Manager
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
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~~
|
|
186
|
-
|
|
187
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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. `
|
|
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-
|
|
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
|
-
- **⚠
|
|
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,
|
|
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-
|
|
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`
|
|
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