toga-ai 1.0.997 → 1.0.998

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.
@@ -182,7 +182,9 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
182
182
  is an empty subclass of `_Model_Compass_ApprovalDecision`; the entire approval/notification/
183
183
  manager-reassignment/VIP-auto-approve flow lives in the parent and branches at runtime on
184
184
  `clientIdentifier === 'Compass_Canada'` (email-template UUID + EN/FR localization). Fixes to the
185
- parent cover Canada automatically — no per-tenant change. See
185
+ parent cover Canada automatically — no per-tenant change. **Canada has no manager (VIP)
186
+ auto-approve by business rule**: no setting-18 limit is seeded, so it never fires; empty
187
+ `ApprovalDecisionTypes` is expected. See
186
188
  [Approval-Decision Flow](../compass-usa/features/approval-decision-flow.md).
187
189
  - **Canada shares the duplicate-`Users`-row / stranded-approval defect — confirmed 2026-08-20.**
188
190
  `Client_CompassCanada` held **2** approvals assigned to dead duplicate user rows (vs. 53 in
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary |
4
4
  |-----|-----------|---------|
5
- | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass sales-order approval-decision / notification flow — approval+CC email lists, manager reassignment, VIP auto-approve, EN/FR localization; open when touch |
5
+ | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass sales-order approval-decision / notification flow — approval+CC email lists, manager reassignment, settings-driven manager (VIP) auto-approve, EN/FR loc |
6
6
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | Compass USA ASN posting auto-creates the ItemFulfillment on the upstream SalesOrder and propagates tracking at every granularity; open when working ASN→IF creat |
7
7
  | [Compass Contact Email Resolution & Crossed `primaryContactEmailAddressId` Pointers](features/contact-email-resolution.md) | 2.0 | Any Compass code reading a contact's email must assert the email row belongs to that contact (~14% of pointers are crossed); open before reading Compass contact |
8
8
  | [Corporate vs Field laptop personas — the sector-52 split (designed, NOT built)](features/corporate-vs-field-laptop-personas.md) | 2.0 | Agreed design for splitting the Compass catalogue into a Corporate laptop and a field "laptop kit": attach Corporate at sector `J00000` via `Locations_Personas` |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-06
9
+ updated: 2026-10-09
10
10
  owners: ["apeterson", "dfranks", "bala"]
11
11
  files:
12
12
  - _underscore/Model/Compass/ApprovalDecision.php
@@ -16,11 +16,14 @@ files:
16
16
  - _underscore/Model/Client/ApprovalTemplateStage.php
17
17
  - _underscore/Model/Quad/SalesOrder.php
18
18
  - dbchanges2/Client_Compass/2026-09-10b - RegisterApprovalDecisionPrePostInterceptor.sql
19
+ - dbchanges2/Core/2026-10-09a - ManagerAutoApproveSettings.sql
20
+ - dbchanges2/Client_Compass/2026-10-09a - ManagerAutoApproveSettingsSeed.sql
19
21
  related:
20
22
  - mr-ma-order-approval-and-status.md
21
23
  - ../../../2.0/apps/toga25-supply/features/action-button-rule-engine.md
22
24
  - ../../../2.0/apps/_underscore/features/sales-order-denial-reason.md
23
25
  - people-file-user-lifecycle.md
26
+ - persona-model-and-levy-gating.md
24
27
  - stranded-approval-reassignment.md
25
28
  - ../../compass-canada/profile.md
26
29
  - ../../compass-canada/features/french-order-email-localization.md
@@ -29,7 +32,7 @@ related:
29
32
  - ../../../2.0/apps/_underscore/features/core-domains-app-host-registry.md
30
33
  - ../../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md
31
34
  ---
32
- Compass sales-order approval-decision / notification flow — approval+CC email lists, manager reassignment, VIP auto-approve, EN/FR localization; open when touching Compass approval emails, manager swaps, or stuck/stranded approvals.
35
+ 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.
33
36
 
34
37
  ## Summary
35
38
  Whole flow lives in shared parent `_Model_Compass_ApprovalDecision`. Both tenant subclasses `_Model_Compass_Usa_ApprovalDecision` and `_Model_Compass_Canada_ApprovalDecision` are **empty** (`extends _Model_Compass_ApprovalDecision {}`, no overrides). Client/language behavior is NOT subclassed — it branches at runtime in the parent on `$api->client->clientIdentifier` (`Compass_Usa` vs `Compass_Canada`). Fix a parent method → applies to both Compass US and Canada; no per-client change.
@@ -84,13 +87,29 @@ Only **three** clients ever build a supply order link — Compass USA, Compass C
84
87
  - **`worker2` — zero** supply/commerce link generation.
85
88
  - **Out of scope, same bug class, deliberately left alone:** user-facing commerce `userOrderUrl` links (`Model/Compass/SalesOrder.php` ~L484 and ~L1060, `Model/Compass/ApprovalDecision.php` ~L423 and ~L730) plus ~ten inline commerce URL literals across worker crons.
86
89
 
87
- ### VIP manager auto-approve — rule + its three (all supervisor-derived) triggers
88
- A manager-stage (step 2) approval auto-approves iff the assigned manager's `Users.c_isVip = 1` AND order subtotal `<= _Model_Compass_SalesOrder::AUTO_APPROVE_IF_MANAGER_IS_VIP_AND_UNDER_AMOUNT` (= `5000`). Subtotal = `SUM(SalesOrderItems.quantity * price)` via `_Model_Client_SalesOrder::_subtotal()`.
90
+ ### Manager auto-approve (VIP) — settings-driven rule + its three triggers (TRUE-82608, 2026-10-09)
91
+ One check, `_Model_Compass_ApprovalDecision::shouldAutoApproveManagerStep(int $managerUserId, float $orderSubtotal)`. Step 2 auto-approves iff ALL:
92
+ 1. Manager has a limit = `MAX(PersonaGlobalSettings.value)` with `settingId = 18` (`manager-auto-approve-limit`) over the manager's `Users_Personas` (numeric values only, `REGEXP` filter) — `getManagerAutoApproveLimit()`.
93
+ 2. Order subtotal `<=` that limit. Subtotal = `SUM(SalesOrderItems.quantity * price)` (`_Model_Client_SalesOrder::_subtotal()`), never `_total`.
94
+ 3. Manager has NOT opted out: no `UserGlobalSettings` row `settingId = 19` (`manager-auto-approve-opt-out`) with value `'1'` — `hasManagerOptedOutOfAutoApprove()`.
89
95
 
90
- Evaluated in exactly three places, all deriving the manager from a supervisor relationship:
91
- - **(a) Order creation** — `_Model_Compass_SalesOrder::postPost`, inside `elseif (!empty($managerUserUuid))`, where `$managerUserUuid` comes from requester's `Users.supervisorUserId`.
92
- - **(b) "Order for" / contact change** — `_Model_Compass_SalesOrder::postPut`, only when the PUT payload changes `contact`; re-derives the manager from the new contact's supervisor.
93
- - **(c) Manager reassignment** — `_Model_Compass_ApprovalDecision::handleManagerReassignment`, wired into `postPut` **only** (added by TRUE-78851). Writes log notes `"Manager reassigned — approval decision reset"` (non-VIP) or `"Manager approval skipped due to VIP logic"` (VIP auto-approve).
96
+ Constants `SETTING_ID__MANAGER_AUTO_APPROVE_LIMIT = 18` / `SETTING_ID__MANAGER_AUTO_APPROVE_OPT_OUT = 19` on `_Model_Compass_ApprovalDecision`. Note text from `buildManagerAutoApproveNote()` is unchanged (`"Auto-approved: Manager is VIP and order total <= 5000"`, amount now read from the setting).
97
+
98
+ **Replaced** the old rule `Users.c_isVip = 1 AND subtotal <= _Model_Compass_SalesOrder::AUTO_APPROVE_IF_MANAGER_IS_VIP_AND_UNDER_AMOUNT (5000)` — that constant is **removed**; do not reintroduce `c_isVip` here. Why: a VIP manager (`Client_Compass` user **60817**) wanted to approve every order himself, but `c_isVip` cannot be cleared (it comes from the HR people file and other teams use it).
99
+
100
+ Data (all ids identical in prod, dev-sandbox, client-sandbox):
101
+ - `dbchanges2/Core/2026-10-09a - ManagerAutoApproveSettings.sql` — `Core.Settings` ids 18/19, pinned + guarded.
102
+ - `dbchanges2/Client_Compass/2026-10-09a - ManagerAutoApproveSettingsSeed.sql` — `PersonaGlobalSettings` persona **35** (VIP Role) / setting 18 = `'5000'`; `UserGlobalSettings` user 60817 / setting 19 = `'1'`. Literal ids, no Core reference.
103
+ - **Release order: Core SQL → Client_Compass SQL → deploy `_underscore`.** Code first = ALL VIP auto-approval stops until seeded.
104
+ - Persona 35 is a safe stand-in for `c_isVip`: PeopleFile grants it to every VIP and removes it when VIP is dropped (prod: 2269/2269 active VIPs hold it, no non-VIP does) — see [Persona model](persona-model-and-levy-gating.md). Replay of the last 5,184 prod manager decisions: old rule 1,663 auto-approved, new 1,662; the only difference is user 60817.
105
+ - To change a limit: edit the persona's setting-18 row (or seed one on another persona). To exempt one manager: add their setting-19 = `'1'` row.
106
+
107
+ Evaluated in exactly three places, all calling the shared check:
108
+ - **(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
+ - **(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
+ **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.
94
113
 
95
114
  ### Who becomes the step-2 manager: delegate note first, supervisor otherwise
96
115
  `_Model_Compass_SalesOrder` resolves the step-2 (Manager) manager two ways:
@@ -155,9 +174,9 @@ When a step-2 (Manager) decision is reassigned, `_swapManagerEmailAddress()` upd
155
174
  - **⚠ A pure reassignment notifies nobody.** Changing only `ApprovalDecisions.assignedToUserId` sends no email: `handleApprovalDecision()` returns early when `isApproved` is absent from the payload, and `handleAssignmentNotification()` fires only for step 1. The manager approval-request email is triggered when the admin approves step 1, not when an assignment changes. Any tool/backfill/admin action that repoints a manager must send its own notification.
156
175
  - **`_evaluateManagerVip()` silently no-ops when the order has no requester `Users` row.** Runs `INNER JOIN Users ON Users.contactId = SalesOrders.contactId`; with nothing linked to the order's contact it returns zero rows and returns without evaluating VIP. Pre-existing — a second distinct way (alongside the no-supervisor POST-path gap below) for an order to never get VIP auto-approve considered.
157
176
  - **Stale supervisor links are the bigger source of stuck approvals — and the supervisor path does no `isActive` check.** Prod 2026-08-12: **869 active users have a `supervisorUserId` pointing at an inactive `Users` row**, and only **2** of those supervisors have a live twin account. The delegate/duplicate problem at the same moment: **77 open step-2 decisions assigned to inactive users** — only **11** automatically resolvable (8 by matching `c_hrEmpPersonnelNbr`, 3 more by email), the other **66** managers who left with no live record. Read them differently: *same human, new account* is mechanically fixable; *departed manager* needs a human to name the replacement. (Counts are point-in-time.)
158
- - **VIP auto-approve bypassed when the requester has no supervisor (POST-path gap).** All three VIP triggers are supervisor-derived. When a requester has `Users.supervisorUserId = NULL`, order creation skips the manager branch (no step-2 decision, no manager). If an admin then manually adds a manager, that CREATES the step-2 decision as a POST to `/approval-decisions` → `_Model_Compass_ApprovalDecision::postPost`, which calls only `logApprovalDecision` + `handleApprovalDecision` — neither contains VIP auto-approve logic. So a VIP manager under $5000 added to a no-supervisor order never auto-approves and must be manually overridden. TRUE-78851 wired `handleManagerReassignment` into `postPut` only, never the first assignment (POST).
177
+ - **VIP auto-approve bypassed when the requester has no supervisor (POST-path gap).** All three VIP triggers are supervisor-derived. When a requester has `Users.supervisorUserId = NULL`, order creation skips the manager branch (no step-2 decision, no manager). If an admin then manually adds a manager, that CREATES the step-2 decision as a POST to `/approval-decisions` → `_Model_Compass_ApprovalDecision::postPost`, which calls only `logApprovalDecision` + `handleApprovalDecision` — neither contains VIP auto-approve logic. So a manager with a setting-18 limit, added to a no-supervisor order under that limit, never auto-approves and must be manually overridden. TRUE-78851 wired `handleManagerReassignment` into `postPut` only, never the first assignment (POST).
159
178
  - **Diagnostic signature:** absence of both `handleManagerReassignment` log notes in `Logs_<Tenant>.Record`, plus a step-2 decision whose `approvalDecisionTypeId = NULL` and `decidedByUserId` = the admin (not the manager). Order event log = logs cluster schema `Logs_Compass` (per-client `Logs_<Tenant>`), table `Record`, columns `(recordId, primaryKeyId, userId, timestamp, note)`; SalesOrder `RECORD_ID = 14`, `ApprovalDecisions` = **179**. Full manager-assignment history reconstructable from these tables — including telling an in-app change from a direct DB edit: see [Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
160
- - Real incident: order SA134461 — VIP manager Spyros Gravas, subtotal $926.47 (both auto-approve conditions met) still required manual override because the requester had no supervisor.
179
+ - Real incident: order SA134461 — VIP manager Spyros Gravas, subtotal $926.47 (auto-approve conditions met) still required manual override because the requester had no supervisor.
161
180
  - **⚠ The `!$hasManager` branch strands orders in an unrecoverable decided-but-untyped state — worst of the no-supervisor family.** When the "Order for" contact is changed to a user with no `Users.supervisorUserId`, `_Model_Compass_SalesOrder::postPut`'s `!$hasManager` branch (`Model/Compass/SalesOrder.php` ~L1018) PUTs `assignedToUser`, `decidedByUser`, `isApproved`, `approvalDecisionType`, `note` **and** `dtDecision` all to `null` — deliberately wiping an existing VIP auto-approval. A later manual approval sets `isApproved = 1` + `decidedByUserId` but never stamps `approvalDecisionTypeId` (correct — not a *system* decision). End state: `isApproved = 1`, `approvalDecisionTypeId = NULL`, and frequently a stale "Auto-approved: Manager is VIP and order total <= 5000" note.
162
181
  - **Frontend consequence (makes it unrecoverable):** blox derives `isPreApproved` from `!!stage.ApprovalDecisionTypes.name`, so it is `false`. The approval modal locks the stage's inputs and hides the Stage Actions block containing the only "Reset to Pending" control — no admin action can recover. This is why the 2026-08-31 frontend fix gates on `stepOneUndecided` rather than `isPreApproved` (see [Action-Button Rule Engine](../../../2.0/apps/toga25-supply/features/action-button-rule-engine.md)).
163
182
  - **Diagnostic signature in order activity log**, in order: `"Manager approval skipped due to VIP logic"` (creation-time VIP auto-approve), then `"Manager assignment cleared: new user has no supervisor"` AND `"VIP auto-approval removed: new user has no supervisor"` (written only by that `!$hasManager` branch), then `"Manager Approved by <admin>"` (manual approval).
@@ -177,9 +196,11 @@ When a step-2 (Manager) decision is reassigned, `_swapManagerEmailAddress()` upd
177
196
  - **⚠ 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.
178
197
  - **⚠ 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).
179
198
  - **⚠ 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.
199
+ - **⚠ 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).
180
200
  - **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.
181
201
 
182
202
  ## Change history
203
+ - 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)
183
204
  - 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)
