toga-ai 1.0.354 → 1.0.356

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: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-13
9
+ updated: 2026-07-16
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/mvc/talos/kb-documents/get.php
@@ -97,36 +97,100 @@ false success.
97
97
  ### `POST /talos/knowledge-bases` — create a new KB (synchronous provisioning)
98
98
 
99
99
  Admin action that provisions a brand-new Talos knowledge base end to end in **one
100
- synchronous request**: creates the S3 prefix, creates the Bedrock KB and **waits for it to
101
- reach `ACTIVE`**, creates the Bedrock data source (`MANAGED_KNOWLEDGE_BASE_CONNECTOR`), and
102
- inserts rows across **5 systems with no shared transaction**: `Team.KnowledgeBases` (`db_team`,
103
- Team MySQL cluster), `Client_True.VectorIndexes` (`db_true`, **Client MySQL cluster** — not the
104
- Team cluster), and 2 Talos **Postgres** rows (`public.knowledge_bases` +
105
- `public.assistant_knowledge_bases`, the latter linking a hard-coded assistant id). Step order:
106
- validate name (≤255) + derive slug via **`App_Talos_S3::slugify()`** (kebab-case, `iconv` ASCII
107
- translit; rejects a duplicate slug — `slug` is UNIQUE); create 3 S3 folder markers
108
- `development-team/{slug}/{approved,archive,upload}/` via **`App_Talos_S3::putFolderMarker()`**;
109
- create the Bedrock KB and wait for `ACTIVE`; create the data source; then the DB inserts. This flow
110
- **stays synchronous by design** — moving it to worker2/SQS async was explicitly rejected;
111
- the fix path is to raise timeouts, never to background the work.
100
+ synchronous request**. The KB is a **classic VECTOR knowledge base backed by Amazon S3
101
+ Vectors** (the canonical design — see the architecture note below), and the flow is now
102
+ **9 steps** across systems with **no shared transaction**:
103
+
104
+ 1. **validate slug** (≤255) + derive via **`App_Talos_S3::slugify()`** (kebab-case, `iconv`
105
+ ASCII translit; rejects a duplicate slug — `slug` is UNIQUE).
106
+ 2. **S3 folders** — 3 markers `development-team/{slug}/{approved,archive,upload}/` via
107
+ **`App_Talos_S3::putFolderMarker()`**.
108
+ 3. **S3 Vectors store** — create a dedicated vector bucket + index via
109
+ `App_Talos_Bedrock::createVectorStore()`, capturing the authoritative **`indexArn`**.
110
+ 4. **VECTOR KB** — `createKnowledgeBase($bedrock, $slug, $indexArn)` (VECTOR + `S3_VECTORS`
111
+ storage).
112
+ 5. **wait for `ACTIVE`** (`waitForKnowledgeBaseActive`).
113
+ 6. **S3 data source** — plain `S3` connector + `BEDROCK_DATA_AUTOMATION` parsing.
114
+ 7. **`Team.KnowledgeBases`** (`db_team`, Team MySQL cluster).
115
+ 8. **`Client_True.VectorIndexes`** (`db_true`, **Client MySQL cluster** — not the Team cluster).
116
+ 9. **2 Talos Postgres rows** (`public.knowledge_bases` + `public.assistant_knowledge_bases`,
117
+ the latter linking a hard-coded assistant id).
118
+
119
+ The create branch sets `set_time_limit(CREATE_MAX_EXECUTION_SECONDS = 600)` up top because the
120
+ flow blocks on multi-minute AWS waits (S3 Vectors index readiness + KB `ACTIVE`, up to ~300s).
121
+ This only prevents the PHP fatal — it does **not** lift the EB Apache `ProxyTimeout` ceiling
122
+ (handled separately in `.platform`, see below). This flow **stays synchronous by design** —
123
+ moving it to worker2/SQS async was explicitly rejected; the fix path is to raise timeouts,
124
+ never to background the work.
125
+
126
+ ### Bedrock KB architecture — VECTOR + S3 Vectors (canonical)
127
+
128
+ The canonical Talos KB config (from **`development-team-dunn`**, confirmed identical across
129
+ `development-team-ninja-rmm` and `development-team-warehouse`) is a **classic VECTOR** KB, **not**
130
+ the fully-MANAGED type:
131
+ - **KnowledgeBase:** `knowledgeBaseConfiguration.type = VECTOR`;
132
+ `vectorKnowledgeBaseConfiguration.embeddingModelArn =
133
+ arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0`;
134
+ `embeddingModelConfiguration.bedrockEmbeddingModelConfiguration.embeddingDataType = FLOAT32`;
135
+ `storageConfiguration.type = S3_VECTORS` pointing at the per-KB S3 Vectors index ARN.
136
+ - **S3 Vectors index** (one dedicated vector bucket + index **per KB**): `dimension = 1024`,
137
+ `dataType = float32`, `distanceMetric = euclidean`,
138
+ `metadataConfiguration.nonFilterableMetadataKeys = [AMAZON_BEDROCK_TEXT, AMAZON_BEDROCK_METADATA]`,
139
+ encryption `sseType = AES256` (SSE-S3). Vector bucket name mirrors the console:
140
+ `bedrock-knowledge-base-{slug}`; index name `bedrock-knowledge-base-default-index`.
141
+ - **DataSource:** `dataSourceConfiguration.type = S3` (plain S3 connector);
142
+ `s3Configuration.bucketArn = arn:aws:s3:::togaiq`;
143
+ `inclusionPrefixes = ["development-team/{slug}/approved/"]`;
144
+ `vectorIngestionConfiguration.parsingConfiguration.parsingStrategy = BEDROCK_DATA_AUTOMATION`;
145
+ `dataDeletionPolicy = DELETE`.
146
+
147
+ Account `654654170868`, region `us-east-1`. Verified working end-to-end in production
148
+ (2026-07-16).
112
149
 
