toga-ai 1.0.856 → 1.0.858

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.
@@ -0,0 +1,147 @@
1
+ ---
2
+ type: session
3
+ slug: TRUE-82180-staples-asn-late
4
+ title: Staples ASNs sent days-to-weeks late — root cause investigation
5
+ author: mhammontree
6
+ repos: [worker, library, dbchanges]
7
+ framework: "1.0"
8
+ client: staples
9
+ status: active
10
+ created: 2026-09-21
11
+ updated: 2026-09-21
12
+ ---
13
+
14
+ # Session: TRUE-82180-staples-asn-late
15
+ **Date:** 2026-09-21
16
+ **Project/Repo:** worker (1.0)
17
+ **Task:** Investigate why 856 ASNs are sent to Staples days-to-weeks after we ship and after we invoice — example PO 478KVU invoiced 9/8, ASN sent 9/18.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ - **Root cause identified and proven.** `worker/crons/sync/staples/staples_asn_netsuite.php` L48 sets
24
+ `$modifiedAfterEpochTime = $currentEpochTime - 3600` and the cron runs `50 * * * *`. The selection window
25
+ equals the run interval, so each shipment gets exactly ONE chance. No state, no backlog, no retry. A missed
26
+ shipment is only sent later when something edits the item fulfillment in NetSuite.
27
+ - **Confirmed the driver is a NetSuite `lastModifiedDate onOrAfter` search** — `App_NetSuite::listAllItemFulfillmentsForCustomer()`
28
+ in `library/app/netsuite.php` L1633-1679. It already accepts an after AND before timestamp, so no library
29
+ change is needed to change the window.
30
+ - **Proved it on the example order.** `Logs`.`FileLog` (env `legacy`), job `STAPLES_ASN_OUT_ORDER`:
31
+ - PO 478KVU: 850 imported 2026-09-01 12:45; ASN cXML says `shipmentDate="2026-09-02T21:00:00.000-07:00"`
32
+ (9/3 Central) and `noticeDate="2026-09-17T11:53:21.000-07:00"`.
33
+ - `noticeDate` is `$order->lastModifiedDate`. The fulfillment was edited 9/17 13:53 Central; the 14:52 run
34
+ sent it. 15 days after ship.
35
+ - **Sized the problem.** First-ASN vs `shipmentDate`, "late" = >1 day. By ship date:
36
+ 09-01 49 on-time/17 late; 09-02 5/16; 09-03 4/13; 09-07 11/30; 09-08 15/11; 09-09 63/11; 09-10 9/9;
37
+ 09-13 0/24; 09-14 0/27; 09-15 8/5. ~half late every day. 40+ orders shipped 8/18–9/07 all first-ASN'd in
38
+ one burst 9/15–9/18.
39
+ - **Found a second, separate bug: duplicate sends.** Dedupe at L453 checks SFTP `/archive` only, never `/out`.
40
+ PO 478KVU written 10 times in 10 hours. PO 755KVB written as `856_755KVB_1_*` repeatedly on 9/11 AND again
41
+ as `856_755KVB_2_1789602977` on a later day — same single item, same tracking `488192721813`. The
42
+ `<Package identifier="...">` is `mt_rand(1000,10000)` so it differs every send.
43
+ - **Proved `partialPackIdentifier` is unusable as a dedupe key.** It restarts at 1 every run and depends on
44
+ iteration order, so the `856_<PO>_<n>` prefix check against `/archive` cannot recognise an already-sent
45
+ shipment.
46
+ - **Ruled out the overlap guard.** `App_Framework::cronInitialization()` (`library/app/framework.php` L689-711)
47
+ calls `exitIfProcessRunning()` BEFORE inserting the `Common`.`CronJobExecutions` check-in row. Check-in rows
48
+ exist at `:50` for all 24 hours on 9/2, 9/3, 9/4. Every run started.
49
+ - **Ruled out reuse of existing tables.** `Common`.`AdvanceShipNotices` (163,145 rows) is an old INBOUND vendor
50
+ ASN feed (ACER/LENOVO/APPLE/LEXMARK), dead since 2022-05-17, wrong grain and wrong direction.
51
+ `TOGA_Staples` DB is the Staples retail-services app (Stores, Incidents, ServiceRequests) — unrelated.
52
+ - **CTO agent review completed** — verdict DISAGREE-WITH-ALTERNATIVE. Agreed on the state table in `Common`,
53
+ rejected widening the lookback, insisted on a different order of work.
54
+ - **Leadership write-up delivered** as a Claude doc:
55
+ https://claude.ai/code/artifact/95957a53-cd23-4ba6-a850-83b143b037b0
56
+ (private — Mark must share it from the Share menu before anyone else can open it).
57
+
58
+ ## What did NOT work — DO NOT RETRY THESE
59
+
60
+ - **CTO claim that a stale `$itemCxmlConfirmation` is emitting package-less ASNs — FALSE, disproven.**
61
+ Query over all `STAPLES_ASN_OUT_ORDER` rows since 2026-08-20: `totalAsns 8831, noPackageTag 0, noItems 0`.
62
+ Every ASN sent contains both `<Package ` and `<ShipNoticeItem `. Do not re-open this theory. The leaked
63
+ variable is still a real code defect, it is just not firing in production.
64
+ - **CTO claim of cross-fulfillment item bleed — real in code, NOT demonstrated in data.** `$itemCXMLArray` is
65
+ initialised at L136, outside the `foreach ($partialItemFulFil as $order)` loop, so a second fulfillment would
66
+ carry the first's items. But only 2 POs since 9/1 have more than one pack (755KVB, 856KWB) and 755KVB's
67
+ `_1` and `_2` files are byte-identical apart from the random package identifier — one item each. Not proven.
68
+ - **Widening the lookback window to 7 days — rejected on numbers.** ~30 shipments/day x ~20 serials, one SOAP
69
+ `get` per serial (L326) plus `getSalesOrder` + `get itemFulfillment` + `listAllItemFulfillmentsForSalesOrder`
70
+ per fulfillment ≈ 4,600 SOAP calls per hourly run, 40–75 min runtime. It would collide with the next `:50`
71
+ run and `exitIfProcessRunning` would then start eating hours. Also re-writes the full cXML BLOB into
72
+ `FileLog` every pass (the FileLog write at L446 happens BEFORE the dedupe check at L453).
73
+ - **Using `CronJobExecutions.dtCheckOut` to detect runs that died mid-loop — does not work.** The script never
74
+ calls `App_Framework::cronFinished()` or `App_Database::closeConnections()`, so EVERY row has
75
+ `dtCheckOut = NULL`. A completed run and a crashed run are indistinguishable. Do not read NULL as "crashed".
76
+ - **Looking for "ASN Not Generated" emails to find failures — there are none, ever.** `Vision_Log`.`Email`
77
+ searched back to 2026-08-25 for `emailObject LIKE '%ASN Not Generated%'`: 0 rows. The else-branch at L469
78
+ has never fired. Late orders are dropped BEFORE reaching that check.
79
+ - **`toga_query` blocks the `REPLACE` keyword.** A `LENGTH(x)-LENGTH(REPLACE(x,...))` counting query is
80
+ rejected with "Forbidden keyword 'REPLACE' found in query". Use `LOCATE`/`SUBSTRING` or fetch and eyeball.
81
+ - **`Logs` schema is on the `legacy` environment, not `production`.** `toga_query` with
82
+ `environment='production'` gives "Table 'Logs.FileLog' doesn't exist". Also `FileLog` has `dtStamp`, NOT
83
+ `dtCreated`.
84
+
85
+ ## Not tried yet (candidates for next session)
86
+
87
+ - Confirm the actual per-order silent skip. Strongest candidate is the `if ($matchedFile)` gate at L160 —
88
+ it requires OUR OWN 855 ack to already be in SFTP `/archive`, i.e. Staples must have collected it before we
89
+ will send the ASN. Customer-controlled, no log, no email. Needs SFTP pickup timing to confirm.
90
+ - Test the mid-run-death theory: per hour, compare the count of fulfillments the NetSuite search returned
91
+ against the `FileLog` row count for that run. A truncated tail proves a warning killed the cron.
92
+ - Size the item-bleed risk: find sales orders with multiple fulfillments AND multiple line items, and check
93
+ whether any delivered ASN carried another fulfillment's items.
94
+ - Nothing has been written. No code changed, no SQL written, no branch commits.
95
+
96
+ ## Current file state
97
+
98
+ | File | Status | Notes |
99
+ |------|--------|-------|
100
+ | `worker/crons/sync/staples/staples_asn_netsuite.php` | **unchanged** | Read only. 496 lines. Branch `TRUE-82180` checked out, working tree clean. |
101
+ | `dbchanges/` | **unchanged** | Branch `TRUE-82180` checked out. No file created. Target would be `dbchanges/Common/MH/<date>.sql`. |
102
+ | `library/` | **unchanged** | On branch `TRUE-81955` (another ticket). Not needed for this work. |
103
+ | Claude doc (leadership findings) | created | https://claude.ai/code/artifact/95957a53-cd23-4ba6-a850-83b143b037b0 — private, not yet shared. |
104
+
105
+ ## Decisions made
106
+
107
+ - **Ticket switched to pseudocode-only, for Jeff to review.** Mark changed the scope late in the session: the
108
+ ticket asks for an investigation, not a fix. Pseudocode + the findings breakdown go in the ClickUp task's
109
+ pseudocode field. **No code is to be written until Jeff reviews.**
110
+ - **This may be solved the 2.0 way instead of fixing 1.0.** Open possibility raised by Mark. Do not assume the
111
+ 1.0 design below is the agreed path — it is the 1.0 option, not the decision.
112
+ - **If fixed in 1.0: a state table, not a wider window.** Rationale: the expensive work (per-serial SOAP calls)
113
+ must be driven by "not yet sent", not by "recently modified". Rejected: widening the lookback (see failures
114
+ above); rejected: keeping SFTP folder listings as the record of what was sent.
115
+ - **Home would be `Common` (`db_common`), table `StaplesAdvanceShipNotices`.** Rationale: `Common` already
116
+ holds cross-app NetSuite state (`App_Model_Common_NsSalesOrderItem`, `NsSerialNumber`, `NsTrackingNumber`)
117
+ and `CronJobExecutions`. Rejected `Logs` — it is append-only under retention pressure (API logs already cut
118
+ to 30 days), and mutable state must not sit where a purge job can delete it. Rejected `TOGA_Staples` —
119
+ wrong business domain. Name must NOT be `AdvanceShipNotices`, that is taken.
120
+ - **Key on `itemFulfillmentInternalId` with a UNIQUE constraint; rename the file to
121
+ `856_<PO>_<itemFulfillmentInternalId>`.** Rationale: stable NetSuite-assigned id, one ASN per fulfillment,
122
+ and the `/out` + `/archive` checks become exact matches instead of prefix guesses. This is the fix for the
123
+ duplicates.
124
+ - **Sequencing, if built (CTO's main correction).** 1) Fix the leaked per-iteration variables, the unchecked
125
+ `$conn->put()` return at L467, move the `FileLog` write after the dedupe, add `cronFinished()` +
126
+ `closeConnections()`, log every skip with a reason. 2) Add the table and switch the filename key. 3) Seed the
127
+ table as already-sent from `FileLog`, run one cycle log-only. 4) Only then enable retry, with an attempt cap
128
+ and an age alert. Rationale: enabling retry before step 1 would re-send bad ASNs at scale, and an empty table
129
+ on day one would dump months of history on Staples.
130
+
131
+ ## Blockers
132
+
133
+ 1. **Waiting on Jeff's review of the pseudocode.** Scope is now pseudocode only; no implementation.
134
+ 2. **Unanswered, needed from Staples:** does a repeated `ShipNoticeRequest` with `operation="new"` and the same
135
+ `shipmentID` get ignored, or treated as a new shipment? We have been sending repeats for weeks, so this
136
+ also decides whether existing damage needs cleanup.
137
+ 3. **Unanswered, business decision:** should the 855 gate at L160 exist at all? It hands our ASN timing to the
138
+ customer's file-pickup schedule.
139
+
140
+ ## Exact next step
141
+
142
+ > Write the pseudocode into the ClickUp task TRUE-82180 pseudocode field (Mark has already put the findings
143
+ > breakdown there), covering the 4-step sequence in "Decisions made", and hold for Jeff's review. Do NOT write
144
+ > code in `worker` or `dbchanges` — both branches are clean and must stay that way until Jeff decides 1.0 vs 2.0.
145
+
146
+ ---
147
+ _Saved by /session-save on 2026-09-21_
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.856",
3
+ "version": "1.0.858",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",