toga-ai 1.0.1034 → 1.0.1035
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -44,6 +44,7 @@ related:
|
|
|
44
44
|
- ../../../clients/compass-usa/features/sales-order-line-renumbering.md
|
|
45
45
|
- ./language-translation-layer.md
|
|
46
46
|
- ../../_underscore/features/persona-nav-visibility-gate.md
|
|
47
|
+
- ../../../clients/compass-usa/features/approval-decision-flow.md
|
|
47
48
|
---
|
|
48
49
|
|
|
49
50
|
How `_Model` prePost/postPost/prePut/postPut hooks are dispatched by the api2 /v2 engine only when a metadata row exists — the missing-row silent no-op, the wrong-row hard fatal, and the persistence/transaction traps; open before writing, activating, or debugging any interceptor.
|
|
@@ -247,6 +248,12 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
|
|
|
247
248
|
- **Do not use the interceptor path just to send an email.** For a plain send there is a documented non-API entry point: `_Model_Client_EmailTemplate::send(string $clientIdentifier, ...)`, versus the `$api`-based `sendEmail()`. See [Email Template Sending](../../_underscore/features/email-template-sending.md). Faking `$api` is for reusing *business logic*, not for reaching the mailer.
|
|
248
249
|
- **Notes will not be logged.** `_Model_Client_Logs_Record::addNote()` is flushed only by `Component/Api/V2/V2.php`, so notes staged by the interceptor you just ran are **dropped** in a worker. See [Record Change Audit Log](../../_underscore/features/record-change-audit-log.md).
|
|
249
250
|
|
|
251
|
+
### Passing state from a PRE hook to a POST hook
|
|
252
|
+
|
|
253
|
+
A `postPut`/`postPost` runs after the save, so it cannot compare old vs new values itself. Capture what it needs in the PRE hook and hand it over on **`$api->passThrough`** (an array on the request object, `null` until set — init it). Use a class const for the key (`PASSTHROUGH_KEY__...`).
|
|
254
|
+
|
|
255
|
+
Main use: **fire a POST side effect only on a REAL field change.** Edit forms resend unchanged fields (TOGa Commerce resends `contact` on every order save), so "key present in `$api->httpPayload`" is not "value changed". In `prePut`, read the current DB value, compare to the payload, store a bool; `postPut` acts only when it is true. Both hooks need their own interceptor row — if the PRE row is missing, the key is absent, so fall back to the old check rather than silently skipping. Reference: `_Model_Compass_SalesOrder::rememberWhetherOrderForPersonIsChanging()` (TRUE-82727, see [Compass approval-decision flow](../../../clients/compass-usa/features/approval-decision-flow.md)). Also used to carry a non-column field past PRE: Rate's `PASSTHROUGH_KEY_FULFILLMENT_PREFERENCE` ([AIG contract creation](../../../clients/rate/features/aig-contract-creation.md)).
|
|
256
|
+
|
|
250
257
|
### Own-row guards in a PRE interceptor
|
|
251
258
|
|
|
252
259
|
A PRE hook that limits a user to their own rows must close four bypasses — each found by review on the `user-global-settings` guard (TRUE-82613):
|
|
@@ -314,3 +321,4 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
314
321
|
## Change history
|
|
315
322
|
- 2026-10-08 — `internalApiRequest` "[]" throw also fires on the OUTER request's warnings (e.g. WO-1); throw site now ~L2436; Core + client duplicate rows fire twice. (rgirish)
|
|
316
323
|
- 2026-10-09 — Own-row guards in PRE hooks (alias keys + parent-route bypasses, PRE before field ACL); user-global-settings reference; known open holes. (jcardinal)
|
|
324
|
+
- 2026-10-15 — PRE → POST state hand-off via `$api->passThrough`; detect a real field change, not a resent one (TRUE-82727). (bala)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["apeterson", "dfranks", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/ApprovalDecision.php
|
|
@@ -31,6 +31,7 @@ related:
|
|
|
31
31
|
- ../../../2.0/apps/_underscore/features/record-change-audit-log.md
|
|
32
32
|
- ../../../2.0/apps/_underscore/features/core-domains-app-host-registry.md
|
|
33
33
|
- ../../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md
|
|
34
|
+
- ../../../2.0/apps/api2/features/api-payload-interceptors.md
|
|
34
35
|
---
|
|
35
36
|
Compass sales-order approval-decision / notification flow — approval+CC email lists, manager reassignment, settings-driven manager (VIP) auto-approve, EN/FR localization; open when touching Compass approval emails, manager swaps, or stuck/stranded approvals.
|
|
36
37
|
|
|
@@ -106,7 +107,7 @@ Data (all ids identical in prod, dev-sandbox, client-sandbox):
|
|
|
106
107
|
|
|
107
108
|
Evaluated in exactly three places, all calling the shared check:
|
|
108
109
|
- **(a) Order creation** — `_Model_Compass_SalesOrder::postPost`, inside `elseif (!empty($managerUserUuid))` (manager = delegate or requester's `Users.supervisorUserId`).
|
|
109
|
-
- **(b) "Order for" / contact or delegate change** — `_Model_Compass_SalesOrder::postPut`; re-derives the manager. Compares **subtotal** since TRUE-82608 (before, it wrongly used `_total` = subtotal + shipping + tax).
|
|
110
|
+
- **(b) "Order for" / contact or delegate change** — `_Model_Compass_SalesOrder::postPut`; re-derives the manager **only when the order's contact really changed** (TRUE-82727, see gotcha). Compares **subtotal** since TRUE-82608 (before, it wrongly used `_total` = subtotal + shipping + tax).
|
|
110
111
|
- **(c) Manager reassignment** — `_Model_Compass_ApprovalDecision::_evaluateManagerVip` via `handleManagerReassignment` (wired into `postPut` only, TRUE-78851); also reached by the bulk stranded-approval worker. Log notes `"Manager reassigned — approval decision reset"` (no auto-approve) or `"Manager approval skipped due to VIP logic"`.
|
|
111
112
|
|
|
112
113
|
**Compass Canada has no manager auto-approve, by business rule.** Same code path runs, finds no setting-18 row (nothing seeded; persona 35 does not exist there), never auto-approves. Empty Canada `ApprovalDecisionTypes` is expected, not a bug.
|
|
@@ -196,10 +197,12 @@ When a step-2 (Manager) decision is reassigned, `_swapManagerEmailAddress()` upd
|
|
|
196
197
|
- **⚠ An exemption from a server-side guard must be decided from the DATABASE, never from a client-supplied field.** The step-1 guard originally skipped itself when the payload carried `approvalDecisionType` / `approvalDecisionTypeId`, treated as a "system auto-approval" marker. That value comes from the request body → any caller could set it and skip the check. The early return and its `_isSystemAutoApproval` helper were removed in PR review. Checked against prod first: all **5,028** step-1 auto-approvals in Compass history are MR/MA orders (Canada has none), so the DB-backed order-number check covers every real case, and VIP auto-approvals land on step 2 and exit at the step check. General rule for any guard added here.
|
|
197
198
|
- **⚠ The green approve tick used to silently replace the step-2 manager with the approver.** `makeApprovalDecision()` / `updateApprovalDecision()` in supply hardcoded `assignedToUserId: { uuid: userUuid }` into every single-stage approve payload, so an admin approving the manager stage overwrote the real manager with themselves — and the timeline read "Manager Approved", making it look like the manager acted. Confirmed on **SA136901** (07:18:35 re-sent an assignee that had not changed; Dawn Anderson already assigned by Emily at 06:50:06); also hit **SA136807** (Angel Almodovar replaced) and **SA137080** (delegate manager Daniel Mezzanares replaced). Fixed front-end by removing the field — `decidedByUserId` already records who approved. The "Approve All Stages" path was always correct, which is why it never reproduced there. Detail: [Record Modals & Nested Tables](../../../2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md).
|
|
198
199
|
- **⚠ A manager swap sent in the SAME save as an approval is never logged, and the email list keeps the old manager.** `_evaluateManagerVip()` returns immediately when the request carries `isApproved`, assuming a request is *either* an assignment *or* an approval. When both arrive together the `"Manager reassigned"` note is skipped (the feature works — fired 757 times since May) AND so is the `SalesOrderEmailAddresses` swap, so the outgoing manager stays on the notification list. A handler for the combined case was drafted on the `_sandbox-client` branch only and is NOT on `TRUE-81900` — known gap, not shipped.
|
|
200
|
+
- **⚠ Commerce's edit-order save resends the unchanged `contact`; only a REAL contact change may re-pick the manager.** Before TRUE-82727, `postPut` treated any `contact` key in `$api->httpPayload` as an "order for" change, re-read the cart `delegate` note and reset step-2 `assignedToUserId` to the cart's manager. So an admin's manager swap in Supply was undone by the user's next Commerce save, and re-setting it ran `_swapManagerEmailAddress()` again, dropping the address the user had added (user "kept disappearing" from notifications). Real orders: **SA138802** (9 Oct), **SA138938** (13 Oct). Fix: `prePut` → `rememberWhetherOrderForPersonIsChanging()` reads the order's current `Contacts.uuid` before the write, compares (case-insensitive) to incoming `contact.uuid`, stores the bool in `$api->passThrough[PASSTHROUGH_KEY__IS_ORDER_FOR_CHANGED]`; `postPut` re-picks only when true. Flag absent (no PRE/PUT row) → old "contact in payload" check. Contact without a `uuid` → counts as changed. Depends on the `recordId 14` PRE/PUT interceptor row (prod: `Client_Compass` id 26, `Client_CompassCanada` id 18). Pattern: [PRE → POST via passThrough](../../../2.0/apps/api2/features/api-payload-interceptors.md#passing-state-from-a-pre-hook-to-a-post-hook).
|
|
199
201
|
- **⚠ Never read the auto-approve LIMIT from `UserGlobalSettings`.** Users can write their own `UserGlobalSettings` rows (the "own" ACL), so a per-user limit would let anyone raise their own approval ceiling. Limit lives only on `PersonaGlobalSettings` (admin-controlled). The opt-out is per-user on purpose: any write to it can only force manual approval, never skip it. Settings 18/19 are not in the user-writable allowlist of the `UserGlobalSettings` own-row guard — see [own-row guards](../../../2.0/apps/api2/features/api-payload-interceptors.md#own-row-guards-in-a-pre-interceptor).
|
|
200
202
|
- **The upstream cause of an unassigned step 2 is a user saved with no `supervisorUserId`.** The admin add-user screen allows saving a user without one, and the PEOPLE-file import may or may not backfill it later. That data hole feeds both the `!$hasManager` wipe above and the never-created step-2 decision at order creation. Not fixed — the guard only stops the order being *stranded*, it does not give it a manager.
|
|
201
203
|
|
|
202
204
|
## Change history
|
|
205
|
+
- 2026-10-15 — postPut re-picks the step-2 manager only on a real "order for" contact change, detected in prePut via `$api->passThrough` (TRUE-82727, `_underscore` PR #771). (bala)
|
|
203
206
|
- 2026-10-09 — Manager auto-approve moved from `c_isVip` + hardcoded 5000 to persona setting 18 (limit) + user setting 19 (opt-out); postPut now compares subtotal (TRUE-82608). (bala)
|
|
204
207
|
- 2026-10-06 — Recorded that no delegate + no supervisor saves an order with no step-2 decision, and that the commerce cart now blocks it (TRUE-82423). (bala)
|
|
205
208
|
|
package/package.json
CHANGED