toga-ai 1.0.145 → 1.0.147

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,8 +6,8 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-12
10
- owners: [jcardinal]
9
+ updated: 2026-06-19
10
+ owners: [jcardinal, mhammontree]
11
11
  files:
12
12
  - Core/
13
13
  - Client/
@@ -141,6 +141,36 @@ file inside `HISTORIC` running; **never** add new changes there.
141
141
  5. Never edit or re-date an already-applied file — add a new dated file instead. Never put new
142
142
  work in a `HISTORIC` folder.
143
143
 
144
+ ## Bulk data loads — one statement, not many
145
+
146
+ When a change inserts many rows (reference-data / code-table loads), write it as a **single
147
+ bulk `INSERT`**, not one `INSERT` statement per row. The database is billed per query and the
148
+ external executor runs each statement as its own round-trip, so N separate inserts cost N times
149
+ the round-trips (and query cost) of one batched statement. This is a standing team preference.
150
+
151
+ - Use one `INSERT ... VALUES (...),(...),...` or `INSERT ... SELECT` over a derived
152
+ (`UNION ALL`) row set.
153
+ - To stay **idempotent** without a unique key to `INSERT IGNORE` against, anti-join the source
154
+ set to the target and insert only the misses:
155
+
156
+ ```sql
157
+ INSERT INTO TargetTable (uuid, keyColumn, otherColumn)
158
+ SELECT
159
+ src.uuid,
160
+ src.keyColumn,
161
+ src.otherColumn
162
+ FROM (
163
+ SELECT '<uuid>' AS uuid, '<key>' AS keyColumn, '<value>' AS otherColumn
164
+ UNION ALL SELECT ...
165
+ ) AS src
166
+ LEFT JOIN TargetTable existing ON existing.keyColumn = src.keyColumn
167
+ WHERE
168
+ existing.id IS NULL;
169
+ ```
170
+
171
+ - Keep the single statement well under `max_allowed_packet` (64 MB default) — a few thousand
172
+ rows is comfortably fine.
173
+
144
174
  ## Relationship to the rest of 2.0
145
175
 
146
176
  `dbchanges2` is registered as a **2.0 core repo** (`role: core` in `registry.json`) — it is
@@ -34,6 +34,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
34
34
 
35
35
  ## Clients
36
36
 
37
+ - **AIG (Staples Protection Plan)** (`aig`) → [clients/aig/INDEX.md](clients/aig/INDEX.md)
37
38
  - **Compass Canada** (`compass-canada`) → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
38
39
  - **Compass USA** (`compass-usa`) → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
39
40
  - **Elite** (`elite`) → [clients/elite/INDEX.md](clients/elite/INDEX.md)