113
150
  The handler is **best-effort with no rollback**: on a mid-flow failure it stops and returns
114
151
  a `steps[]` report **without tearing down** already-created AWS resources (see Gotchas —
115
152
  orphaned KB).
116
153
 
117
- **`App_Talos_Bedrock`** (`_/app/talos/bedrock.php`) — Bedrock Agent client:
154
+ **`App_Talos_Bedrock`** (`_/app/talos/bedrock.php`) — Bedrock Agent + S3 Vectors clients:
155
+ - **`ROLE_ARN`** — the single **shared** execution role
156
+ `arn:aws:iam::654654170868:role/AmazonBedrockExecutionRoleForKnowledgeBase-talos`, hard-coded
157
+ (replaced the old per-KB `_tonej` role). See the IAM prerequisite below.
158
+ - `s3VectorsClient()` — `Aws\S3Vectors\S3VectorsClient`, api version `2025-07-15`, using the same
159
+ `[talos]` credential resolution as `App_Talos_S3::client()`.
160
+ - `vectorBucketName()` — derives `bedrock-knowledge-base-{slug}` and enforces the S3 Vectors
161
+ bucket-name length limit (3–63 chars).
162
+ - `createVectorStore()` — creates the vector bucket + index sized to Titan v2 (dim 1024,
163
+ float32, euclidean), then **polls `getIndex` for the authoritative index ARN** with a short
164
+ `NotFound` retry; returns an actionable error on `Conflict`/`AlreadyExists` from an orphaned
165
+ prior attempt.
166
+ - `createKnowledgeBase($bedrock, string $slug, string $indexArn)` — creates the VECTOR KB with
167
+ `S3_VECTORS` storage (signature now takes the index ARN).
118
168
  - `waitForKnowledgeBaseActive()` polls `getKnowledgeBase` every `WAIT_INTERVAL_SECONDS` (3s)
