toga-ai 1.0.855 → 1.0.857
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/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +38 -2
- package/knowledge/2.0/apps/_underscore/features/order-for-delegation.md +162 -0
- package/knowledge/2.0/apps/api2/features/request-logging.md +7 -1
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +19 -2
- package/knowledge/2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md +8 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md +24 -1
- package/knowledge/2.0/apps/toga2-commerce/features/header-order-for-dropdown.md +16 -5
- package/knowledge/2.0/apps/toga2-commerce/features/view-as-session-tokens.md +7 -3
- package/knowledge/2.0/apps/toga2-commerce/workflows/amplify-build-and-deploy.md +8 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/sessions/2026-09-21-order-for-delegation-bala.md +106 -0
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | Shared 2.0 `_underscore` NetSuite REST client (`_Component_Api_Netsuite`) — record GET/write, SuiteQL, auth, escaping, logging; open before any 2.0 NetSuite cal |
|
|
39
39
|
| [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | Where the shared NetSuite SO importer (`_Trait_Netsuite_SalesOrder`) sources SO#, PO#, and ship-to phone, and the traps in changing it; open before editing this |
|
|
40
40
|
| [Note records have NO author — every `*Notes` model's `contactId` is unwritten](features/note-record-authorship.md) | Every `*Notes` model declares a `contactId` FK but no code writes it — note authorship is unrecoverable from 2.0 data; open before building any note author disp |
|
|
41
|
+
| ["Order For" delegation (Users_DelegatorUsers, the permission gate, originalUserId)](features/order-for-delegation.md) | Server side of "Order For" (TRUE-81848): who may order for whom, where that is enforced, and how the audit column records it. |
|
|
41
42
|
| [Outbound third-party API rate budget (_Component_Api_RateBudget) — designed + staged, NOT merged](features/outbound-api-rate-budget.md) | Design record for a shared outbound API rate budget (one component + one Core table, keyed by `bucketKey`); open when adding call-rate metering/limiting to a 2. |
|
|
42
43
|
| [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | Legacy `Page::meta()` page-meta resolver + context-scoped `ClientRecordFieldSettings`; open when a grid column header is wrong/camelCase, or a `/pages/meta` pag |
|
|
43
44
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | `_underscore` opens three per-client DB connections (data/logs/archive) per request; open when a local or prod request 500s with `Unknown database`, when resolv |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-21
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -53,7 +53,11 @@ AclRecordPermissions → AclLogicGroups → AclLogicGroupExpressions →
|
|
|
53
53
|
> rejected with the exact message **`No ACL Logic Groups defined for ACL Record Permissions`**
|
|
54
54
|
> (~L4437 / ~L5845); a missing expression binding fails on the second flag the same way. So an
|
|
55
55
|
> `AclRecordPermissions` row with no `AclLogicGroups` row returns UNAUTHORIZED carrying that string
|
|
56
|
-
> — which means the **chain**, not the grant, is the problem.
|
|
56
|
+
> — which means the **chain**, not the grant, is the problem. The mechanism is
|
|
57
|
+
> `_Model_Core_AclRecordPermission::buildSqlExpression`, which returns **`'(0)'`** when the
|
|
58
|
+
> permission has no logic group — so `allowRead = 1` on its own **denies everything**. Second
|
|
59
|
+
> symptom shape (2026-09-18): a 403 that lists `aclRecordPermissionIds` with the nested child array
|
|
60
|
+
> coming back as `[null, null, null]`.
|
|
57
61
|
|
|
58
62
|
**Field visibility — `AclFieldPermissions`.** Record-level access alone does **not** expose
|
|
59
63
|
fields. Each field needs a row: `recordFieldId`, `roleId`, `isWritable` (0 = read-only, 1 =
|
|
@@ -503,6 +507,15 @@ path on the strength of them.
|
|
|
503
507
|
> [V2 REST query contract → ACL behavior](../../api2/features/v2-rest-query-contract.md). Confirm
|
|
504
508
|
> the request shape first; it eliminates the entire ACL chain in one step.
|
|
505
509
|
|
|
510
|
+
**⚠ A `CLIENT` record needs the grant in EVERY `Client_*` DB — a Core row for it is dead metadata.**
|
|
511
|
+
Every ACL lookup in `api2/Component/Api/V2/V2.php` (L3236, L4057, L6502, L7307) switches on the
|
|
512
|
+
record's `aclDatabase` and reads only the matching bucket, so `AclRecordPermissions` /
|
|
513
|
+
`AclRecordExpressions` / `AclFieldPermissions` written into `Core` for a `CLIENT` record are **never
|
|
514
|
+
evaluated**. Found 2026-09-18 on record **362** (`user-delegator-users`): role 3 (API) has a Core
|
|
515
|
+
grant and none in any client DB, so no integration can reach the route. For every other client
|
|
516
|
+
record the role-3 grant is mirrored in **both** places (268 rows in `Core`, 275 in
|
|
517
|
+
`Client_Compass`) — mirror it, or the API role silently has nothing.
|
|
518
|
+
|
|
506
519
|
**Role ids differ per client DB**, so never hardcode a `roleId` in a `CLIENT` ACL migration —
|
|
507
520
|
resolve it with a subselect, e.g. the Base role:
|
|
508
521
|
|
|
@@ -510,6 +523,21 @@ resolve it with a subselect, e.g. the Base role:
|
|
|
510
523
|
roleId = (SELECT id FROM Roles WHERE `name` = 'Base')
|
|
511
524
|
```
|
|
512
525
|
|
|
526
|
+
### ⚠ On a real top-level route, `sqlExpression = '1'` leaks every tenant row
|
|
527
|
+
|
|
528
|
+
`'1'` (slug `all`) is right for a record only ever read nested under its parent. If the record also
|
|
529
|
+
has its own route, `'1'` lets any user `GET /v2/<route>` and read everyone else's rows. Scope the
|
|
530
|
+
expression to the caller instead — `AclUser` is joined by `_Model_True::isPermitted` on the
|
|
531
|
+
logged-in user:
|
|
532
|
+
|
|
533
|
+
```sql
|
|
534
|
+
CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Caught in security review on `user-delegator-users` (2026-09-18), where a `'1'` grant would have
|
|
538
|
+
listed who every colleague may order for. See
|
|
539
|
+
[order-for delegation](./order-for-delegation.md).
|
|
540
|
+
|
|
513
541
|
## Canonical example (grant the Base role full CRUD on a record)
|
|
514
542
|
|
|
515
543
|
```sql
|
|
@@ -893,6 +921,14 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
893
921
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
894
922
|
|
|
895
923
|
## Change history
|
|
924
|
+
- 2026-09-21 — Named the mechanism behind the half-built chain: `_Model_Core_AclRecordPermission::buildSqlExpression`
|
|
925
|
+
returns **`'(0)'`** with no logic group, so `allowRead = 1` alone denies everything (second symptom: 403 listing
|
|
926
|
+
`aclRecordPermissionIds` with `[null, null, null]`). Added that a `CLIENT` record's grant must exist in every
|
|
927
|
+
`Client_*` DB — the `aclDatabase` switch at `V2.php` L3236/L4057/L6502/L7307 makes a Core row dead metadata;
|
|
928
|
+
record 362 (`user-delegator-users`) role 3 is Core-only and therefore unreachable, while every other client record
|
|
929
|
+
mirrors role 3 in both. Also recorded the caller-scoped row expression pattern
|
|
930
|
+
(`CASE WHEN <Table>.userId = AclUser.id THEN 1 ELSE 0 END`) required when a record is a real top-level route.
|
|
931
|
+
(bala)
|
|
896
932
|
- 2026-09-18 — **The `2026-09-17a` fleet migration has a gap: it granted only the PO-first bridges
|
|
897
933
|
325/329, leaving the TO-first siblings 326 (`transfer-orders-purchase-orders`) and 330
|
|
898
934
|
(`transfer-order-items-purchase-order-items`) with ZERO `AclRecordPermissions` rows.** Found on
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Order For" delegation (Users_DelegatorUsers, the permission gate, originalUserId)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-21
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/Users/DelegatorUser.php
|
|
13
|
+
- _underscore/Model/Client/User.php
|
|
14
|
+
- _underscore/Model/Client/Logs/Api.php
|
|
15
|
+
- api2/Component/Api/V2/V2.php
|
|
16
|
+
- dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql
|
|
17
|
+
- dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql
|
|
18
|
+
related:
|
|
19
|
+
- ../../api2/features/request-logging.md
|
|
20
|
+
- ./acl-permission-chain.md
|
|
21
|
+
- ../../toga2-commerce/features/header-order-for-dropdown.md
|
|
22
|
+
- ../../toga2-commerce/features/view-as-session-tokens.md
|
|
23
|
+
- ../../api2/features/encrypted-user-uuid-auth-handoff.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Server side of "Order For" (TRUE-81848): who may order for whom, where that is enforced, and how the audit column records it. Open before touching `/auth/delegator`, `Users_DelegatorUsers`, or `Logs_<Client>.Api.originalUserId`. Team skill: `/order-for`.
|
|
27
|
+
|
|
28
|
+
## Summary
|
|
29
|
+
|
|
30
|
+
A user may place an order **as** another user only when a `Users_DelegatorUsers` row says so.
|
|
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).
|
|
34
|
+
|
|
35
|
+
**The table ships EMPTY** — no admin screen, no import. Until rows exist every delegation is
|
|
36
|
+
refused and every delegation dropdown is empty. That is intended, not a bug.
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
### 1. `Users_DelegatorUsers` (every `Client_*` DB)
|
|
41
|
+
|
|
42
|
+
`id`, `uuid`, `userId`, `delegatorUserId`, UNIQUE on the pair. Read as **"`userId` may order for
|
|
43
|
+
`delegatorUserId`"**. Model `_Model_Client_Users_DelegatorUser`.
|
|
44
|
+
|
|
45
|
+
Core metadata (`Core/2026-09-18a`): record **362**, route `user-delegator-users`,
|
|
46
|
+
`aclDatabase = CLIENT`, RecordFields **2604** id / **2605** uuid / **2606** userId /
|
|
47
|
+
**2607** delegatorUserId, registered as an **inherent child of record 2 (Users)** so
|
|
48
|
+
`userDelegatorUsers` is readable nested under `/users/{uuid}`.
|
|
49
|
+
|
|
50
|
+
### 2. The gate — checked TWICE
|
|
51
|
+
|
|
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`.
|
|
55
|
+
|
|
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.
|
|
63
|
+
|
|
64
|
+
`V2.php` sets `$isDelegatorAuthRequest = true` as the **first statement** of `case 'delegator'`,
|
|
65
|
+
which then falls through into `case 'encrypted'`; the flag is declared `false` (L609) so a forged or
|
|
66
|
+
expired bearer cannot mint. **Fail-closed is deliberate** — a fail-open default was caught in
|
|
67
|
+
security review before it shipped.
|
|
68
|
+
|
|
69
|
+
### 3. `originalUserId` audit column
|
|
70
|
+
|
|
71
|
+
`Logs_<Client>.Api.originalUserId` — nullable int, directly after `userId`
|
|
72
|
+
(`Logs_Client/2026-09-18a`), on `_Model_Client_Logs_Api`. `V2.php` fills it at all three client
|
|
73
|
+
log-write sites plus the CloudWatch entry.
|
|
74
|
+
|
|
75
|
+
- `userId` = who the token is for · `originalUserId` = who actually signed in · **NULL = no
|
|
76
|
+
delegation**. No row should ever have `originalUserId = userId`.
|
|
77
|
+
- Verified on local order SA136321: `SalesOrders.createdByUserId` = 124256 (Dawn Anderson), log row
|
|
78
|
+
`userId 124256 / originalUserId 127046` (Dev Team). Last 200 rows: 126 own-token (NULL) + 70
|
|
79
|
+
delegated, zero self-referencing.
|
|
80
|
+
|
|
81
|
+
### 4. ACL — read-only, and scoped to the CALLER
|
|
82
|
+
|
|
83
|
+
Per-tenant files `Client_Compass/2026-09-18a` and `Client_CompassCanada/2026-09-18a`, `appId 5`
|
|
84
|
+
(TOGa Commerce). **Read only, on purpose:** a role that can write its own row here self-grants the
|
|
85
|
+
right to order for anyone in the tenant.
|
|
86
|
+
|
|
87
|
+
The row expression is scoped to the caller, never `'1'`:
|
|
88
|
+
|
|
89
|
+
```sql
|
|
90
|
+
CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`user-delegator-users` is a real top-level route, so `'1'` would let any user
|
|
94
|
+
`GET /v2/user-delegator-users` and list who every colleague may order for. `AclUser` is joined by
|
|
95
|
+
`_Model_True::isPermitted` on the logged-in user. Chain rules: [ACL permission chain](./acl-permission-chain.md).
|
|
96
|
+
|
|
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.
|
|
101
|
+
|
|
102
|
+
**Dev-sandbox needs its own files** (`2026-09-21a … DevSandbox.sql`) because the pinned field ids
|
|
103
|
+
collide there — field ids **2767/2768/2769/2770**, record 362 unchanged. See
|
|
104
|
+
[non-prod metadata drift](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
105
|
+
|
|
106
|
+
### 5. The front-end call that exists
|
|
107
|
+
|
|
108
|
+
`toga2-commerce/src/pages/Cart/api/CartApi.ts` → `fetchDelegationList(ownUuid)` returns
|
|
109
|
+
`DelegationOption[]` (`{uuid, name, email}`) — everyone that user may order for, one call, no
|
|
110
|
+
paging. Two hard rules in
|
|
111
|
+
[header order-for dropdown](../../toga2-commerce/features/header-order-for-dropdown.md).
|
|
112
|
+
|
|
113
|
+
## Gotchas
|
|
114
|
+
|
|
115
|
+
- **The table is empty in every environment.** "Delegation refused / dropdown empty" is the
|
|
116
|
+
expected state until someone inserts rows; there is no UI or import to do it.
|
|
117
|
+
- **Never let the `isDelegatorAuthRequest` flag default to true** — the encrypted pair is
|
|
118
|
+
unauthenticated, replayable input.
|
|
119
|
+
- **Outbound logs never carry `userId` or `originalUserId`.** `_underscore/ApiRequest.php` and
|
|
120
|
+
`SoapRequest.php` write `direction = OUT` rows for calls TOGa makes to NetSuite/UPS — there is no
|
|
121
|
+
signed-in user on those. A CodeRabbit review asking for the stamp there was wrong and rejected.
|
|
122
|
+
- **Role 3 (API) cannot reach this record.** Its grant for record 362 was written into `Core`, but
|
|
123
|
+
`aclDatabase = CLIENT` means Core rows are never consulted — so no integration can read or write
|
|
124
|
+
`user-delegator-users` through api2 until the grant is mirrored into each `Client_*` DB.
|
|
125
|
+
|
|
126
|
+
## Still open (next person: do not rediscover)
|
|
127
|
+
|
|
128
|
+
1. **The front end does not switch the token, so the feature is not real yet.** The header dropdown
|
|
129
|
+
only saves the pick to localStorage and passes it as a cart form value; it never calls
|
|
130
|
+
`/auth/delegator` (only admin View As does, in `NavIconList.tsx`). With that, `$api->delegator` is
|
|
131
|
+
never set, `originalUserId` stays NULL, the gate never fires, and the buyer browses their **own**
|
|
132
|
+
catalog and prices while "ordering for" someone else. Required switch: `GET
|
|
133
|
+
/users/encryptedUser?uuid=<target>` → `POST /auth/delegator` → `beginDelegation(tokens,
|
|
134
|
+
originalUserUuid)` (`src/api/sessionTokens.ts`); `restoreOriginalTokens()` returns with no new
|
|
135
|
+
login. **One parked-token slot, shared with View As** — the two cannot nest.
|
|
136
|
+
2. **`c_orderSubmittedById` must be stamped server side.** The column exists on `SalesOrders` in
|
|
137
|
+
`Client_Compass`, `Client_CompassCanada`, `Client_Quad`, with `CustomRecordFields` id 40 on record
|
|
138
|
+
14 and `_Model_Compass_SalesOrder` L51 as `FIELD_INTEGER`. It is a plain integer, not an FK, so
|
|
139
|
+
api2 will not resolve a `{uuid}` into it and the client cannot set it. Belongs in
|
|
140
|
+
`_Model_Compass_SalesOrder::prePost()` (L445; `ApiPayloadInterceptor` 24 is already PRE on POST
|
|
141
|
+
for record 14), read from `$api->delegator->user`, NULL when there is no delegation — matching
|
|
142
|
+
1.0 `store/common/compass/convertcarttoorder.php` L28-34. **Reading old data:** of 112,096
|
|
143
|
+
Compass orders 8,118 have the field set and 7,174 of those equal `createdByUserId`, because the
|
|
144
|
+
1.0 guard compared a manager id against a contact id.
|
|
145
|
+
3. **`/auth/encrypted` still accepts a delegation pair with no bearer and records no delegator.**
|
|
146
|
+
Narrowed because Commerce no longer calls it; `toga2-desk` still does for client switching.
|
|
147
|
+
|
|
148
|
+
## Change history
|
|
149
|
+
|
|
150
|
+
- 2026-09-21 — Created (TRUE-81848). `Users_DelegatorUsers` + record 362, the two-point
|
|
151
|
+
`canUserOrderForOtherUser` gate, `Logs_<Client>.Api.originalUserId`, caller-scoped read-only ACL
|
|
152
|
+
for Compass US/Canada (+ dev-sandbox variants), and `fetchDelegationList`. Table ships empty; the
|
|
153
|
+
token switch, `c_orderSubmittedById` and the `/auth/encrypted` hole are still open. Team skill
|
|
154
|
+
`/order-for` (commit d4751825) loads this context. (bala)
|
|
155
|
+
|
|
156
|
+
## Related
|
|
157
|
+
|
|
158
|
+
- [request logging](../../api2/features/request-logging.md)
|
|
159
|
+
- [ACL permission chain](./acl-permission-chain.md)
|
|
160
|
+
- [header order-for dropdown](../../toga2-commerce/features/header-order-for-dropdown.md)
|
|
161
|
+
- [view-as session tokens](../../toga2-commerce/features/view-as-session-tokens.md)
|
|
162
|
+
- [encrypted-user auth handoff](../../api2/features/encrypted-user-uuid-auth-handoff.md)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-21
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -21,6 +21,7 @@ related:
|
|
|
21
21
|
- ./v2-deadlock-retry.md
|
|
22
22
|
- ./environment-variable-drives-underscore-branch.md
|
|
23
23
|
- ../../_underscore/features/error-reporting-issue-event.md
|
|
24
|
+
- ../../_underscore/features/order-for-delegation.md
|
|
24
25
|
- ../../../../clients/aig/features/entitlement-intake.md
|
|
25
26
|
- ../../../../clients/prudential/features/service-request-auth-rejection-alert.md
|
|
26
27
|
- ../../../../clients/compass-usa/features/mits-po-to-so-item-linking.md
|
|
@@ -39,6 +40,7 @@ In `api2/Component/Api/V2/V2.php` (~L2168) every request is logged and routed:
|
|
|
39
40
|
- **A successful, client-scoped read (GET)** → the **CLIENT log DB** via `_Model_Client_Logs_Api` → table `Logs_<Client>.Api` (e.g. `Logs_Aig.Api`).
|
|
40
41
|
- **A request fails *before* client scoping** (bad/expired auth, unknown client — e.g. `EN-1` / `EN-4`) → the **CORE log DB** via `_Model_Core_Logs_Api` → the core `Logs` DB (older docs call this `Logs_2`). Pre-scope failures land in **Core**, **not** the client log.
|
|
41
42
|
- **A FAILED write attempt (non-2xx on a write) lands in the CORE/writer `Logs`, even when the request authenticated to a client.** The subtle one: the request can carry a valid client JWT (fully scoped to, say, `Aig`) and still have its non-2xx `POST`/`PUT` logged to the production **core `Logs`** on the **writer cluster** (host `api-writer.togahub.com`), **not** to `Logs_<Client>`. So `Logs_<Client>.Api` effectively shows the client's **reads**; **failed writes are in core `Logs`.** (A *successful* write is logged to the client log as normal.)
|
|
43
|
+
- **`Logs_<Client>.Api.originalUserId`** (added 2026-09-18) records **who actually signed in** when the token was minted for someone else: `userId` = the token's user, `originalUserId` = the real person, **NULL = no delegation** (no row should ever have the two equal). `V2.php` fills it at all three client log-write sites plus the CloudWatch entry. **Outbound rows (`direction = OUT`, written by `_underscore/ApiRequest.php` / `SoapRequest.php`) never set either column** — there is no signed-in user on a call TOGa makes out to NetSuite or UPS. See [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
42
44
|
- There is also a **config-gated branch** (~L2166): when `_Config::api('log_filepath')` is set, the entry is written to a **file** on the host instead of the DB.
|
|
43
45
|
|
|
44
46
|
**401 lands in base `Logs`; 403 lands in `Logs_<Client>`.** The pre-scope/post-scope split has a sharp edge on the two auth codes:
|
|
@@ -207,6 +209,10 @@ Both a **fallback recovery path** and a **scope diagnostic**. In the TRUE-80179
|
|
|
207
209
|
- **Query prod `Logs.Api` by narrowing on `dtStamp` (indexed) first**, collect ids, then fetch `mediumtext` payloads by id with `LEFT()`/`SUBSTRING()`. Never start from a `LIKE` over `requestPayload`/`responsePayload`.
|
|
208
210
|
- **Never copy logged rows into committed artifacts** — they carry live Bearer tokens and PII.
|
|
209
211
|
|
|
212
|
+
## Change history
|
|
213
|
+
|
|
214
|
+
- 2026-09-21 — Added `Logs_<Client>.Api.originalUserId` (delegation audit: NULL = none, never equal to `userId`) and the rule that outbound `direction = OUT` rows stamp neither user column. (bala)
|
|
215
|
+
|
|
210
216
|
## Related
|
|
211
217
|
|
|
212
218
|
- [api2 architecture](../architecture.md)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-21
|
|
10
10
|
owners: [bala, kyalamarthi, apeterson, ajean, jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
@@ -87,7 +87,16 @@ DELETE FROM AclFieldPermissions WHERE uuid IN ('<literal-1>', …, '<literal-8>'
|
|
|
87
87
|
|
|
88
88
|
It can only ever match rows this file inserted. **Reusing the same literal across tenants is valid**
|
|
89
89
|
— `uuid` is unique *per table per database*, and each tenant is a separate database, so one literal
|
|
90
|
-
per (row × file) is enough for the whole fan-out.
|
|
90
|
+
per (row × file) is enough for the whole fan-out.
|
|
91
|
+
|
|
92
|
+
**⚠ But do NOT reuse a pinned uuid across two files whose rows have DIFFERENT natural keys**
|
|
93
|
+
(e.g. a prod file and its `…DevSandbox.sql` twin, 2026-09-21). `AclFieldPermissions` rows are
|
|
94
|
+
guarded on `recordFieldId`, which differs per environment, so the `NOT EXISTS` guard lets the
|
|
95
|
+
insert through and the UNIQUE `uuid` key then fails: *"Duplicate entry … for key
|
|
96
|
+
`aclfieldpermissions.uuid`"*. Rule of thumb: **same natural key across environments → keep the same
|
|
97
|
+
uuid** (a row then means the same thing everywhere — `RecordFields` keyed on `recordId+field`,
|
|
98
|
+
record permissions keyed on `recordId+roleId`); **environment-specific natural key → its own
|
|
99
|
+
uuid.** Keep the `NOT EXISTS` guard on the real unique key
|
|
91
100
|
(here `(recordFieldId, roleId)`) so re-running is still a no-op on tenants that already have the
|
|
92
101
|
grant; the literal governs only deletion.
|
|
93
102
|
|
|
@@ -278,6 +287,10 @@ it. Two cheap local checks catch almost everything, and neither needs a sandbox:
|
|
|
278
287
|
2. **Prove re-runnability by concatenating the file to itself** and running *both copies inside one
|
|
279
288
|
transaction*. If the second pass errors — or changes anything the first pass did not — the file is
|
|
280
289
|
not idempotent, and the executor will eventually run it twice.
|
|
290
|
+
**First DELETE the rows to reproduce the TARGET environment's starting state**, inside the same
|
|
291
|
+
transaction, then run the file twice, `SELECT` the resulting counts, and `ROLLBACK`. Rehearsing
|
|
292
|
+
against your own already-seeded local DB proves nothing — every guard short-circuits. This is
|
|
293
|
+
what caught the duplicate-uuid failure above, which eyeballing missed.
|
|
281
294
|
|
|
282
295
|
3. **When the migration must not change behaviour, simulate the OLD rules and the NEW rules inside
|
|
283
296
|
ONE database** and diff the results per population. **Never diff prod against a sandbox clone** —
|
|
@@ -353,6 +366,10 @@ concluding a migration is production-safe.
|
|
|
353
366
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
354
367
|
|
|
355
368
|
## Change history
|
|
369
|
+
- 2026-09-21 — Narrowed the "reuse one uuid literal across tenants" rule: it holds only when the rows share a
|
|
370
|
+
natural key. An env-specific twin file (guarded on `recordFieldId`, which differs per environment) reusing the
|
|
371
|
+
literal dies on `Duplicate entry … aclfieldpermissions.uuid`. Also added the DELETE-first transaction rehearsal
|
|
372
|
+
(reproduce the target env's starting state, run twice, count, rollback), which is what found it. (bala)
|
|
356
373
|
- 2026-09-17 — Three additions from the Compass persona refactor: a **subtractive** phase cannot
|
|
357
374
|
`NOT EXISTS` against the table it deletes from (**MySQL 1093**) — use a multi-table `DELETE` with a
|
|
358
375
|
self `LEFT JOIN … IS NULL`; comparing a varchar `number` column to a string user-variable can raise
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-21
|
|
10
10
|
owners: ["bala", "mhammontree", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Growrk/2026-05-28.sql
|
|
@@ -164,12 +164,19 @@ This is the write-side twin of the read-side rule above (*"the field named in th
|
|
|
164
164
|
|
|
165
165
|
## Gotchas
|
|
166
166
|
|
|
167
|
+
- **A LOCAL checkout that is many `dbchanges` behind looks exactly like a schema gap — it is not.** Every error in a 2026-09-21 local Compass session was an **existing file never applied** (`Client/2026-09-11a` → `SalesOrderItems.fulfillmentType`, the real cause of an order-submit 400 *"Unknown column 'fulfillmentType'"*; the `Logs_Client` blank-DB `Email`/`EmailAttachment` tables, cause of a 500 *"Table 'logs_compass.email' doesn't exist"*; the `Logs` blank-DB `Issue`/`Event` tables; `Client/2026-08-28b`, `2026-08-28f`, `2026-09-01b`, `2026-09-01c`, `2026-09-14a`, `_modules/netsuite/2026-09-11a`). **Grep `dbchanges2` for the column or table name before writing any SQL.**
|
|
168
|
+
- **Diff a whole environment against prod in ONE pass** — missing tables *and* missing columns together — by comparing `SELECT CONCAT(TABLE_NAME, ':', COUNT(*)) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '<db>' GROUP BY TABLE_NAME` on both sides and `comm`-ing the sorted lists. Step 3's per-table diff is for one known traversal path; this is the catch-up sweep.
|
|
169
|
+
|
|
167
170
|
- **Partial means arbitrary — never assume all-or-nothing.** The observed run landed **6 of 7** columns and **1 of 6** metadata rows from one file. "The migration ran" is not a fact about any individual statement in it.
|
|
168
171
|
- **A `CustomRecordFields` row with no ACL grant is not automatically a defect.** Compare with the healthy client before "fixing" it — `Client_Growrk` recordId 312 (`c_netsuiteInternalSalesOrderId`, `TransferOrders`) has no grant, and `Client_Aig` is identical, so that is the normal cross-client state. Repairing it would be a change, not a fix.
|
|
169
172
|
- **A client can be fine for months and then break on onboarding.** Before a client joins the module, its records resolve to the plain `_Model_Client_*` class with none of the `c_` fields, so the missing columns are unreachable. The 500s begin the day the client is switched onto the module-specific model — which is why the "it has always worked" evidence is misleading. Prove it with `Logs.Api`: successful 200s for the exact same request shape right up to the onboarding timestamp, 500s immediately after.
|
|
170
173
|
- **`ALTER TABLE … ADD COLUMN` was INSTANT here** (MySQL 8.0.39, ~5.4k rows) — a small tenant table needs no maintenance window, but confirm the server version and row count rather than assuming.
|
|
171
174
|
- **Re-running the whole module change-set is the wrong repair** when only one environment drifted: the other statements are already applied and some are not idempotent. Write a scoped catch-up file instead.
|
|
172
175
|
|
|
176
|
+
## Change history
|
|
177
|
+
|
|
178
|
+
- 2026-09-21 — Added the local-checkout case (behind on `dbchanges` reads as a schema gap; the fix is always an unapplied existing file — grep first) and the one-pass whole-schema `information_schema` column-count diff. (bala)
|
|
179
|
+
|
|
173
180
|
## Related
|
|
174
181
|
|
|
175
182
|
- [dbchanges2 architecture](../architecture.md)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-21
|
|
10
10
|
owners: ["mhammontree", "bala", "kyalamarthi", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
|
|
@@ -133,6 +133,23 @@ Verified 2026-08-17 on beta `Client_Aig`: **`_modules/netsuite/2026-07-10a - Uni
|
|
|
133
133
|
|
|
134
134
|
**5. Related observation, same escalation.** `Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql` and `Client/2026-07-16b - TrackingNumberSignatureTypeFieldPermission.sql` resolve their `recordFieldId` via `SET @fieldId = (SELECT … Core.RecordFields …)` before each INSERT block, with later blocks' `SET`s positioned **after** earlier INSERTs. Two factual consequences observed: (a) reading `Core.RecordFields` from a `Client/` file is itself a **Database-isolation violation** per [architecture.md](../architecture.md); (b) when the file is run in chunks / per client selection, the later block's variables are unset, `WHERE sibling.recordFieldId = NULL` matches nothing, and **0 rows insert with no error** — this silently no-op'd the `measures.measureType` read grant **twice** (first in production, then again in dev-sandbox), each time presenting as "empty unit dropdowns". The architecture-consistent remedy is to **hardcode the `recordFieldId` literal**, which removes the `Core` lookup entirely and with it the half-execution failure mode — but that is exactly the practice blocked by the id-divergence hazard in item 2 above, which is why both go to jcardinal together.
|
|
135
135
|
|
|
136
|
+
**6. Confirmed again 2026-09-21 — and the failure is WORSE than a duplicate-key error: it truncates
|
|
137
|
+
the file.** `Core/2026-09-18a - UserDelegatorUserRecords.sql` pins `RecordFields` **2604-2607**; on
|
|
138
|
+
dev-sandbox those four ids already belong to record 344 (`ItemCategoryFeatureGroupTranslations`).
|
|
139
|
+
The file dies on its **first** `RecordFields` insert and **every statement after that point never
|
|
140
|
+
runs** — dev-sandbox was left with `Records` 362 present, **zero** `RecordFields`, no
|
|
141
|
+
`InherentRecordChildren` row and no ACL, i.e. a half-registered record that looks provisioned.
|
|
142
|
+
Only the field ids clashed; `Records.id` 362 was free in both.
|
|
143
|
+
|
|
144
|
+
**Working practice until the escalation is answered:** read `MAX(id)` on the **target** environment
|
|
145
|
+
before pinning, and ship an environment-specific twin file when they differ — here
|
|
146
|
+
`Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql` with ids **2767-2770**, plus matching
|
|
147
|
+
`Client_Compass` / `Client_CompassCanada` ACL twins. Give the twin its **own** uuid literals for any
|
|
148
|
+
row whose natural key is environment-specific (see
|
|
149
|
+
[re-runnable additive migrations](../features/rerunnable-additive-inserts.md)). After running a
|
|
150
|
+
pinned-id `Core/` file anywhere, **verify the tail of the file landed**, not just its first
|
|
151
|
+
statement.
|
|
152
|
+
|
|
136
153
|
### Known remaining drift (dev-sandbox, as of 2026-08-03)
|
|
137
154
|
|
|
138
155
|
- `item-fulfillments.returnAddressId` — the **column exists** but there is **no Core RecordField**. Only needed for the return-label flow.
|
|
@@ -154,10 +171,16 @@ All three missing columns are merged in `dbchanges2` `_main`. Note the shape dif
|
|
|
154
171
|
|
|
155
172
|
## Gotchas
|
|
156
173
|
|
|
174
|
+
- **A pinned-id `Core/` metadata file that hits a duplicate key STOPS THERE** — the record row can exist while its `RecordFields`, `InherentRecordChildren` and ACL silently never ran. Check the last statement's rows, not the first.
|
|
175
|
+
|
|
157
176
|
- **NO carrier can be tested on ANY non-production environment.** Beta runs `[_underscore] debug_mode = 1`, which routes UPS to `wwwcie.ups.com` (CIE/test), and its `[fedex] oauth` points at `https://apis-sandbox.fedex.com` (production uses `https://apis.fedex.com`). But the carrier **account numbers** in non-prod data are copied from production, and neither test environment accepts a production account — UPS returns **"Missing or invalid shipper number" (120100)** and FedEx returns **HTTP 400**. **Label generation can only be verified in production** until CIE/sandbox-valid test accounts are seeded into non-prod `ShippingCarrierAccountNumbers`. (Remedy = obtain those test accounts; documented as the fix, not done.)
|
|
158
177
|
- **⚠ Beta and production point at the SAME NetSuite account (1095849).** Any beta fulfillment writes to **production** NetSuite. Treat a beta fulfill run as a production side effect.
|
|
159
178
|
- **In dev-sandbox, `Core` and `Client_*` share one cluster**, so a `Core.`-qualified reference in a `Client/` migration resolves there and looks fine. **Production splits them** (prod-core / prod-client) — a clean dev-sandbox run is never proof a migration is cluster-safe (see the architecture doc's hard isolation rule).
|
|
160
179
|
|
|
180
|
+
## Change history
|
|
181
|
+
|
|
182
|
+
- 2026-09-21 — Second confirmed case of the pinned `Core.RecordFields` id divergence (2604-2607 belong to record 344 on dev-sandbox) and the worse consequence: the duplicate key **aborts the rest of the file**, leaving a half-registered record. Added the environment-specific twin-file practice (`…DevSandbox.sql`, ids 2767-2770) used for `user-delegator-users`. (bala)
|
|
183
|
+
|
|
161
184
|
## Related
|
|
162
185
|
|
|
163
186
|
- [dbchanges2 architecture](../architecture.md)
|
|
@@ -6,8 +6,8 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [tcox]
|
|
9
|
+
updated: 2026-09-21
|
|
10
|
+
owners: [tcox, bala]
|
|
11
11
|
files:
|
|
12
12
|
- src/components/OrderForDropdown/OrderForDropdown.tsx
|
|
13
13
|
- src/components/OrderForDropdown/useOrderForDropdownViewModel.ts
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
- src/pages/Cart/api/CartApi.ts
|
|
19
19
|
- src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/ADMIN/HEADERFIELDS.json
|
|
20
20
|
- src/pages/Cart/viewModel/FIELDS/COMPASS/ENGLISH/ADMIN/CARTPAGE.ts
|
|
21
|
+
- src/pages/Cart/types.ts
|
|
21
22
|
- cypress/e2e/homePage/orderForDropdown.cy.ts
|
|
22
23
|
related:
|
|
23
24
|
- 2.0/apps/toga2-commerce/architecture.md
|
|
@@ -25,6 +26,7 @@ related:
|
|
|
25
26
|
- 2.0/apps/toga2-commerce/features/cart-notification-emails.md
|
|
26
27
|
- 2.0/apps/toga2-commerce/features/cart-page-config-architecture.md
|
|
27
28
|
- 2.0/apps/toga2-commerce/features/view-as-session-tokens.md
|
|
29
|
+
- 2.0/apps/_underscore/features/order-for-delegation.md
|
|
28
30
|
- 2.0/apps/toga2-commerce/workflows/cypress-testing.md
|
|
29
31
|
---
|
|
30
32
|
|
|
@@ -49,11 +51,17 @@ MVVM split: `OrderForDropdown.tsx` (view), `useOrderForDropdownViewModel.ts` (st
|
|
|
49
51
|
- Menu: search box → yourself first as **"<Name> (Myself)"** → the user list → **Load more** past 100. Check mark on the current pick. `aria` listbox/option roles, Escape closes and returns focus to the trigger, outside click closes.
|
|
50
52
|
- Copy keys under `orderFor`: `isEnabled`, `label`, `searchPlaceholder`, `myselfLabel`, `loadingMessage`, `noOptionsFoundMessage`, `loadMoreLabel` — in all four COMPASS/ENGLISH role files.
|
|
51
53
|
|
|
52
|
-
### 2.
|
|
54
|
+
### 2. The delegation list — `fetchDelegationList`, plus the legacy `GET /users` list
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
**Corrected 2026-09-21: a delegation table and API now exist** (TRUE-81848). The 2026-09-17 note here — "no delegation endpoint or table exists" — was true then and is **false now**: `Users_DelegatorUsers` + route `user-delegator-users` ship in every `Client_*` DB, and `/auth/delegator` now enforces the delegation, so it is no longer only the view-as token. Server side: [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
`fetchDelegationList(ownUuid)` in `src/pages/Cart/api/CartApi.ts` returns `DelegationOption[]` (`{uuid, name, email}`, `src/pages/Cart/types.ts`) — everyone that user may order for, one call, no paging. Three rules:
|
|
59
|
+
|
|
60
|
+
- **Call it BEFORE any token switch.** Once the token belongs to the other person, the signed-in user's own record reads as a **403**.
|
|
61
|
+
- **Never request `id` in the fields** — asking for `id` makes the whole GET 403 for these roles. Use `uuid`.
|
|
62
|
+
- **It returns an empty list today** — `Users_DelegatorUsers` ships empty, with no admin screen and no import.
|
|
63
|
+
|
|
64
|
+
The list the pill still renders is the same `GET /users` the cart uses, via `getUserOptions()` / `getSearchValuesFromAdvancedInput()` in `src/pages/Cart/api/CartApi.ts`:
|
|
57
65
|
|
|
58
66
|
- **Admins** get all active users; server-side search when `CARTPAGE settings.isSearchAnyoneEnabled`.
|
|
59
67
|
- **Supervisors** get their direct reports through the `Users@supervisor` join, filtered locally.
|
|
@@ -88,6 +96,7 @@ Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name`, `
|
|
|
88
96
|
|
|
89
97
|
## Gotchas
|
|
90
98
|
|
|
99
|
+
- **⚠ The pick alone does NOT order for anyone — the front end must switch the token.** Today the pill only stores the pick and passes it as a cart form value; nothing calls `/auth/delegator`. So `$api->delegator` is never set, `originalUserId` stays NULL, the server-side delegation gate never fires, and the buyer browses their **own** catalog and prices while "ordering for" someone else. Required switch: `GET /users/encryptedUser?uuid=<target>` → `POST /auth/delegator` → `beginDelegation(tokens, originalUserUuid)` from `src/api/sessionTokens.ts`; `restoreOriginalTokens()` returns with no new login. **One parked-token slot, shared with View As** — Order For and View As cannot nest. Owned by another developer; not in TRUE-81848.
|
|
91
100
|
- **The `ownerUuid` guard is what makes persistence safe — never drop it.** A persisted pick with no owner check is inherited by the next login or by a view-as target and silently orders for the wrong person. Read the store through `getOrderForUserFor(pick, ownerUuid)`, never raw.
|
|
92
101
|
- **Do not set `useSelectedUserZu` from the header.** The cascade reads the *previous* selected user out of the store (see [cart-notification-emails](cart-notification-emails.md)); writing it early makes the previous-user email cleanup a no-op and strands the old person's notification emails on the order.
|
|
93
102
|
- **A missing `orderFor` key renders nothing, by design.** `useAssignClientFields` has no fallback for a missing key, so Compass Canada and Quad get `undefined` and the component returns `null` — safe default-off. Adding a tenant means adding the key to every role file for that tenant.
|
|
@@ -97,6 +106,7 @@ Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name`, `
|
|
|
97
106
|
|
|
98
107
|
## Change history
|
|
99
108
|
|
|
109
|
+
- 2026-09-21 — Corrected the "no delegation API exists" finding: `Users_DelegatorUsers`, route `user-delegator-users` and the enforced `/auth/delegator` now exist (TRUE-81848). Added `fetchDelegationList` + `DelegationOption` (call before any token switch; never request `id`), and recorded that without the token switch the pick changes nothing server side. (bala)
|
|
100
110
|
- 2026-09-17 — Pick is now persisted to `localStorage["order-for-zu"]` with an `ownerUuid` guard (TRUE-81852); the submit / edit-order-exit / view-as-return / reload clears were removed, and the cart writes the loaded user back to the store. Spec now 4/4 green. (tcox)
|
|
101
111
|
- 2026-09-17 — Header order-for pill added (TRUE-81851), in-memory hand-off store, cart clears on failed user load. (tcox)
|
|
102
112
|
|
|
@@ -107,4 +117,5 @@ Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name`, `
|
|
|
107
117
|
- [cart-notification-emails](cart-notification-emails.md)
|
|
108
118
|
- [cart-page-config-architecture](cart-page-config-architecture.md)
|
|
109
119
|
- [view-as-session-tokens](view-as-session-tokens.md)
|
|
120
|
+
- [order-for delegation (server side)](../../_underscore/features/order-for-delegation.md)
|
|
110
121
|
- [cypress-testing](../workflows/cypress-testing.md)
|
|
@@ -6,8 +6,8 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [tcox]
|
|
9
|
+
updated: 2026-09-21
|
|
10
|
+
owners: [tcox, bala]
|
|
11
11
|
files:
|
|
12
12
|
- src/api/sessionTokens.ts
|
|
13
13
|
- src/api/apiBaseUrl.ts
|
|
@@ -21,6 +21,7 @@ related:
|
|
|
21
21
|
- 2.0/apps/toga2-commerce/features/header-order-for-dropdown.md
|
|
22
22
|
- 2.0/apps/toga2-commerce/workflows/cypress-testing.md
|
|
23
23
|
- 2.0/apps/api2/architecture.md
|
|
24
|
+
- 2.0/apps/_underscore/features/order-for-delegation.md
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
How an admin impersonates another user and gets back: the delegated token pair, the parked admin pair in localStorage, and the return path. Open before touching auth tokens, the axios refresh, or the view-as button.
|
|
@@ -76,10 +77,12 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
|
|
|
76
77
|
## Open items (out of ticket, not fixed)
|
|
77
78
|
|
|
78
79
|
- `SENTRY_AUTH_TOKEN` is committed in 25 tracked `.env*` files — treat it as compromised and rotate it. (Never store the value in this KB.)
|
|
79
|
-
- Confirm api2 authorizes `/auth/delegator` **server-side**
|
|
80
|
+
- ~~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).
|
|
81
|
+
- **The parked-token slot is shared with "Order For".** There is one `originalAuthTokens` key and `beginDelegation` refuses to overwrite it, so once Order For switches tokens the two features cannot nest — decide the precedence before wiring the Order For switch.
|
|
80
82
|
|
|
81
83
|
## Change history
|
|
82
84
|
|
|
85
|
+
- 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)
|
|
83
86
|
- 2026-09-17 — Created. Admin pair parked in `originalAuthTokens` on delegation and restored on return; `/auth/encrypted` re-mint removed; single-flight identity-bound refresh (TRUE-81852, commit a8c3a537). (tcox)
|
|
84
87
|
|
|
85
88
|
## Related
|
|
@@ -88,3 +91,4 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
|
|
|
88
91
|
- [header-order-for-dropdown](header-order-for-dropdown.md)
|
|
89
92
|
- [cypress-testing](../workflows/cypress-testing.md)
|
|
90
93
|
- [api2 architecture](../../api2/architecture.md)
|
|
94
|
+
- [order-for delegation](../../_underscore/features/order-for-delegation.md)
|
|
@@ -6,8 +6,8 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["jcardinal", "apeterson", "tcox"]
|
|
9
|
+
updated: 2026-09-21
|
|
10
|
+
owners: ["jcardinal", "apeterson", "tcox", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/amplify.yml
|
|
13
13
|
- toga2-commerce/.gitattributes
|
|
@@ -117,6 +117,8 @@ AWS Amplify (build + hosting); GitHub Actions (`sync-stage-environments.yml` + t
|
|
|
117
117
|
3. Restart the dev server.
|
|
118
118
|
|
|
119
119
|
A **full `node_modules` wipe is not needed**. Because blox is installed per channel, every branch switch can move the required blox version — treat this class of error as "reinstall blox and clear the Vite cache" before touching any import statement.
|
|
120
|
+
|
|
121
|
+
**Second shape of the SAME drift (2026-09-21): the app crashes on any toast.** `package.json` + lock pinned `1.1.1-production.139` while `node_modules` again held `1.0.317`. The **old** blox `Toaster` takes `message` as an **array** and calls `message.map()`; `ToasterList.tsx` passes a string or `{title, content}` matching the **new** one — so `Uncaught TypeError: message.map is not a function` plus a React error boundary on `<Toaster>`, which reads as an app bug. Same fix, plus **restart Vite with `--force`** so it drops the pre-bundled dep cache. Any blox runtime type error is a version check first.
|
|
120
122
|
- **`resolve.dedupe` was missing from commerce's `vite.config.ts`** (a `frontend.md` §11 MUST), which made consuming blox via a local symlink hazardous: `toga-blox-npm/node_modules/` contains its own `react@18.3.1`/`react-dom@18.3.1`, and because blox ships **unbundled**, imports resolve against the nearest `node_modules` — under a symlink that is blox's own, giving a second React copy. This is precisely why supply's symlink workflow works (it has always deduped `react`/`react-dom`/`@tanstack/react-table`) and commerce's did not. Commerce now dedupes `react`, `react-dom`, `react-hook-form`, `react-router-dom`, `framer-motion`, `@tanstack/react-query`, `@tanstack/react-table`.
|
|
121
123
|
- **`package.json` pins blox with a CARET** (`"@agilant/toga-blox": "^1.0.322-sandbox-client.117"`), violating `frontend.md` §13(a) — some published versions ship an empty `dist`, and a caret can float you onto one. Pin exact. Related: the `TRUE-80707` branch pin is **11 versions behind** the `_sandbox-client` branch pin (`1.0.333-sandbox-client.134`); reconcile before merging.
|
|
122
124
|
- **Secret hygiene re-confirmed unremediated 2026-08-26** — the live tokens are still in the tracked `.npmrc` in **both** `toga2-commerce` and `toga25-supply`.
|
|
@@ -125,6 +127,10 @@ AWS Amplify (build + hosting); GitHub Actions (`sync-stage-environments.yml` + t
|
|
|
125
127
|
- **A caret on a PRERELEASE is inert, not floating.** `^1.0.322-sandbox-client.117` resolves to **exactly** `1.0.322-sandbox-client.117`: npm only matches prereleases sharing the same `major.minor.patch`. Verified with `semver.maxSatisfying`. It is still a [frontend standard](../../../standards/frontend.md) §13(a) violation and confusing, but it is **not** a drift risk — don't chase it as one.
|
|
126
128
|
- **Commerce's dependency health is better than supply's**, verified 2026-08-27: all 19 blox peers satisfied (none missing) and `resolve.dedupe` is committed on `TRUE-80707`.
|
|
127
129
|
|
|
130
|
+
## Change history
|
|
131
|
+
|
|
132
|
+
- 2026-09-21 — Added the second symptom of stale-`node_modules` blox drift: the old `Toaster` `message.map()` crash on any toast (installed 1.0.317 vs pinned 1.1.1-production.139); fix is installing the locked version and restarting Vite with `--force`. (bala)
|
|
133
|
+
|
|
128
134
|
## Related
|
|
129
135
|
|
|
130
136
|
- [toga2-supply Amplify Build & Deploy](../../toga2-supply/workflows/amplify-build-and-deploy.md) — the base pattern and shared gotchas.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
|
|
20
20
|
## 2.0 framework
|
|
21
21
|
|
|
22
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
22
|
+
- **_underscore** (_Underscore) _(framework core)_ — 88 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
23
23
|
- **worker2** (Worker) — 73 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)_ — 19 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: order-for-delegation
|
|
4
|
+
title: Compass Order For — server-side delegation gate, originalUserId audit, delegation-list GET
|
|
5
|
+
author: bala
|
|
6
|
+
repos: [_underscore, api2, dbchanges2, toga2-commerce]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: compass-usa
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-09-21
|
|
11
|
+
updated: 2026-09-21
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: order-for-delegation
|
|
15
|
+
**Date:** 2026-09-21
|
|
16
|
+
**Project/Repo:** _underscore, api2, dbchanges2, toga2-commerce (2.0)
|
|
17
|
+
**Task:** Build the server side of Compass "Order For" (TRUE-81848) so user A can place an order for user B only when a `Users_DelegatorUsers` row allows it, record who really signed in via `Logs_<Client>.Api.originalUserId`, and hand the front end a delegation-list GET call. Covers US Compass and Compass Canada.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
|
|
23
|
+
- **`Users_DelegatorUsers` junction table in every `Client_*` DB.** `dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql`. Columns `id, uuid, userId, delegatorUserId`, unique on the pair. Read as "userId may order for delegatorUserId". Model `_underscore/Model/Client/Users/DelegatorUser.php`. Applied to all 12 local `Client_*` schemas; confirmed present on dev-sandbox `Client_Compass` and `Client_CompassCanada`.
|
|
24
|
+
- **Core metadata.** `dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql`. Record 362, route `user-delegator-users`, `aclDatabase = CLIENT`, RecordFields 2604 id / 2605 uuid / 2606 userId / 2607 delegatorUserId, plus an `InherentRecordChildren` row under record 2 (Users) so `userDelegatorUsers` reads nested under `/users/{uuid}`.
|
|
25
|
+
- **The gate, checked twice.** `_Model_Client_User::canUserOrderForOtherUser()` and `getRequestingUser()` called from `encryptedUser()`, then re-checked in `api2/Component/Api/V2/V2.php` when `/auth/delegator` issues the token, and again on `/auth/refresh`. Evidence: local suite of 27 cases passed — listed target allowed, self allowed, unlisted refused, cross-list refused, chain escalation blocked (Dev Team acting as Gina cannot reach Kathy who is on Gina's list), replayed pairs refused, forged/garbage/missing bearers refused, refresh honours a deleted row and a deactivated account.
|
|
26
|
+
- **`originalUserId` audit column.** `dbchanges2/Logs_Client/2026-09-18a - ApiOriginalUserId.sql` + `_Model_Client_Logs_Api` + three write sites in `V2.php`. Evidence on local, order **SA136321**: `Client_Compass.SalesOrders` id 118454 `createdByUserId = 124256` (Dawn Anderson); `Logs_Compass.Api` row 3618506 `POST /v2/sales-orders` 201 with `userId 124256 / originalUserId 127046` (Dev Team). The switch itself is visible at 13:10:23 — `GET /v2/users/encryptedUser` 200 as 127046, then `POST /v2/auth/delegator` 201 and every row after that is 124256/127046. Across the last 200 rows: 126 own-token rows with NULL, then 70 delegated rows. `SELECT COUNT(*) FROM Logs_Compass.Api WHERE originalUserId = userId` returns 0.
|
|
27
|
+
- **`fetchDelegationList` + `DelegationOption`** in `toga2-commerce/src/pages/Cart/api/CartApi.ts` and `src/pages/Cart/types.ts`. `npx tsc --noEmit` clean. Tanner has since wired it into `useOrderForDropdownViewModel.ts` on `_feature-OrderForDropDown`.
|
|
28
|
+
- **Caller-scoped ACL for both tenants.** `dbchanges2/Client_Compass/2026-09-18a` and `Client_CompassCanada/2026-09-18a`. Read only. US roles 7 Compass Base / 8 Manager; **Canada roles 8 Compass Base / 9 Manager**; `appId 5` = TOGa Commerce. Expression is `CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END`, slug `user-logged-in`. Evidence: with `AclUser.id = 127046` the three Dev Team rows return `isPermitted 1`; with `124256` the same rows return `0`.
|
|
29
|
+
- **Canada `encryptedUser` grant.** Folded into the Canada file as section 4: `Core.RecordScripts` id 16 granted to Canada roles 8 and 9. Canada had only role 1 (Base), so a normal Canada user got a 403 on every pick. US already had it (prod rows 16/30/13). Applied twice locally, no duplicates.
|
|
30
|
+
- **Dev-sandbox twin files** for the three metadata files, field ids **2767 id / 2768 uuid / 2769 userId / 2770 delegatorUserId**: `Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql`, `Client_Compass/2026-09-21a - …DevSandbox.sql`, `Client_CompassCanada/2026-09-21a - …DevSandbox.sql`. Verified by reproducing the dev-sandbox starting state inside a local transaction, running each file twice, then `ROLLBACK`.
|
|
31
|
+
- **Local dbchanges catch-up.** Diffed every local table and column against prod and closed the gaps: `Logs_*` `Email` + `EmailAttachment`, core `Logs` `Issue`/`Event`/`Issue*`, `Client/2026-09-11a` (`SalesOrderItems.fulfillmentType`), `2026-08-28b`, `2026-08-28f`, `2026-09-01b`, `2026-09-01c`, `2026-09-14a`, `_modules/netsuite/2026-09-11a`. `Client_Compass` and `Client_CompassCanada` now match prod column for column.
|
|
32
|
+
- **`/order-for` team skill**, pushed as `d4751825`. Knowledge captured as `0960de3b` (1 new doc + 8 updates).
|
|
33
|
+
|
|
34
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
35
|
+
|
|
36
|
+
- **`empty()` / `isset()` on a `_Model` field.** `$isLoaded = (is_object($api->user) && !empty($api->user->id))` is **always false**. `_Model::initialize()` (`_underscore/Model.php:151`) runs `unset()` on every declared field and `_Model` has no `__isset`, so PHP short-circuits before `__get` runs. This made the gate 403 **everyone**, including a user acting as themself. `php -l` and both reviewers passed it; only running the endpoint found it. Use `(int)$x->id > 0` or `!== null`.
|
|
37
|
+
- **An ACL permission with no logic group.** `allowRead = 1` alone grants nothing — `buildSqlExpression()` returns `'(0)'`. Symptom was a 403 naming `aclRecordPermissionIds: ["1050"]` with `userDelegatorUsers: [null, null, null]`. You need all three rows: `AclRecordExpressions`, `AclLogicGroups`, `AclLogicGroupExpressions`.
|
|
38
|
+
- **Putting the ACL for record 362 in Core.** Record 362 is `aclDatabase = CLIENT`, and every lookup in `V2.php` (3236, 4057, 6502, 7307) switches on that and reads only the matching bucket. The Core rows are never evaluated. They are kept in the file only so Core matches across environments.
|
|
39
|
+
- **`$isDelegationPermitted = true` as the default** in the `/auth/delegator` path. A forged or expired bearer with no `id`, or an already-used `transactionId`, left `$this->delegator` null with no status set, and the fall-through minted a token. Caught in security review. Fixed with `$isDelegatorAuthRequest = false` set before the switch and `true` as the first statement of `case 'delegator'`.
|
|
40
|
+
- **`sqlExpression = '1'` on record 362.** `user-delegator-users` is a real top-level route, so any Compass user could `GET /v2/user-delegator-users` and list who every colleague can order for. Valid CodeRabbit finding.
|
|
41
|
+
- **Reusing the prod pinned uuids in the dev-sandbox files.** Failed with `ERROR 1062 (23000): Duplicate entry 'afb16e5b-f77c-4fe5-94d1-be8ed683e1f5' for key 'aclfieldpermissions.uuid'`. The `AclFieldPermissions` rows are guarded on `recordFieldId`, which differs per environment, so the guard passes and the unique uuid key fails. Only those 9 rows got new uuids; rows keyed the same across environments kept theirs.
|
|
42
|
+
- **Running `Core/2026-09-18a` on dev-sandbox.** RecordFields 2604-2607 there belong to record 344 (`ItemCategoryFeatureGroupTranslations`), so the file dies on a duplicate key at its first insert and **everything after section 2 silently never runs**. Dev-sandbox was left with record 362 present but zero RecordFields, no `InherentRecordChildren` row and no ACL.
|
|
43
|
+
- **Core referencing an api2 class.** `\api\_Component_Api_V2_Response` from inside `_underscore` broke layering (php-reviewer CRITICAL). Use `$api->response::DEFINED_MESSAGE_ERROR_UNAUTHORIZED`.
|
|
44
|
+
- **Requesting `id` in `fetchDelegationList` fields.** 403s the whole GET for these roles. Use `uuid`.
|
|
45
|
+
- **Fetching the delegation list after a token switch.** The signed-in user's own record reads as a 403 once the token belongs to the other person. Load it before the switch and keep it.
|
|
46
|
+
- **Mapping `fetchDelegationList` to `{name}` for the dropdown.** `toOption()` reads `_name`, so every name rendered blank. TypeScript missed it because all `ApiUser` fields are optional.
|
|
47
|
+
- **`sed`/heredoc with `\\` in Bash for PHP namespaces.** `'\\api\\_Component…'` collapsed to `\a` = BEL (0x07) and produced `Parse error: unexpected character 0x07`. Build backslashes with `chr(92)` in Python instead.
|
|
48
|
+
- **Reading/writing `V2.php` with universal newlines.** Converted the whole CRLF file to LF and showed 9221 changed lines. Always read and write with `newline=''`.
|
|
49
|
+
- **Minting a test JWT by hand** to exercise the endpoint. Blocked by the harness as credential handling. Test through the browser or a real login instead.
|
|
50
|
+
- **Blaming the app for `message.map is not a function`.** It was dependency drift: `package.json` and the lock pin `@agilant/toga-blox` at `1.1.1-production.139`, `node_modules` had `1.0.317`. The old Toaster takes an array; `ToasterList.tsx` passes a string or `{title, content}`. Any toast crashed the page.
|
|
51
|
+
- **`npm install <pkg>@<exact>`** rewrote `package.json` to `^1.1.1-production.139`. Reverted both `package.json` and the lock to keep the exact pin.
|
|
52
|
+
|
|
53
|
+
## Not tried yet (candidates for next session)
|
|
54
|
+
|
|
55
|
+
- **Stamp `c_orderSubmittedById` in `_Model_Compass_SalesOrder::prePost()`** (line 445; `ApiPayloadInterceptor` id 24 is already active as PRE on POST for record 14), reading `$api->delegator->user`. Column, `CustomRecordFields` id 40 and the model field (line 51, `FIELD_INTEGER`) all already exist from Compass 1.0. Leave NULL when there is no delegation. Old data is dirty: of 112,096 Compass orders 8,118 have it set and 7,174 of those equal `createdByUserId`.
|
|
56
|
+
- **Grant role 3 (API) on record 362 in each `Client_*` database.** Only the dead Core grant exists, so no integration can read or write the table through api2. Nothing can seed `Users_DelegatorUsers` that way today.
|
|
57
|
+
- **Close the `/auth/encrypted` hole** — it still accepts a delegation pair with no bearer and records no delegator. Three options were drafted and none chosen: guard `/auth/encrypted`, use a separate key for delegation pairs, or drop pairs for a plain uuid. Narrowed because Commerce no longer calls it; toga2-desk still does for client switching.
|
|
58
|
+
- **Seed `Users_DelegatorUsers`.** No admin screen and no import exist. The table ships empty, so the feature does nothing until rows are added.
|
|
59
|
+
- **Turn the 27-case suite into PHPUnit.** It lives only in a scratch `t.sh` and was never committed.
|
|
60
|
+
- **Replay the Core surface seeds on local.** Local Core data is behind prod: Surfaces 39 vs 56, SurfaceElements 113 vs 214, RecordFields 2171 vs 2186, CronJobs 57 vs 71. Affects UI columns and actions, not ordering.
|
|
61
|
+
- **`Client_Aig`, `Client_Growrk`, `Client_Managelife`, `Client_True`** have no `TransferOrders` tables locally, so those dbchanges skipped them.
|
|
62
|
+
|
|
63
|
+
## Current file state
|
|
64
|
+
|
|
65
|
+
| File | Status | Notes |
|
|
66
|
+
|------|--------|-------|
|
|
67
|
+
| `_underscore/Model/Client/Users/DelegatorUser.php` | new, uncommitted | Model for the junction table |
|
|
68
|
+
| `_underscore/Model/Client/User.php` | modified, uncommitted | `canUserOrderForOtherUser`, `getRequestingUser`, gate inside `encryptedUser` |
|
|
69
|
+
| `_underscore/Model/Client/Logs/Api.php` | modified, uncommitted | `originalUserId` field after `userId` |
|
|
70
|
+
| `api2/Component/Api/V2/V2.php` | modified, uncommitted | `$isDelegatorAuthRequest`, delegator re-check, refresh re-check, `getOriginalUserId()` + 3 log sites |
|
|
71
|
+
| `dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql` | new, uncommitted | Applied to all 12 local `Client_*` |
|
|
72
|
+
| `dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql` | new, uncommitted | Record 362, fields 2604-2607. Its ACL block is inert (CLIENT record) |
|
|
73
|
+
| `dbchanges2/Client_Compass/2026-09-18a - UserDelegatorUsersReadAcl.sql` | new, uncommitted | Roles 7/8, caller-scoped expression |
|
|
74
|
+
| `dbchanges2/Client_CompassCanada/2026-09-18a - UserDelegatorUsersReadAcl.sql` | new, uncommitted | Roles 8/9, caller-scoped, plus the `encryptedUser` grant as section 4 |
|
|
75
|
+
| `dbchanges2/Logs_Client/2026-09-18a - ApiOriginalUserId.sql` | new, uncommitted | Applied to all 10 local `Logs_*` |
|
|
76
|
+
| `dbchanges2/Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql` | new, uncommitted | Field ids 2767-2770. NOT yet run on dev-sandbox |
|
|
77
|
+
| `dbchanges2/Client_Compass/2026-09-21a - …DevSandbox.sql` | new, uncommitted | Field ids 2768-2770. NOT yet run on dev-sandbox |
|
|
78
|
+
| `dbchanges2/Client_CompassCanada/2026-09-21a - …DevSandbox.sql` | new, uncommitted | Field ids 2768-2770. NOT yet run on dev-sandbox |
|
|
79
|
+
| `toga2-commerce/src/pages/Cart/api/CartApi.ts` | modified, uncommitted | `fetchDelegationList` only |
|
|
80
|
+
| `toga2-commerce/src/pages/Cart/types.ts` | modified, uncommitted | `DelegationOption` only |
|
|
81
|
+
| `toga2-commerce/src/components/OrderForDropdown/useOrderForDropdownViewModel.ts` | changed by Tanner | Now calls `fetchDelegationList`; still no token switch |
|
|
82
|
+
| `~/toga-tech/skills/order-for/SKILL.md` | pushed `d4751825` | Also copied to `C:\WWW\.claude\skills` and `~/.claude/skills` |
|
|
83
|
+
| `~/toga-tech/knowledge/**` | pushed `0960de3b` | 1 new doc + 8 updates |
|
|
84
|
+
|
|
85
|
+
## Decisions made
|
|
86
|
+
|
|
87
|
+
- **The server is the gate, never the UI.** Checked at pair mint and again at token issue, because the encrypted pair has no expiry and no MAC (`_String::encryptWithKey`, AES-256-CBC keyed on `Core.Parameters.API_SECRET_REFRESH_TOKEN`) so a captured pair is replayable forever. Rejected: checking once at mint.
|
|
88
|
+
- **Read-only ACL for the tenant roles.** A role that can write its own row can self-grant the right to order for anyone. Rejected: giving Compass Base create/update.
|
|
89
|
+
- **Scope the row expression to `AclUser.id` rather than `'1'`.** `user-delegator-users` is a routable record. Rejected: `'1'` with the nested-read path as the only protection.
|
|
90
|
+
- **Separate `…DevSandbox.sql` twins instead of editing the originals.** Keeps local and prod stable while dev-sandbox uses its own field ids. Rejected: renumbering the shared files.
|
|
91
|
+
- **Keep the inert Core ACL block in the dev-sandbox Core file.** Core then looks the same in every environment and diffs cleanly. Rejected: deleting three rows that do nothing.
|
|
92
|
+
- **Scoped `toga2-commerce` back to the GET call only.** The token switch is Tanner's work, not this ticket. I had built the full switch (new `useOrderForSessionZu` store, `beginDelegation` on pick, cross-tab handling) and reverted all of it on the developer's instruction. Rejected: shipping both halves from this branch.
|
|
93
|
+
- **Share the feature as a `/order-for` skill, not the raw chat.** The transcript is 9.2 MB of JSONL and contains a bearer token and prod query output.
|
|
94
|
+
|
|
95
|
+
## Blockers
|
|
96
|
+
|
|
97
|
+
- **Nothing seeds `Users_DelegatorUsers`.** The feature is inert in every environment until rows exist. No admin screen, no import, and role 3 has no working grant to write them through api2.
|
|
98
|
+
- **The front end does not switch the token.** Until it calls `/auth/delegator`, `$api->delegator` is never set, so `originalUserId` stays NULL, the `Users_DelegatorUsers` check never fires, and the user browses their own catalog and prices while "ordering for" someone else. This is a Tanner/PM decision, not a code blocker on the server side.
|
|
99
|
+
- Nothing else is blocked. All four repos have working, tested changes sitting uncommitted.
|
|
100
|
+
|
|
101
|
+
## Exact next step
|
|
102
|
+
|
|
103
|
+
> Run `dbchanges2/Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql` against dev-sandbox `Core`, then the two `…DevSandbox.sql` files against dev-sandbox `Client_Compass` and `Client_CompassCanada`. Confirm with `SELECT id, field FROM RecordFields WHERE recordId = 362` returning 2767-2770. Dev-sandbox is currently half-registered: record 362 exists with no fields, no `InherentRecordChildren` row and no ACL, so any `user-delegator-users` or nested `userDelegatorUsers` read there is broken until these run.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
_Saved by /session-save on 2026-09-21_
|
package/package.json
CHANGED