184
205
 
185
206
  ## Related
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-05
9
+ updated: 2026-10-09
10
10
  owners: [bala, ajean, mhammontree]
11
11
  files:
12
12
  - worker2/Worker/Client/Compass/PeopleFile.php
@@ -51,7 +51,7 @@ below.
51
51
  | Base (was "All") | **1** | The **9** kits Levy and non-Levy both hold (81, 82, 84, 85, 105, 115, 120, 165, 166), plus the 71 items / 11 categories Levy also holds. **Every US user holds it.** |
52
52
  | Levy Purchaser | **24** | Levy's own **5** kits: 189, 193, 213, 231, 242. |
53
53
  | Non-Levy | **resolved by name** — **id 40, number 42 in prod** | The other 14 persona-1 kits / 123 items / 3 categories. Granted to non-Levy users only. |
54
- | VIP | 35 | VIP add-on. |
54
+ | VIP | 35 | VIP add-on. Carries the manager auto-approve limit (`PersonaGlobalSettings` setting 18 = `'5000'`) — see [Approval-Decision Flow](approval-decision-flow.md). |
55
55
  | CDL (MacBook / Surface) | 30 | `@compassdigital.io` + VIP users. |
56
56
 
57
57
  **Never hardcode the Non-Levy id, and never assume it matches the number.** In prod it is **id 40**