119
- up to **`WAIT_MAX_SECONDS` = 300** (raised from 90; MANAGED KBs were observed taking ~3 min
120
- to leave `CREATING`). Exits immediately on `ACTIVE`/`FAILED`, so fast activations are
121
- unaffected. 300s sits well under the 900s Apache/PHP ceiling.
122
- - `createDataSource()` — `connectorParameters` for `managedKnowledgeBaseConnectorConfiguration`
123
- is a **`Document` shape (free-form JSON)** in the AWS PHP SDK model and **must be passed as a
124
- native PHP array** so the SDK serializes a JSON *object*. Passing a pre-`json_encode()`d
125
- **string** fails `ValidationException "Invalid connector parameters format"`. Working object
126
- shape (matches the craftex data source):
127
- `{type:S3, filterConfiguration:{maxFileSizeInMegaBytes:"500",
128
- inclusionPrefixes:["development-team/{slug}/approved/"]}, connectionConfiguration:{bucketName:togaiq,
129
- bucketOwnerAccountId:654654170868, bucketArn:arn:aws:s3:::togaiq}, aclEnabled:false, version:"1"}`.
169
+ up to **`WAIT_MAX_SECONDS` = 300**. Exits immediately on `ACTIVE`/`FAILED`. 300s sits well
170
+ under the 900s Apache/PHP ceiling.
171
+ - `createDataSource()` — plain **`S3`** connector + `BEDROCK_DATA_AUTOMATION` parsing (rewritten
172
+ from the old `MANAGED_KNOWLEDGE_BASE_CONNECTOR`). See the architecture note above for the exact
173
+ shape.
174
+ - The vendored `aws/aws-sdk-php` (`^3.387`) already ships both the `S3Vectors` and `BedrockAgent`
175
+ clients and the `S3_VECTORS` `storageConfiguration` shape (verified against the SDK model). The
176
+ now-dead `BUCKET_NAME` / `BUCKET_OWNER_ACCOUNT_ID` constants were removed.
177
+
178
+ **IAM prerequisites (one-time, manual — the app does NOT provision IAM):**
179
+ - **Shared execution role** `AmazonBedrockExecutionRoleForKnowledgeBase-talos` — one role for
180
+ ALL Talos KBs (decision: do **not** create a role+policies per KB, which would require granting
181
+ the web app IAM write powers — rejected as a security expansion). Has 4 inline policies
182
+ mirroring dunn's but **wildcarded**: s3vectors data-plane on
183
+ `bucket/bedrock-knowledge-base-*/index/*`; `s3:GetObject` on
184
+ `togaiq/development-team/*/approved/*` (+ `s3:ListBucket` on `togaiq`); `bedrock:InvokeModel`
185
+ on `titan-embed-text-v2:0` (+ marketplace); BDA `GetDataAutomationStatus` +
186
+ `InvokeDataAutomationAsync`. Trust policy: `bedrock.amazonaws.com` assume-role with
187
+ `SourceAccount 654654170868` and `SourceArn knowledge-base/*`.
188
+ - **App credential grant** — the `[talos]` app credentials are IAM user
189
+ `arn:aws:iam::654654170868:user/bedrock-model-accessor`. It was granted (new inline policy
190
+ **`TalosKbProvisioning`**) `s3vectors:CreateVectorBucket/CreateIndex/GetIndex` and
191
+ `iam:PassRole` on the shared role. Its bedrock create actions were already covered by
192
+ `AmazonBedrockFullAccess` and S3 by `AmazonS3FullAccess`. **`s3vectors` is a SEPARATE service
193
+ NOT covered by `AmazonBedrockFullAccess`** — that was the real missing grant.
130
194
 
131
195
  ## Helpers
132
196
 
@@ -200,20 +264,23 @@ orphaned KB).
200
264
  - **php-fpm `request_terminate_timeout` can still kill the request.** If
201
265
  `/etc/php-fpm.d/www.conf`'s `request_terminate_timeout` is ever set to 60 it terminates the
202
266
  request regardless of Apache — it must be `0` or `>= 900`.
