toga-ai 1.0.416 → 1.0.418

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.
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-07-09
10
- owners: ["dfranks", "bala"]
10
+ owners: ["dfranks", "bala", "jcardinal"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
13
13
  - worker/crons/toga2/netsuite/sync_togasupply_canon.php
@@ -196,8 +196,26 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
196
196
  excluded** from the NetSuite Sync Alert monitor — including it would generate permanent
197
197
  false-positive stale tickets.
198
198
 
199
+ - **`syncSalesOrderFromNetsuite` reconciled existing customer-PO lines by `uuid` ONLY → duplicate
200
+ PO lines on dual-catalog SKUs.** When a physical SKU exists as **two `Items` records across
201
+ catalogs** (same `partNumber`, different `uuid` — e.g. a customer-catalog `HYBRID` item vs. an
202
+ internal `SERIALIZED` item), the fulfillment SO item can resolve to the *other-catalog* `Item`
203
+ than the line already on the PO. The uuid-only match (and uuid-only retrieval) misses the sibling
204
+ line, so the importer **appends a duplicate PO line** backed by an auto-created **"shadow"
205
+ VendorItem** (`vendorPartNumber` = SKU literal, description NULL, cost 0). Observed on Compass
206
+ (`vendorId=1` Office Depot): 4,886 duplicate lines across 1,320 POs. **Fix (2026-07-22):** added
207
+ a `partNumber` (SKU) fallback retrieval + lookup, and gated VendorItem creation to fire only when
208
+ a new line is actually created. Full root cause, the prod cleanup migration, and the
209
+ deploy-fix-before-cleanup sequencing live in the
210
+ [Compass ODP Duplicate PO-Line Cleanup](../../../clients/compass-usa/workflows/odp-duplicate-po-line-cleanup.md).
211
+
199
212
  ## Change history
200
213
 
214
+ - 2026-07-22 — Recorded the **dual-catalog duplicate OD PO-line bug** in
215
+ `syncSalesOrderFromNetsuite` (uuid-only PO-line reconciliation → shadow-VendorItem duplicate
216
+ lines) and its SKU-fallback fix. Compass cleanup + sequencing captured in the
217
+ [Compass ODP Duplicate PO-Line Cleanup](../../../clients/compass-usa/workflows/odp-duplicate-po-line-cleanup.md).
218
+ Code + migration uncommitted, pending deploy. (jcardinal)
201
219
  - 2026-07-20 — Fixed a pre-existing **missing `try`-close / brace bug** in
202
220
  `common_sync_togasupply.php` (the inventory-adjustments and item-fulfillments blocks lacked a
203
221
  `try` close, a parse error that broke the Compass sync run). Same failure class as the 2026-07-09
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 13 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 14 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 15 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
10
10
  - **togadesk** (TOGa Desk) — 9 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -11,5 +11,6 @@
11
11
  | [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
12
12
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
13
13
  | [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
14
+ | [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
14
15
  | [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, library/app/client/compass.php |
15
16
  | [Compass Order Lifecycle & Data-Integrity Invariants](workflows/order-lifecycle-and-data-integrity.md) | 2.0 | End-to-end map of how a Compass order flows through the `Client_Compass` (2.0) database and the **expected raw-data shape** at each link/ASN/IF level. | |
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-17
10
- owners: ["apeterson", "dfranks"]
9
+ updated: 2026-07-23
10
+ owners: ["apeterson", "dfranks", "bala"]
11
11
  files:
12
12
  - _underscore/Model/Compass/ApprovalDecision.php
13
13
  - _underscore/Model/Compass/SalesOrder.php
@@ -50,11 +50,35 @@ tenants differ, the **parent** branches on `$api->client->clientIdentifier === '
50
50
  - **Email-template resolution** — `resolveEmailTemplateUuid()` picks the tenant's template UUID.
51
51
  - **EN/FR localization** — user language is read from `UserGlobalSettings` (`settingId = 2`;
52
52
  `en` / `fr-CA`) so Canadian notifications go out in the recipient's language.
53
+ - **Web-link host resolution** — `_Model_Compass_SalesOrder::resolveSupplyHost()` /
54
+ `resolveCommerceHost()` pick the tenant's supply/commerce domain for the links embedded in
55
+ approval and order-status emails (see below).
53
56
 
54
57
  Because the divergence is a runtime branch and not an override, do **not** add per-client logic to
55
58
  the empty subclasses — extend the shared parent and branch there if a genuine tenant difference is
56
59
  needed.
57
60
 
61
+ ### Email-link host resolution (US vs Canada domains)
62
+ The "Review Order" link (togasupply) and the user order-details link (togacommerce) embedded in
63
+ Compass approval and order-status emails are built from a **client-aware host resolver** on the
64
+ shared parent `_Model_Compass_SalesOrder`, so the link always points at the domain that actually
65
+ holds the order:
66
+ - Four host constants: `HOST_SUPPLY__COMPASS_US = 'compass.togasupply.com'`,
67
+ `HOST_SUPPLY__COMPASS_CANADA = 'compasscanada.togasupply.com'`,
68
+ `HOST_COMMERCE__COMPASS_US = 'compass.togacommerce.com'`,
69
+ `HOST_COMMERCE__COMPASS_CANADA = 'compasscanada.togacommerce.com'`.
70
+ - Public static `resolveSupplyHost(&$api)` / `resolveCommerceHost(&$api)` return the correct host;
71
+ private static `isCompassCanadaClient(&$api)` returns
72
+ `($api->client->clientIdentifier ?? '') === 'Compass_Canada'`. Anything not Canada falls back to
73
+ the **US** host, so US behavior is unchanged.
74
+ - All 8 link-building sites build the URL as `"https://" . resolveSupplyHost($api) . "/?..."` (only
75
+ the host varies; the query string / uuid is identical). Sites wired: `ApprovalDecision.php` —
76
+ manager-approval-request block and VIP auto-approve block (`orderUrl` + `userOrderUrl` in each);
77
+ `SalesOrder.php` — admin-approved block (`postPut`), in-transit/delivered block, and
78
+ `_sendVipManagerNotification`.
79
+ - This reuses the **same `$api->client->clientIdentifier` signal** as the email-template language
80
+ resolver, so the link host and the email template can never disagree.
81
+
58
82
  ### VIP manager auto-approve — the rule and its three (all supervisor-derived) triggers
59
83
  A single rule governs VIP auto-approve: a **manager-stage (step 2)** approval auto-approves iff the
60
84
  assigned manager's `Users.c_isVip = 1` **AND** the order subtotal
@@ -114,8 +138,25 @@ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), mat
114
138
  requester/new-manager guards.
115
139
  - **Don't put tenant behavior in the empty subclasses.** `Usa`/`Canada` `ApprovalDecision` are
116
140
  intentionally empty; the tenant branch lives in the parent on `clientIdentifier`.
141
+ - **Never hardcode the web host in email links.** The email links previously used string literals
142
+ `compass.togasupply.com` / `compass.togacommerce.com` in the parent classes. Because the tenant
143
+ subclasses are empty, **Compass Canada orders inherited the US links** and sent Canada
144
+ recipients to the US supply/commerce site, which does not contain their order — so the link never
145
+ opened. Always build the host via `_Model_Compass_SalesOrder::resolveSupplyHost()` /
146
+ `resolveCommerceHost()`, never a literal. Real incident: order SAC100650 — manager Gary Berenz
147
+ could not open the order from the "Manager Approval Needed" email, so admin Kai Wong had to
148
+ approve as manager on his behalf. (Fixed 2026-07-23.)
117
149
 
118
150
  ## Change history
151
+ - 2026-07-23 — Fixed Compass approval/status **email links** being hardcoded to the US domains
152
+ (`compass.togasupply.com` / `compass.togacommerce.com`) in the parent classes, so Compass Canada
153
+ orders (empty `Canada` subclass) inherited US links and Canada recipients were sent to a site
154
+ without their order. Added a client-aware host resolver to `_Model_Compass_SalesOrder` (four
155
+ `HOST_*` constants, `resolveSupplyHost()`/`resolveCommerceHost()`, `isCompassCanadaClient()`
156
+ branching on `clientIdentifier === 'Compass_Canada'`, US fallback), and rewired all 8 link sites
157
+ in `ApprovalDecision.php` and `SalesOrder.php` to build the host through it — reusing the same
158
+ `clientIdentifier` signal as the email-template resolver so host and template always agree.
159
+ Verified both client contexts resolve correctly; `php -l` clean. Prod incident SAC100650. (bala)
119
160
  - 2026-07-17 — Documented the VIP manager auto-approve rule (VIP manager + subtotal ≤ $5000) and
120
161
  its three supervisor-derived triggers (`postPost` creation, `postPut` contact change,
121
162
  `handleManagerReassignment` on `postPut`), plus the **POST-path gap**: a requester with no
@@ -24,6 +24,7 @@ related:
24
24
  - features/cost-centers.md
25
25
  - features/approval-decision-flow.md
26
26
  - workflows/cross-kit-bundle-corruption.md
27
+ - workflows/odp-duplicate-po-line-cleanup.md
27
28
  - ../../2.0/apps/worker2/features/compass-vip-support-importer.md
28
29
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
29
30
  - ../../2.0/apps/toga2-commerce/features/cart-bundle-submission-and-identity.md
@@ -65,6 +66,11 @@ separate, related client (see its own profile).
65
66
  - **Office Depot (ODP)** — vendor id 1. ASNs arrive via **cXML** (direct V2 API) and via the
66
67
  email cron's ODP CSV format.
67
68
  - **Strategic Systems** — ASNs via the email cron's SS CSV format (serial numbers → Units).
69
+ - **Dual-catalog SKU → duplicate OD PO lines (2026-07-22).** A physical SKU that exists as two
70
+ `Items` rows (same `partNumber`, different `uuid`) made `syncSalesOrderFromNetsuite` append
71
+ duplicate Office Depot PO lines backed by shadow VendorItems (4,886 dup lines / 1,320 POs).
72
+ Root cause, forward fix, and the `2026-07-22a` cleanup migration:
73
+ [ODP Duplicate PO-Line Cleanup](workflows/odp-duplicate-po-line-cleanup.md).
68
74
  - ASN ingestion entry points: cXML to the V2 API (logged in `Logs_Compass.Api`) and
69
75
  `worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php`.
70
76
 
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: compass-usa
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-07-22
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql
14
+ related:
15
+ - clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
16
+ - clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md
17
+ - clients/compass-usa/features/mits-po-to-so-item-linking.md
18
+ - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
19
+ - clients/compass-usa/profile.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on
25
+ Compass USA because it reconciled the fulfillment SO against the existing customer PO by matching
26
+ existing PO lines **only on `VendorItems.item.uuid`**. When one physical SKU exists as **two
27
+ `Items` records across catalogs** (same `partNumber`, different `uuid` — e.g. a customer-catalog
28
+ `HYBRID` item vs. an internal `SERIALIZED` item), the fulfillment item can resolve to the
29
+ *other-catalog* `Item` than the line already on the PO. The uuid match then misses (the uuid-only
30
+ retrieval never even loaded the sibling line), and a **duplicate PO line** is appended, backed by
31
+ an auto-created **"shadow" VendorItem** (`vendorPartNumber` = SKU literal, `vendorPartDescription`
32
+ NULL, `overridePoSubmissionIntegrationId` NULL, cost 0).
33
+
34
+ **Blast radius (prod `Client_Compass`):** 4,886 duplicate lines across 1,320 Office Depot POs.
35
+ Reference order: SO **SA133668** → OD PO **41342788-5125** (PO id 104382), duplicate lines
36
+ 179909 / 179910 / 179911.
37
+
38
+ This doc is both the durable **integration gotcha** (the reconciliation behavior) and the
39
+ **data-repair record + reusable pattern** for the one-time cleanup. The forward code fix and the
40
+ cleanup migration must ship **in the correct order** (see Sequencing).
41
+
42
+ ## Root cause (`syncSalesOrderFromNetsuite`, `library/app/api/toga2.php`)
43
+
44
+ In the existing-customer-PO branch of `syncSalesOrderFromNetsuite()`, the reconciliation loaded and
45
+ keyed existing PO lines **only by `vendorItem.item.uuid`**. A dual-catalog SKU (two `Items` rows,
46
+ same `partNumber`, different `uuid`) whose fulfillment item resolved to the sibling `Item` produced
47
+ no uuid hit → a new duplicate line + a shadow VendorItem, instead of re-linking the existing line.
48
+
49
+ ## The forward fix (already applied locally, uncommitted)
50
+
51
+ `syncSalesOrderFromNetsuite()` gained a **`partNumber` (SKU) fallback** in the same branch:
52
+ - Collect the relevant SKUs; a **second retrieval pass** fetches existing PO lines by
53
+ `Items.partNumber` and merges them (deduped) into the uuid lookup.
54
+ - Build a `partNumber`-keyed lookup; the match block **falls back to it** after the two uuid
55
+ lookups, so an existing same-SKU line is **reused (re-linked)** instead of duplicated.
56
+ - **VendorItem creation is gated to run ONLY when a new line is actually created** — a SKU match no
57
+ longer spawns a shadow VendorItem. (This was php-reviewer's critical regression check.)
58
+
59
+ `php -l` clean; php-reviewer verified. Neither this edit nor the migration was committed this
60
+ session — both are pending the developer's own deploy/commit.
61
+
62
+ ## The cleanup migration (`dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql`)
63
+
64
+ **Deletion set (precisely defined):** a shadow-VendorItem line (cost 0) that has a **different
65
+ non-shadow sibling** for the same `UPPER(partNumber)` on the **same PO**, on **`vendorId = 1`
66
+ (Office Depot) POs only**. Validated in prod: all cost 0, **zero** customer-SO
67
+ (`SalesOrderItems_PurchaseOrderItems`) links, and **zero** references from
68
+ `AdvanceShippingNoticeItems` / `BillItems` / `ItemReceiptItems`.
69
+
70
+ **FK deletion order (DELETE RESTRICT):** delete the two bridge tables
71
+ (`PurchaseOrderItems_SalesOrderItems`, `SalesOrderItems_PurchaseOrderItems`) **before**
72
+ `PurchaseOrderItems`.
73
+
74
+ **Migration properties:** stages target ids into an audit table, prints a sanity count, wraps the
75
+ deletes in a transaction, idempotent. `sql-reviewer` verified the logic and FK order.
76
+
77
+ **Explicitly EXCLUDED from cleanup (left for manual review):** 412 both-shadow PO-SKU pairs, 39
78
+ no-shadow pairs, the 3–4-shadow handful, non-Office-Depot vendors, and the shadow `VendorItems`
79
+ rows themselves.
80
+
81
+ ## Sequencing (critical)
82
+
83
+ **Deploy the code fix BEFORE running the cleanup.** Otherwise the next sync recreates the
84
+ duplicates. Once the fix is live, the corrected sync re-links fulfillment SO items to the surviving
85
+ lines by SKU.
86
+
87
+ ## Schema reference — tables that FK `Client_Compass.PurchaseOrderItems.id`
88
+
89
+ Relevant to any PO-item deletion under DELETE RESTRICT (verified against prod schema): 7 tables —
90
+ `AdvanceShippingNoticeItems`, `BillItems`, `ItemReceiptItems`,
91
+ `PurchaseOrderItems_SalesOrderItems`, `PurchaseOrderItems_TransferOrderItems`,
92
+ `SalesOrderItems_PurchaseOrderItems`, `TransferOrderItems_PurchaseOrderItems`. The TransferOrder
93
+ bridges are empty for this deletion set; the two SalesOrder bridges are the only ones the shadow
94
+ lines touch.
95
+
96
+ ## Change history
97
+ - 2026-07-22 — Documented the dual-catalog (same-`partNumber`, two-`uuid`) duplicate OD PO-line
98
+ bug in `syncSalesOrderFromNetsuite` (uuid-only reconciliation; auto-created shadow VendorItems;
99
+ 4,886 dup lines / 1,320 OD POs in prod), the forward SKU-fallback fix (second partNumber
100
+ retrieval + gated VendorItem creation), and the `2026-07-22a` cleanup migration (deletion set,
101
+ FK order, exclusions, deploy-fix-before-cleanup sequencing). Reference: SA133668 → OD PO
102
+ 41342788-5125. Code + migration uncommitted, pending deploy. (jcardinal)
103
+ </content>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.416",
3
+ "version": "1.0.418",
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",