@@ -184,6 +184,7 @@ Both are **latent, not live, and not yet fixed** — each needs its own ticket.
184
184
  constants from this file into tickets, docs, or chat transcripts.
185
185
 
186
186
  ## Change history
187
+ - 2026-10-09 — Persona 35 now carries the manager auto-approve limit (setting 18); PeopleFile keeps it in exact sync with `c_isVip` (prod 2269/2269), so do not decouple the two (TRUE-82608). (bala)
187
188
  - 2026-10-05 — Linked the deferred
188
189
  [corporate vs field laptop split](./corporate-vs-field-laptop-personas.md) (Corporate attached at
189
190
  sector `J00000` via `Locations_Personas`; Field assigned here in `PeopleFile`, mirroring the Levy
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: workflow
7
7
  status: active
8
- updated: 2026-09-16
8
+ updated: 2026-10-09
9
9
  owners: ["jcardinal", "bala", "dfranks"]
10
10
  files: []
11
11
  related:
@@ -32,7 +32,8 @@ Compass customer SO (SalesOrders.customerId = 2, number SA/MA/MR)
32
32
  --PurchaseOrders_SalesOrders--> Agilant/NetSuite SO (customerId = 3)
33
33
  ```
34
34
  - **SA** orders are user-created in Compass then sent to MITS; **MR** orders are created retroactively from the inbound MITS PO (they do not originate as Compass-entered SOs); MA exists too. Structurally MR uses the same bridges as SA.
35
- - One Compass SO item can split into multiple PO items (bundles / `c_isConfiguration`, `parentSalesOrderItemId`, `bundleId`).
35
+ - One Compass SO item can split into multiple PO items (bundles / `c_isConfiguration`, `parentSalesOrderItemId`). Kit lines link via `SalesOrderItems.bundleItemId → BundleItems.bundleId`; `SalesOrderItems.bundleId` is effectively unused — never query kits by it.
36
+ - `Client_Compass` order numbers `29xxxx` and `45`–`48xxxxxxxxx` are the Office Depot / Agilant side copies (`customerId` 1 / 3), not commerce orders — exclude them when counting Compass orders.
36
37
 
37
38
  ## Steps
38
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.997",
3
+ "version": "1.0.998",
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",