203
- - **`get-data-source` echoes `connectorParameters` back as a quoted STRING** even though the
204
- create input must be a JSON **object** — the GET representation is misleading and does NOT
205
- indicate the required input shape.
206
- - **KB create has no rollback → orphaned KB risk.** A failure at the data-source step leaves an
207
- orphaned Bedrock KB with **no DB rows** (`Team.KnowledgeBases`/`VectorIndexes`/Postgres never
208
- inserted). Blindly re-running with the same name creates a **duplicate** Bedrock KB (Bedrock
209
- allows same-name KBs with new ids). After a failed create: either
210
- `aws bedrock-agent delete-knowledge-base` the orphan and re-run clean, or finish the DB rows
211
- manually.
212
- - **Newest KBs use Bedrock's fully-MANAGED type** (`knowledgeBaseConfiguration.type=MANAGED`,
213
- `managedKnowledgeBaseConfiguration.embeddingModelType=MANAGED`) — **AWS owns the vector store**,
214
- so there is **no S3 Vectors index, no `embeddingModelArn`, and no `storageConfiguration`** to
215
- build. Config was replicated from the existing `development-team-craftex` KB. Account
216
- `654654170868`, region `us-east-1`, execution role `…ForKnowledgeBase_ofl5a`.
267
+ - **The Bedrock API does NOT auto-provision the S3 Vectors bucket/index** (the console does).
268
+ They must be created first (`s3vectors CreateVectorBucket` + `CreateIndex`) and the resulting
269
+ index ARN passed into `CreateKnowledgeBase`. `createVectorStore()` handles this before KB
270
+ creation.
271
+ - **`s3vectors` is a separate AWS service** — it is **not** covered by `AmazonBedrockFullAccess`.
272
+ The app credentials need explicit `s3vectors:*` grants (see IAM prerequisite above); this was
273
+ the real missing permission.
274
+ - **KB create has no rollback → orphaned KB risk.** A failure mid-flow can leave an orphaned
275
+ S3 Vectors bucket/index and/or Bedrock KB with **no DB rows** (`Team.KnowledgeBases`/
276
+ `VectorIndexes`/Postgres never inserted). Blindly re-running with the same slug hits
277
+ `Conflict`/`AlreadyExists` on the vector bucket (surfaced as an actionable error) or creates a
278
+ **duplicate** Bedrock KB (Bedrock allows same-name KBs with new ids). After a failed create:
279
+ delete the orphaned vector store + KB and re-run clean, or finish the DB rows manually.
280
+ - **Two prior KBs use the WRONG (fully-MANAGED) type.** `development-team-craftex` and
281
+ `development-team-toga-desk` were created by the earlier MANAGED code and were **intentionally
282
+ left as-is** (this session's scope was code-only). They should be **recreated later** to match
283
+ the canonical VECTOR + S3 Vectors design (dunn).
217
284
  - **KB-list source differs per page.** The new `/talos/knowledge-bases` page lists from
218
285
  `Team.KnowledgeBases` (`db_team`); `/talos/kb-documents` lists from live S3 folders. Don't
219
286
  assume one reflects the other.
@@ -222,15 +289,27 @@ orphaned KB).
222
289
  `Client_True.VectorIndexes` (Client cluster): `id, uuid`(UNIQUE)`, name, docSlug`(UNIQUE,
223
290
  nullable)`, sectionSlug, chunkSlug, description` — create sets `docSlug=sectionSlug=chunkSlug=
224
291
  bedrockKbId`.
225
- - **Not yet run end-to-end in production.** The whole feature is code-complete, `php -l` clean,
226
- and reviewed, but the create flow has not been exercised in prod pending the `pdo_pgsql`
227
- deploy plus network/IAM prerequisites (the developer is handling those).
292
+ - **Verified working end-to-end in production (2026-07-16)** with the VECTOR + S3 Vectors design.
228
293
  - No new secret literals introduced; credentials live in the config sections above (documented by
229
294
  section/account, never by value). AWS account id `654654170868`, bucket `togaiq`, and the KB