@@ -0,0 +1,6 @@
1
+ # Client: AIG (Staples Protection Plan) `aig`
2
+
3
+ | Doc | Framework | Summary | Files |
4
+ |-----|-----------|---------|-------|
5
+ | [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | AIG protection-plan entitlements arrive as `api2` V2 JSON POSTs. | _underscore/Model/Aig/Entitlement.php, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql |
6
+ | [AIG (Staples Protection Plan)](profile.md) | 2.0 | AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. | |
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: AIG Entitlement Intake & SaleItem Code Resolution
3
+ framework: "2.0"
4
+ project: API
5
+ client: aig
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-06-19
9
+ owners: ["mhammontree"]
10
+ files:
11
+ - _underscore/Model/Aig/Entitlement.php
12
+ - dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
13
+ related:
14
+ - 2.0/apps/api2/architecture.md
15
+ - 2.0/apps/dbchanges2/architecture.md
16
+ - 2.0/apps/_underscore/architecture.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ AIG protection-plan entitlements arrive as `api2` V2 JSON POSTs. Before the entitlement is
22
+ written, the `_Model_Aig_Entitlement::prePost` interceptor resolves the **sold warranty SKU**
23
+ (`saleItem.partNumber`) against `Client_Aig.Items` to set the `Entitlement.saleItemId` foreign
24
+ key and to attach fulfillment types/methods. If that part number has no matching `Items` row,
25
+ the FK can't resolve and intake fails with **"Missing AIG item ID."** Keeping
26
+ `Client_Aig.Items` populated with AIG's current sale-item catalog is what keeps intake working.
27
+
28
+ ## Key files / entry points
29
+
30
+ - **`_underscore/Model/Aig/Entitlement.php`** — `prePost(&$api, &$payload)` is the PRE-POST
31
+ API payload interceptor (named by the `prePost*` convention) that runs on entitlement intake.
32
+ - **`Client_Aig.Items`** — the catalog the interceptor looks up against (`partNumber` column).
33
+ - **`dbchanges2/Client_Aig/…`** — where new code batches are loaded (see *Uploading new codes*).
34
+
35
+ ## How it works
36
+
37
+ 1. AIG POSTs an entitlement payload (vendor `STS_001`) to the `api2` V2 API.
38
+ 2. `_Model_Aig_Entitlement::prePost` runs and:
39
+ - cleans a duplicated-firstName-in-lastName quirk on `contact`;
40
+ - if `payload->item` (the covered **device/unit**) has a `description` but no `itemCategory`,
41
+ sets `itemCategory.name = item.description`;
42
+ - reads `partNumber = payload->saleItem->partNumber` and runs
43
+ `SELECT id FROM Items WHERE partNumber = '{partNumber}' LIMIT 1` to get `$itemId`;
44
+ - uses `$itemId` to load fulfillment **types** (`Items_EntitlementFulfillmentTypes`) and
45
+ **methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload.
46
+ 3. The V2 engine resolves `Entitlement.saleItemId` (FK → `_Model_Client_Item`) from that same
47
+ `saleItem.partNumber`. **No matching `Items` row → unresolvable FK → "Missing AIG item ID".**
48
+ 4. `postPost` then emails the contact a Staples Protection Plan registration link
49
+ (`staplesprotection.togaview.com`).
50
+
51
+ ## Data model
52
+
53
+ `Client_Aig.Items` is **dual-purpose** — it holds two different kinds of row, distinguished by
54
+ whether `itemCategoryId` is set:
55
+
56
+ | Row kind | Example `partNumber` | `description` | `itemCategoryId` |
57
+ |---|---|---|---|
58
+ | **Sale item** (warranty SKU) | `ASI-2YG2`, `SP-2D-TAB3` | price-band plan, e.g. `2YR Product under $500: ($30-$49.99)` | **NULL** |
59
+ | **Unit/device item** | `157C`, `1159`, `5022` | device type, e.g. `Chromebook`, `Printer Inkjet` | set → `ItemCategories` |
60
+
61
+ - `Client_Aig.ItemCategories` is a **device-classification** table (`Laptop`, `Printer`,
62
+ `Tablet`) — it classifies the physical hardware (unit items), **not** the warranty sale
63
+ items. Sale items correctly carry `itemCategoryId = NULL`; do not try to assign them a
64
+ category (the price-band plan descriptions don't map to a device type).
65
+ - `ItemCategories.c_premiumTechSupportCategory` (enum `PC`/`Tablet`) is read by the AIG
66
+ ClosedClaims → NetSuite invoicing sync.
67
+
68
+ ### SaleItem code schemes
69
+
70
+ AIG migrated their sale-item coding. The catalog spans:
71
+ - **Legacy `SP-*`** — the original scheme (only a handful seeded historically).
72
+ - **New `ASI-*` / `SM-*`** — current STS_001 (Staples) scheme; the bulk of the catalog.
73
+ - **Numeric `SA` codes** (8-digit, e.g. `24664377`) — a separate program sheet in AIG's
74
+ spreadsheet, distinct part-number space (no collisions with the STS codes).
75
+
76
+ ## Uploading new codes (the recurring task)
77
+
78
+ When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Description`):
79
+
80
+ 1. Load it into `Client_Aig.Items` via a **`dbchanges2/Client_Aig/`** migration (dated
81
+ `YYYY-MM-DD<letter>` per the dbchanges2 contract).
82
+ 2. Insert minimally `uuid`, `partNumber`, `description` — everything else (`isActive`,
83
+ `isVisible`, `inventoryType=HYBRID`, timestamps) takes table defaults; **leave
84
+ `itemCategoryId` NULL** for sale items.
85
+ 3. Generate **fully-random UUIDs** (e.g. `uuid4`), never the time-based MySQL `UUID()` —
86
+ per the 2.0 standard. Bake literal UUIDs into the SQL since a migration can't call PHP's
87
+ `_String::generateUuid()`.
88
+ 4. Use a **single bulk `INSERT`** (see the dbchanges2 architecture rule), made idempotent with
89
+ an anti-join because `Items` has **no unique key on `partNumber`** (so `INSERT IGNORE`
90
+ can't help):
91
+ ```sql
92
+ INSERT INTO Items (uuid, partNumber, description)
93
+ SELECT src.uuid, src.partNumber, src.description
94
+ FROM ( SELECT '<uuid>' AS uuid, '<part>' AS partNumber, '<desc>' AS description
95
+ UNION ALL SELECT ... ) AS src
96
+ LEFT JOIN Items existing ON existing.partNumber = src.partNumber
97
+ WHERE existing.id IS NULL;
98
+ ```
99
+
100
+ ## Client variations
101
+
102
+ This is AIG-specific behavior (the `_Model_Aig_Entitlement` override); other clients do not run
103
+ this interceptor or use this dual-purpose Items pattern.
104
+
105
+ ## Gotchas / known issues
106
+
107
+ - **"Missing AIG item ID" = the sale-item code isn't in `Client_Aig.Items`.** This is the
108
+ classic symptom of AIG shipping new codes before the catalog is loaded. Fix = load the codes.
109
+ - **The `partNumber` lookup is raw-interpolated** (`WHERE partNumber = '{$partNumber}'`) — a
110
+ latent SQL-injection point in `_Model_Aig_Entitlement::prePost`. Payload data is partner-
111
+ supplied; should be escaped with `_Database::escape()` or parameterized. Not yet fixed
112
+ (TRUE-79534 was data-load only).
113
+ - **The lookup is not vendor-scoped** (`LIMIT 1` on `partNumber` alone). It relies on
114
+ `partNumber` being globally unique within `Items`; safe today because the code spaces don't
115
+ collide, but a future collision would silently resolve to the wrong item.
116
+ - **Unit/device item codes are a separate source.** The entitlement payload also carries
117
+ `entitlementUnits[].unit.item.partNumber` (e.g. `1167` "Tablet Accessories"); those device
118
+ codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
119
+ - **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
120
+ `INSERT IGNORE`.
121
+
122
+ ## Change history
123
+
124
+ - 2026-06-19 — Documented intake flow; loaded 2,200 new SaleItem codes (1,700 STS `ASI-*`/`SM-*`
125
+ + 500 unique numeric `SA`) into `Client_Aig.Items` via TRUE-79534, fixing "Missing AIG item ID"
126
+ on the new `ASI-*` scheme. Follow-up to TRUE-79441. (mhammontree)
127
+
128
+ ## Related docs
129
+
130
+ - [api2 architecture](../../../2.0/apps/api2/architecture.md) — the V2 intake engine + payload interceptors.
131
+ - [dbchanges2 architecture](../../../2.0/apps/dbchanges2/architecture.md) — migration naming + the bulk-insert rule.
@@ -0,0 +1,40 @@
1
+ ---
2
+ title: "AIG (Staples Protection Plan)"
3
+ framework: "2.0"
4
+ apps:
5
+ - _underscore
6
+ - api2
7
+ - dbchanges2
8
+ project: API
9
+ client: aig
10
+ type: profile
11
+ status: active
12
+ updated: 2026-06-19
13
+ owners: ["mhammontree"]
14
+ files: []
15
+ related:
16
+ - clients/aig/features/entitlement-intake.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. In the
22
+ 2.0 platform AIG is a database-level tenant (`Client_Aig` schema) with its own `_Model_Aig_*`
23
+ class overrides in `_underscore` (most pulling in NetSuite traits). Protection-plan
24
+ **entitlements** are pushed to TOGa via the `api2` V2 JSON API; on intake an interceptor
25
+ resolves the sold warranty SKU against `Client_Aig.Items`, and closed claims are invoiced
26
+ back to AIG through NetSuite.
27
+
28
+ The intake vendor on entitlement payloads is `STS_001` ("STS" = Staples); registration emails
29
+ go out under the Staples Protection Plan brand and link to `staplesprotection.togaview.com`.
30
+
31
+ ## Integration touchpoints
32
+
33
+ - **`api2`** — entitlement intake (`/v2` JSON), where the AIG payload is POSTed.
34
+ - **`_underscore`** — `_Model_Aig_*` overrides (Entitlement intake interceptor, Contact sync,
35
+ NetSuite item/SO/invoice traits).
36
+ - **`dbchanges2`** — `Client_Aig/` schema + reference-data migrations (e.g. the SaleItem code
37
+ catalog in `Client_Aig.Items`).
38
+
39
+ See [entitlement-intake.md](features/entitlement-intake.md) for how a payload becomes an
40
+ entitlement and how to load new sale-item codes.
@@ -2,4 +2,5 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
5
6
  | [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
@@ -0,0 +1,105 @@
1
+ ---
2
+ title: Grand & Toy ASN Import (Compass Canada)
3
+ framework: "2.0"
4
+ project: _Underscore
5
+ client: compass-canada
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-06-19
9
+ owners: ["bala"]
10
+ files:
11
+ - worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php
12
+ - worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php
13
+ - worker/schedules/cron.worker.sync.json
14
+ - _underscore/Model/Compass/AdvanceShippingNotice.php
15
+ - _underscore/Model/Compass/Canada/AdvanceShippingNotice.php
16
+ - dbchanges2/Client_CompassCanada/
17
+ related:
18
+ - ../profile.md
19
+ ---
20
+
21
+ ## Summary
22
+ Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. A 1.0 worker cron reads
23
+ G&T's ASN CSV from a mailbox, posts each shipped line to the 2.0 API as an AdvanceShippingNotice
24
+ (ASN); a Compass interceptor then auto-creates the ItemFulfillment chain, and the customer gets
25
+ an in-transit email in English or French. It mirrors the Compass USA ODP / Strategic-Systems ASN
26
+ flow, adapted for Canadian carriers and bilingual email.
27
+
28
+ ## Key files / entry points
29
+ - `worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php`
30
+ — the scheduled cron (1.0 worker tier). Reads mailbox `compasscanada.status@togatech.com`
31
+ (OAuth2 creds in config `[compasscanada]`), parses the 24-column CSV, posts to api2
32
+ `/advance-shipping-notices`. Scheduled in `worker/schedules/cron.worker.sync.json` (every 4h).
33
+ - `_underscore/Model/Compass/AdvanceShippingNotice.php` — `postPost` interceptor that builds the
34
+ ItemFulfillment chain on each ASN POST. Compass Canada inherits it via the empty
35
+ `_underscore/Model/Compass/Canada/AdvanceShippingNotice.php`.
36
+ - `worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php` — one-time
37
+ backfill variant: reads a CSV from disk (no mailbox/OAuth), creates ASN data only, sends NO
38
+ user emails/notifications, and stamps `c_dtInTransitEmailSent = NOW()`.
39
+
40
+ ## How it works
41
+ 1. Cron gets an O365 OAuth2 token (creds from `App_Registry::get('config')['compasscanada']`),
42
+ opens the INBOX.
43
+ 2. Per attachment: decode CSV, gate on exactly 24 columns (otherwise email the team the file +
44
+ skip).
45
+ 3. Per row: skip electronic-delivery carriers (`Digital` / `E-delivered` / `E-verified`);
46
+ validate PO / part / tracking; look up SO + contact user + CC addresses; resolve carrier →
47
+ Ground shipping method; upsert `TrackingNumbers`; resolve the item by
48
+ `VendorItems.vendorPartNumber`; find-or-create a `Units` row per serial.
49
+ 4. POST `/advance-shipping-notices` with the tracking number nested at header + item + unit
50
+ level (so the interceptor sees it during the POST).
51
+ 5. The Compass ASN `postPost` interceptor (when enabled) creates the `ItemFulfillment` (one per
52
+ SO number, reused if it exists), `ItemFulfillmentItems`, `ItemFulfillmentItemUnits`, and the
53
+ IF/IFI/IFIU `_TrackingNumbers` bridges.
54
+ 6. Only when a tracking number is newly inserted: write Notifications (1.0 `db_store` + 2.0),
55
+ resolve the user's language, send the EN/FR in-transit email via POST
56
+ `/email-templates/sendEmail`, then stamp `c_dtInTransitEmailSent = NOW()`.
57
+
58
+ ## Data model
59
+ - ASN: `AdvanceShippingNotices` / `…Items` / `…ItemUnits` + their `…_TrackingNumbers` bridges.
60
+ - IF: `ItemFulfillments` / `…Items` / `…ItemUnits` + their `…_TrackingNumbers` bridges.
61
+ - Vendor: `App_Client_CompassCanada::UUID_VENDOR__GRAND_TOY`.
62
+ - Carriers + methods: `ShippingCarriers` Precision / Purolator / ATSL / Nationex, each with its
63
+ own Ground `ShippingMethods` row (added 2026-06-18 via dbchanges2; tracking-URL prefixes +
64
+ carrier logos set on the carrier).
65
+ - Language: `UserGlobalSettings.settingId = 2`, values `en` / `fr-CA`. In-transit `EmailTemplates`
66
+ uuids — EN `26d2cb67-bdc5-4973-9e39-160c89b0af56`, FR `fb8be211-7c07-4939-9682-08b6c3245f66`.
67
+ - Interceptor wiring: `Core.RecordScripts` + `Core.ApiPayloadInterceptors`, `recordId 55`
68
+ (AdvanceShippingNotice). The `sendEmail` POST record script is `Core.RecordScripts recordId 202`.
69
+
70
+ ## Client variations
71
+ Compass-Canada-specific; parallels Compass USA's ODP / Strategic-Systems ASN import. Canada sends
72
+ a bilingual EN/FR in-transit email (USA is English only) and uses Canadian carriers.
73
+
74
+ ## Gotchas / known issues
75
+ - **CSV "Part Number" is the VENDOR part number (`VendorItems.vendorPartNumber`), NOT
76
+ `Items.partNumber`.** Resolve the item through `VendorItems`. (e.g. vendor `IMFP3260` →
77
+ `Items.partNumber` `C2CY5UC#ABA`.)
78
+ - **G&T's ASN file inconsistently drops the `IM` prefix** (`FP3260` vs `IMFP3260`) on some lines
79
+ → item-not-found → those lines are skipped and reported in the import-report email. This is a
80
+ G&T-side export issue; toga transmits the correct `IMFP…` to G&T as the cXML `SupplierPartID`
81
+ (see `2_transmit_mits_purchase_orders_to_vendors.php`), so it is NOT a MITS bug.
82
+ - **The ASN→IF interceptor must be ENABLED per client.** `Core.ApiPayloadInterceptors` for
83
+ `recordId 55`, `prePostProcessing=POST`, `httpMethod=POST` must be `isActive=1` (and the client
84
+ `AclRecordScripts` must grant the caller's roles). It was OFF for Compass Canada; enabling it is
85
+ required or ASNs post but the IF/IFI/IFIU chain never builds.
86
+ - **Tracking must be nested in the ASN POST payload** (header/item/unit), not linked via SQL
87
+ after the POST — the interceptor runs during the POST, so post-POST links are invisible to it.
88
+ - **`/email-templates/sendEmail` must be POST** (not GET) so a large `orderItems` HTML payload
89
+ does not hit the URL-length limit (HTTP 414). POST requires the `sendEmail` record script
90
+ registered for the POST method (`Core.RecordScripts`), else the engine returns `EV-8`.
91
+ - Each CSV line creates one ASN; the interceptor dedups to **one ItemFulfillment per SO**.
92
+ - A new carrier needs its **own** Ground `ShippingMethod`, or the TrackingNumber ends up with the
93
+ right carrier but another carrier's Ground method id.
94
+ - `App_Database::fetchOne()` (1.0) throws on a 0-row result — guard item lookups with `numRows()`
95
+ before `fetchOne()`.
96
+ - The cron reads OAuth2 creds from config `[compasscanada]`; the updated `config.<env>.ini` must
97
+ be deployed with the code or the token request fails.
98
+
99
+ ## Change history
100
+ - 2026-06-19 — Built the G&T ASN import cron + one-time backfill script; matched items by
101
+ `VendorItems.vendorPartNumber`; added Canadian carriers + per-carrier Ground methods; EN/FR
102
+ in-transit email via POST; enabled the Compass ASN→IF interceptor for Compass Canada. (bala)
103
+
104
+ ## Related docs
105
+ - [Compass Canada profile](../profile.md)
@@ -12,7 +12,7 @@ project: _Underscore
12
12
  client: compass-canada
13
13
  type: profile
14
14
  status: active
15
- updated: 2026-06-18
15
+ updated: 2026-06-19
16
16
  owners: [jcardinal, bala]
17
17
  files: []
18
18
  related:
@@ -31,10 +31,17 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
31
31
  - **1.0:** worker crons under `worker/crons/toga2/compasscanada/`.
32
32
 
33
33
  ## Vendors & integrations
34
- - Not yet captured. Confirm vendors / ASN ingestion paths before relying on them.
34
+ - **Grand & Toy (G&T)** — primary hardware vendor. SOs flow toga MITS PO to G&T; G&T sends
35
+ back ASNs. ASN ingestion (email CSV + the auto-created ItemFulfillment chain + bilingual
36
+ in-transit email) is documented in [Grand & Toy ASN Import](features/grand-and-toy-asn-import.md).
37
+ Vendor uuid: `App_Client_CompassCanada::UUID_VENDOR__GRAND_TOY`. Canadian carriers in use:
38
+ UPS, FedEx, Purolator, Precision, Nationex, ATSL (each with a Ground `ShippingMethod`).
39
+ - Worker crons for the G&T flow live under `worker/crons/toga2/compasscanada/workflow/`
40
+ (`1_…` transmit SOs to MITS, `2_…` transmit POs to vendors, `3_…` status from G&T cXML,
41
+ `4_…` import G&T ASNs).
35
42
 
36
43
  ## Notes
37
- - **Stub** created alongside the Compass USA profile for disambiguation. The 2026-06-08 ASN
38
- ItemFulfillment work was for **Compass USA**, not Compass Canada. Expand this profile as
39
- Compass Canada behavior is investigated.
44
+ - Customer language preference: `UserGlobalSettings.settingId = 2` (`en` / `fr-CA`); customer-
45
+ facing emails are sent in EN or FR accordingly.
46
+ - The 2026-06-08 ASN → ItemFulfillment work was for **Compass USA**, not Compass Canada.
40
47
  - Related: [Compass USA](../compass-usa/profile.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.145",
3
+ "version": "1.0.147",
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",