230
295
  execution-role ARN are non-secret resource identifiers.
231
296
 
232
297
  ## Change history
233
298
 
299
+ - 2026-07-16 — **Corrected the KB-create architecture from MANAGED to VECTOR + S3 Vectors**
300
+ (the canonical design, from `development-team-dunn`; the earlier code was modeled on the WRONG
301
+ `craftex`/MANAGED KB). Rewrote `App_Talos_Bedrock`: KB is now `VECTOR` + `S3_VECTORS` storage;
302
+ added `s3VectorsClient()`, `vectorBucketName()`, and `createVectorStore()` (create vector
303
+ bucket + index sized to Titan v2, poll `getIndex` for the ARN); `createKnowledgeBase()` now
304
+ takes the index ARN; `createDataSource()` rewritten to plain `S3` + `BEDROCK_DATA_AUTOMATION`;
305
+ removed dead `BUCKET_NAME`/`BUCKET_OWNER_ACCOUNT_ID`. Wired a new **S3 Vectors provisioning
306
+ step** into `post.php` before KB creation (flow is now 9 steps) and added
307
+ `set_time_limit(CREATE_MAX_EXECUTION_SECONDS=600)`. **Decided on ONE shared IAM execution role**
308
+ (`…ForKnowledgeBase-talos`, hard-coded `ROLE_ARN`) instead of per-KB roles, to avoid granting
309
+ the app IAM write powers; granted the `[talos]` user (`bedrock-model-accessor`) the missing
310
+ `s3vectors` + `iam:PassRole` permissions (`s3vectors` is NOT in `AmazonBedrockFullAccess`).
311
+ **Verified working end-to-end in production.** `craftex` and `toga-desk` remain on the old
312
+ MANAGED type (left as-is; recreate later). (jcardinal)
234
313
  - 2026-07-13 — Added the **`/talos/knowledge-bases` list + inline-rename page** (`get.php`;
235
314
  lists from `Team.KnowledgeBases`, rename-only, no delete, existence-check before update),
236
315
  registered first in the `talos-kb` nav group. Added the **`App_Pg`** tools-only PDO `pgsql`
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 32 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 33 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 11 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -14,11 +14,12 @@ project: _Underscore
14
14
  client: compass-canada
15
15
  type: profile
16
16
  status: active
17
- updated: 2026-06-30
17
+ updated: 2026-07-16
18
18
  owners: [jcardinal, bala, tcox, apeterson]
19
19
  files: []
20
20
  related:
21
21
  - ../compass-usa/profile.md
22
+ - ../compass-usa/features/approval-decision-flow.md
22
23
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
23
24
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
24
25
  - ../../2.0/apps/_underscore/features/surface-resolver.md
@@ -59,6 +60,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
59
60
  Compass USA's 1/3/4. See [Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
60
61
  - Customer language preference: `UserGlobalSettings.settingId = 2` (`en` / `fr-CA`); customer-
61
62
  facing emails are sent in EN or FR accordingly.
63
+ - **Approval decisions use the shared Compass parent.** `_Model_Compass_Canada_ApprovalDecision`
64
+ is an empty subclass of `_Model_Compass_ApprovalDecision`; the entire approval/notification/
65
+ manager-reassignment/VIP-auto-approve flow lives in the parent and branches at runtime on
66
+ `clientIdentifier === 'Compass_Canada'` (email-template UUID + EN/FR localization). Fixes to the
67
+ parent cover Canada automatically — no per-tenant change. See
68
+ [Approval-Decision Flow](../compass-usa/features/approval-decision-flow.md).
62
69
  - Assortment (product-grouping) names are served in fr-CA via the `AssortmentTranslations` sidecar
63
70
  (16 rows seeded). French was extracted from the old bilingual `"English/French"` `Assortments.name`
64
71
  values, which were then cleaned to English-only. See
@@ -2,6 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php |
5
6
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
6
7
  | [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
7
8
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Compass Approval-Decision Flow (Notifications & Manager Reassignment)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-07-16
10
+ owners: ["apeterson"]
11
+ files:
12
+ - _underscore/Model/Compass/ApprovalDecision.php
13
+ - _underscore/Model/Compass/Usa/ApprovalDecision.php
14
+ - _underscore/Model/Compass/Canada/ApprovalDecision.php
15
+ related:
16
+ - mr-ma-order-approval-and-status.md
17
+ - ../../compass-canada/profile.md
18
+ - ../../../2.0/apps/_underscore/features/email-template-sending.md
19
+ ---
20
+
21
+ ## Summary
22
+ Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**,
23
+ VIP auto-approve, and EN/FR localization — lives **entirely in the shared parent
24
+ `_Model_Compass_ApprovalDecision`**. Both tenant subclasses,
25
+ `_Model_Compass_Usa_ApprovalDecision` and `_Model_Compass_Canada_ApprovalDecision`, are **empty**
26
+ (`extends _Model_Compass_ApprovalDecision {}`, no overrides). Client- and language-specific
27
+ behavior is **not** subclassed; it branches at runtime inside the parent on
28
+ `$api->client->clientIdentifier` (`Compass_Usa` vs `Compass_Canada`). Practical consequence: a fix
29
+ to a parent method applies to **both** Compass US and Compass Canada automatically — there is no
30
+ per-client code change to make.
31
+
32
+ This doc covers the **approval-decision / notification** half of Compass approvals. The separate
33
+ auto-approval + `_status` gating on the order itself is in
34
+ [Compass MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md).
35
+
36
+ ## Key files / entry points
37
+ - **`_underscore/Model/Compass/ApprovalDecision.php`** — the shared parent that owns the whole
38
+ flow (notification-list maintenance, `_swapManagerEmailAddress()`, VIP auto-approve, email-template
39
+ UUID resolution, localization).
40
+ - **`_underscore/Model/Compass/Usa/ApprovalDecision.php`** — empty subclass, no overrides.
41
+ - **`_underscore/Model/Compass/Canada/ApprovalDecision.php`** — empty subclass, no overrides.
42
+
43
+ ## How it works
44
+ ### One shared parent, runtime client branch (not subclass overrides)
45
+ Compass US and Compass Canada resolve to different model classes at request time
46
+ (`_Model_Compass_Usa_ApprovalDecision` / `_Model_Compass_Canada_ApprovalDecision`), but both are
47
+ empty shells extending `_Model_Compass_ApprovalDecision`. All behavior is inherited. Where the two
48
+ tenants differ, the **parent** branches on `$api->client->clientIdentifier === 'Compass_Canada'`:
49
+ - **Email-template resolution** — `resolveEmailTemplateUuid()` picks the tenant's template UUID.
50
+ - **EN/FR localization** — user language is read from `UserGlobalSettings` (`settingId = 2`;
51
+ `en` / `fr-CA`) so Canadian notifications go out in the recipient's language.
52
+
53
+ Because the divergence is a runtime branch and not an override, do **not** add per-client logic to
54
+ the empty subclasses — extend the shared parent and branch there if a genuine tenant difference is
55
+ needed.
56
+
57
+ ### Manager reassignment and the notification list
58
+ When a step-2 (Manager) approval decision is **reassigned** to a new manager,
59
+ `_swapManagerEmailAddress()` updates the order's CC/notification list in `SalesOrderEmailAddresses`:
60
+ it removes the outgoing manager's email and adds the incoming manager's. The one email that must
61
+ **never** be dropped is the **order requester's** — the person the order is for, resolved via
62
+ `SalesOrders.contactId → Users` (the requester's email is looked up with a parameterized
63
+ `SELECT Users.email FROM SalesOrders INNER JOIN Users ON Users.contactId = SalesOrders.contactId
64
+ WHERE SalesOrders.id = ?`). Before deleting the old-manager email, the method compares it against
65
+ both the new-manager email and the requester email and **skips the delete when it matches the
66
+ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), matching
67
+ `getFilteredCcEmails()`.
68
+
69
+ ## Gotchas / known issues
70
+ - **Never drop the order requester from notifications on manager reassignment.** If the outgoing
71
+ manager happens to also be the order requester (same email), deleting the old-manager address
72
+ from `SalesOrderEmailAddresses` would silently remove the requester from all order notifications.
73
+ `_swapManagerEmailAddress()` guards against this by comparing the old-manager email to the
74
+ requester's email (looked up via `SalesOrders.contactId → Users`) and skipping the delete when
75
+ they match. This was a live bug (fixed 2026-07-16).
76
+ - **Email comparisons must be case-insensitive.** Use `strcasecmp()`, not `!==` — the notification
77
+ list can hold the same address in different casing, and `getFilteredCcEmails()` already compares
78
+ case-insensitively. A strict `!==` comparison here previously let a casing mismatch defeat the
79
+ requester/new-manager guards.
80
+ - **Don't put tenant behavior in the empty subclasses.** `Usa`/`Canada` `ApprovalDecision` are
81
+ intentionally empty; the tenant branch lives in the parent on `clientIdentifier`.
82
+
83
+ ## Change history
84
+ - 2026-07-16 — Fixed `_swapManagerEmailAddress()` dropping the order requester from the order's
85
+ notification list (`SalesOrderEmailAddresses`) when a step-2 manager was reassigned and the
86
+ outgoing manager's email also belonged to the requester: now looks up the requester email via
87
+ `SalesOrders.contactId → Users` and skips the delete when they match; also switched the
88
+ old-vs-new and old-vs-requester comparisons to case-insensitive `strcasecmp()` (was `!==`) to
89
+ match `getFilteredCcEmails()`. Documented that the flow is the shared parent
90
+ `_Model_Compass_ApprovalDecision` (empty `Usa`/`Canada` subclasses, runtime `clientIdentifier`
91
+ branch), so the fix covers Compass Canada automatically. Shipped on branch TRUE-80244 (pushed;
92
+ not yet merged to `_beta`). (apeterson)
93
+
94
+ ## Related docs
95
+ - [Compass MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md) — the
96
+ auto-approval + `_status` gating half of Compass approvals (on `_Model_Compass_SalesOrder`).
97
+ - [Compass Canada](../../compass-canada/profile.md) — uses this identical flow; the runtime
98
+ `Compass_Canada` branch drives its EN/FR notification localization.
99
+ - [Email Template Sending](../../../2.0/apps/_underscore/features/email-template-sending.md) — the
100
+ shared engine the resolved approval-notification templates are sent through.
@@ -15,12 +15,13 @@ project: _Underscore
15
15
  client: compass-usa
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-07-15
18
+ updated: 2026-07-16
19
19
  owners: [jcardinal, bala, tcox, apeterson]
20
20
  files: []
21
21
  related:
22
22
  - features/asn-to-item-fulfillment.md
23
23
  - features/cost-centers.md
24
+ - features/approval-decision-flow.md
24
25
  - workflows/cross-kit-bundle-corruption.md
25
26
  - ../../2.0/apps/worker2/features/compass-vip-support-importer.md
26
27
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
@@ -75,6 +76,10 @@ separate, related client (see its own profile).
75
76
  - [VIP Support Importer (worker2)](../../2.0/apps/worker2/features/compass-vip-support-importer.md)
76
77
  — manual (Postman) worker2 action that reads Compass's quarterly VIP spreadsheet and sets
77
78
  `Users.c_supportedByUserId` (assigned support tech) per VIP.
79
+ - [Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) —
80
+ the shared `_Model_Compass_ApprovalDecision` parent (empty `Usa`/`Canada` subclasses; runtime
81
+ `clientIdentifier` branch) that owns approval notifications, manager reassignment, VIP
82
+ auto-approve, and EN/FR localization.
78
83
 
79
84
  ## Notes
80
85
  - **Order status is shipped-only (2026-06-30).** Compass imports all IF stages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.354",
3
+ "version": "1.0.356",
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",