toga-ai 1.0.719 → 1.0.721
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.
- package/knowledge/1.0/apps/worker/workflows/netsuite-duplicate-items-dedup.md +33 -1
- package/knowledge/2.0/apps/api2/features/request-logging.md +75 -1
- package/knowledge/clients/compass-canada/profile.md +7 -1
- package/knowledge/clients/compass-usa/INDEX.md +4 -2
- package/knowledge/clients/compass-usa/features/item-catalogs-and-duplicate-items.md +38 -10
- package/knowledge/clients/compass-usa/features/mits-po-to-so-item-linking.md +70 -2
- package/knowledge/clients/compass-usa/features/mits-sales-order-transmission-alerting.md +33 -1
- package/knowledge/clients/compass-usa/features/odp-edi-850-item-resolution.md +16 -13
- package/knowledge/clients/compass-usa/features/two-catalog-item-model.md +103 -0
- package/knowledge/clients/compass-usa/profile.md +2 -0
- package/knowledge/clients/compass-usa/workflows/catalog-fold-reversal.md +120 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -14,6 +14,8 @@ files:
|
|
|
14
14
|
related:
|
|
15
15
|
- ../features/netsuite-togasupply-per-client-sync.md
|
|
16
16
|
- ../../library/features/netsuite-suiteql-rest-shim.md
|
|
17
|
+
- ../../../clients/compass-usa/workflows/catalog-fold-reversal.md
|
|
18
|
+
- ../../../clients/compass-usa/features/two-catalog-item-model.md
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Summary
|
|
@@ -29,6 +31,16 @@ after the code fix is deployed.
|
|
|
29
31
|
**Verified in prod (2026-08-31):** all **17** NetSuite clients now show **0 duplicate part numbers**
|
|
30
32
|
(e.g. `Client_Nychh` 3954→1818 items, `Client_Canon` 913→425; 0 orphans, 0 collisions).
|
|
31
33
|
|
|
34
|
+
> **⚠ CORRECTION (2026-09-01): "0 duplicate part numbers" is NOT proof of a clean run for a
|
|
35
|
+
> multi-catalog client.** This merge is **catalog-blind** — it groups on `UPPER(partNumber)` only.
|
|
36
|
+
> For **Compass**, the same part number **legitimately exists once per catalog** (catalog 1 =
|
|
37
|
+
> store, catalog 2 = Agilant), so the merge folded **199 intentional catalog-1 store items into
|
|
38
|
+
> their Agilant canonical**, broke MITS transmission platform-wide, and had to be reversed. Before
|
|
39
|
+
> running this on any client, confirm the client is **single-catalog**; if not, the dedup must be
|
|
40
|
+
> **catalog-aware** (dedup within a catalog, never across). See the Compass
|
|
41
|
+
> [catalog-fold reversal](../../../clients/compass-usa/workflows/catalog-fold-reversal.md) and
|
|
42
|
+
> [two-catalog item model](../../../clients/compass-usa/features/two-catalog-item-model.md).
|
|
43
|
+
|
|
32
44
|
## Root cause — why the duplicates existed at all
|
|
33
45
|
|
|
34
46
|
`syncInventoryAdjustmentFromNetsuite`'s item-create **omitted the catalog** that
|
|
@@ -90,9 +102,29 @@ duplicates** and must be deployed first — see
|
|
|
90
102
|
**benign skip**, not a failed migration.
|
|
91
103
|
- **A plain `UPDATE IGNORE` + delete-leftover is insufficient.** Dupe-vs-dupe unique-key collisions leave
|
|
92
104
|
orphaned child FKs; only the survivor-per-key re-pointing (step 5) is correct.
|
|
105
|
+
- **⚠ The merge is CATALOG-BLIND — it does NOT handle multi-catalog clients.** It groups on
|
|
106
|
+
`UPPER(partNumber)` only, with an Agilant-preferred canonical rule. For a client that keeps the same
|
|
107
|
+
part number in two intentional catalogs (Compass: store vs Agilant), it folds legitimate per-catalog
|
|
108
|
+
twins together and **destroys** the store items. Confirm a client is single-catalog before running;
|
|
109
|
+
otherwise make the dedup catalog-scoped. The Compass fold deleted 199 store items and broke MITS —
|
|
110
|
+
see the [catalog-fold reversal](../../../clients/compass-usa/workflows/catalog-fold-reversal.md).
|
|
111
|
+
- **`UPDATE IGNORE ...; DELETE ... WHERE fk=dupe` permanently DELETES collision rows** and drops its
|
|
112
|
+
helper maps, so prod cannot self-reverse. A reversal needs a **pre-merge snapshot** restored as a
|
|
113
|
+
scratch DB — plan the backup before running any variant of this.
|
|
114
|
+
- **Blast radius of the 2026-08-31 `duplicate_items_merge_ALL_CLIENTS.sql`:** it ran on 17 clients
|
|
115
|
+
(Masonite, Ncci, Erau, Shrss, Wmchealth, Trividiahealth, Prudential, Miamidade, Browardsheriff,
|
|
116
|
+
Nychh, Canon, Quad, Compass, Elite, Growrk, Spglobal, Endeavorhealth). At-risk = two-catalog +
|
|
117
|
+
TOGa Commerce clients like Compass; a **per-client assessment is outstanding**.
|
|
93
118
|
|
|
94
119
|
## Change history
|
|
95
120
|
|
|
121
|
+
- 2026-09-01 — **⚠ Corrected the success claim.** "0 duplicate part numbers" is NOT a clean-run proof
|
|
122
|
+
for a multi-catalog client. This merge is catalog-blind (groups on `UPPER(partNumber)` only), so for
|
|
123
|
+
Compass it folded 199 intentional catalog-1 store items into their Agilant canonical, broke MITS
|
|
124
|
+
transmission platform-wide, and had to be reversed from a pre-merge snapshot. Added the
|
|
125
|
+
confirm-single-catalog precondition, the permanent-delete / no-self-reverse warning, and the
|
|
126
|
+
17-client blast radius with the outstanding per-client assessment. See the Compass
|
|
127
|
+
[catalog-fold reversal](../../../clients/compass-usa/workflows/catalog-fold-reversal.md). (jcardinal)
|
|
96
128
|
- 2026-08-31 — Documented the one-time duplicate-`Items` dedup across all 17 NetSuite clients (recursive
|
|
97
129
|
MERGE over ~23 `itemId` tables + `Entitlements.saleItemId`, `VendorItems`/`Units` FK cascades,
|
|
98
130
|
survivor-per-key collision resolution), its root cause (the inventory-adjustment item-create omitted
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -16,7 +16,10 @@ files:
|
|
|
16
16
|
related:
|
|
17
17
|
- ../architecture.md
|
|
18
18
|
- ./api-payload-interceptors.md
|
|
19
|
+
- ./v2-api-error-codes.md
|
|
20
|
+
- ../../_underscore/features/error-reporting-issue-event.md
|
|
19
21
|
- ../../../../clients/aig/features/entitlement-intake.md
|
|
22
|
+
- ../../../../clients/compass-usa/features/mits-po-to-so-item-linking.md
|
|
20
23
|
- ../../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
|
|
21
24
|
---
|
|
22
25
|
|
|
@@ -146,6 +149,67 @@ transaction no matter how many times apiproxy retried. That is why the retry att
|
|
|
146
149
|
there even in principle — the two logs are not two views of the same rows. Join the two by
|
|
147
150
|
`transactionId` when you need the full attempt sequence for one user action.
|
|
148
151
|
|
|
152
|
+
## Getting the REAL error behind a generic `EO-1` 500 (the three-hop chain)
|
|
153
|
+
|
|
154
|
+
A 500 whose only content is `EO-1` — *"Operation Failed: An internal server error occurred"* —
|
|
155
|
+
names neither the failing field nor the value. Very often the true failure is a **validation error
|
|
156
|
+
on a NESTED sub-request** that `internalApiRequest()` issued inside the outer write, and it is fully
|
|
157
|
+
recorded — just in three different places. Walk them in order.
|
|
158
|
+
|
|
159
|
+
**1. Decode `error.id` → `Logs.Issue` + `Logs.Event`.** The envelope carries `error.id` shaped
|
|
160
|
+
`<Issue.reference>-<Event.eventNumber>` (e.g. `QT-605`). Query the **prod `Logs`** schema:
|
|
161
|
+
|
|
162
|
+
```sql
|
|
163
|
+
# reference is the part BEFORE the dash — searching for the whole 'QT-605' matches NOTHING
|
|
164
|
+
SELECT id, reference, status, errorMessage, trace, totalOccurrences, dtLastOccurred
|
|
165
|
+
FROM Issue
|
|
166
|
+
WHERE reference = 'QT';
|
|
167
|
+
|
|
168
|
+
SELECT clientId, dtCreated, errorMessage
|
|
169
|
+
FROM Event
|
|
170
|
+
WHERE issueId = <issue.id> AND eventNumber = 605;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Full semantics of the reference encoding live in
|
|
174
|
+
[Error Reporting — Issue/Event Capture](../../_underscore/features/error-reporting-issue-event.md).
|
|
175
|
+
|
|
176
|
+
**2. Read `Issue.trace`, not `Issue.errorMessage`.** `Issue.errorMessage` and `Event.errorMessage`
|
|
177
|
+
are **`varchar(255)`** and truncate, and on an api2 validation failure the cut lands **exactly where
|
|
178
|
+
the useful part begins** — the `identifiers` / `searchCriteria` block naming the field and the value
|
|
179
|
+
that matched nothing. `Issue.trace` is full text and gives the exact `file:line` chain, which is
|
|
180
|
+
usually enough to identify *which* nested call failed even though it never tells you *with what
|
|
181
|
+
value*.
|
|
182
|
+
|
|
183
|
+
**3. The complete inner error is a row in `Logs_<Client>.Api` — and you CANNOT find it by the outer
|
|
184
|
+
`transactionId`.** `internalApiRequest()` logs **every nested sub-request** as its own client-log
|
|
185
|
+
row, and since the 2026-08-25 collision fix that row's `transactionId` is a **freshly generated
|
|
186
|
+
random uuid** (`_String::generateUuid()`), deliberately unrelated to the outer request — see
|
|
187
|
+
[api2 architecture](../architecture.md). So the inner row shares **nothing** with the caller except
|
|
188
|
+
its timestamp. Locate it by **`dtStamp` window + `route`**, then read its `responsePayload`, which
|
|
189
|
+
carries the complete message including `identifiers` and `searchCriteria`.
|
|
190
|
+
|
|
191
|
+
```sql
|
|
192
|
+
SELECT id, dtStamp, route, method, responseCode, LEFT(responsePayload, 2000)
|
|
193
|
+
FROM Logs_Compass.Api
|
|
194
|
+
WHERE
|
|
195
|
+
dtStamp BETWEEN '2026-09-01 04:12:00' AND '2026-09-01 04:12:30' AND
|
|
196
|
+
route LIKE '/v2/purchase-orders%'
|
|
197
|
+
ORDER BY dtStamp;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
> **⚠ Three traps in this chain**, each of which has already sent an investigation the wrong way:
|
|
201
|
+
> searching `Issue.reference` for the **full** `QT-605` string (it stores only `QT`); believing the
|
|
202
|
+
> **truncated** `errorMessage` is the whole error (it is severed at 255 chars, right before the
|
|
203
|
+
> field name); and trying to join the inner sub-request row by `transactionId` (it is random by
|
|
204
|
+
> design, so the join returns nothing and reads as "the inner call was never logged").
|
|
205
|
+
|
|
206
|
+
**Worked example.** A Compass `POST /v2/purchase-orders` returned `EO-1`/`QT-605`. `Issue.trace`
|
|
207
|
+
pointed at `internalApiRequest()` (`V2.php` ≈L2433); the matching `Logs_Compass.Api` row for the
|
|
208
|
+
nested sales-order write held the real verdict — **`EV-12` "unable to find a unique match" on field
|
|
209
|
+
`item`** for a part number scoped to the wrong catalog. Nothing in the 500 the caller saw contained
|
|
210
|
+
the word `item`. See
|
|
211
|
+
[Compass MITS PO → SO item linking](../../../../clients/compass-usa/features/mits-po-to-so-item-linking.md).
|
|
212
|
+
|
|
149
213
|
## Blind spots when auditing a client's API traffic
|
|
150
214
|
|
|
151
215
|
Looking only at `Logs_<Client>.Api` misses four sources:
|
|
@@ -225,6 +289,16 @@ re-send.
|
|
|
225
289
|
|
|
226
290
|
## Change history
|
|
227
291
|
|
|
292
|
+
- 2026-09-01 — Added **Getting the REAL error behind a generic `EO-1` 500** — the three-hop chain
|
|
293
|
+
from an opaque 500 to the actual failing field. Key new fact: `internalApiRequest()` logs each
|
|
294
|
+
**nested** sub-request into `Logs_<Client>.Api` under a **randomly generated `transactionId`**
|
|
295
|
+
(the 2026-08-25 collision fix), so the inner row **cannot** be joined to the outer request — find
|
|
296
|
+
it by `dtStamp` window + `route` and read its `responsePayload`, which is the only place the
|
|
297
|
+
`identifiers`/`searchCriteria` survive. Also recorded that `error.id` is
|
|
298
|
+
`<Issue.reference>-<Event.eventNumber>` and `Issue.reference` stores only the part **before** the
|
|
299
|
+
dash, and that the `varchar(255)` `errorMessage` truncates exactly where the field name starts, so
|
|
300
|
+
`Issue.trace` is what to read. Found while root-causing the Compass MR purchase-order 500 storm
|
|
301
|
+
(`EV-12` on `item`, hidden behind `EO-1`). No code change. (bala)
|
|
228
302
|
- 2026-08-29 — Noted that `Logs.Api.route` values are stored **with the `/v2` prefix** (e.g.
|
|
229
303
|
`/v2/inventory-adjustments/{uuid}`), so narrowing queries must filter `route LIKE '/v2/...'` — a
|
|
230
304
|
bare `/inventory-adjustments` predicate matches nothing. Reinforced that a narrowed row's
|
|
@@ -16,7 +16,7 @@ project: _Underscore
|
|
|
16
16
|
client: compass-canada
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-
|
|
19
|
+
updated: 2026-09-01
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -190,4 +190,10 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
190
190
|
8 Manager. USA grants the fulfillment records to roles 3 + 7; Canada grants most of them to role 1
|
|
191
191
|
only. Nearly every logged-in Canada user carries role 1, so this is usually invisible — but ~10
|
|
192
192
|
users hold Compass Base without Base and still hit the gap.
|
|
193
|
+
- **Canada was NOT damaged by the 2026-08-31 cross-client item merge, and is structurally
|
|
194
|
+
immune.** It was **not** in the `duplicate_items_merge_ALL_CLIENTS.sql` client list; it has a
|
|
195
|
+
**single catalog** ("compass"), 0 part numbers in multiple catalogs, and 0 duplicate part
|
|
196
|
+
numbers — because it does **not** use NetSuite, so it never hit the inventory-adjustment
|
|
197
|
+
dupe-item bug that fed the merge. The catalog-fold damage and reversal were Compass USA only.
|
|
198
|
+
See [Compass USA catalog-fold reversal](../compass-usa/workflows/catalog-fold-reversal.md).
|
|
193
199
|
- Related: [Compass USA](../compass-usa/profile.md).
|
|
@@ -6,9 +6,9 @@
|
|
|
6
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 |
|
|
7
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 |
|
|
8
8
|
| [Compass isFulfillable — Data Quality & the Type-Derived Rule](features/isfulfillable-data-quality-and-type-rule.md) | 2.0 | A **prod-data investigation (2026-08-12, read-only)** into why so many Compass storefront lines still have a dead **Qty Fulfilled** cell. | _underscore/Model/Client/Item.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx |
|
|
9
|
-
| [Compass Item Catalogs (Compass vs Agilant) & the Duplicate Items the NetSuite Sync Creates](features/item-catalogs-and-duplicate-items.md) | 1.0 | `Client_Compass.Items` is **split across catalogs by which pipeline created the item**, and the two halves are routinely mistaken for one. | library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
9
|
+
| [Compass Item Catalogs (Compass vs Agilant) & the Duplicate Items the NetSuite Sync Creates](features/item-catalogs-and-duplicate-items.md) | 1.0 | `Client_Compass.Items` is **split across catalogs by which pipeline created the item**, and the two halves are routinely mistaken for one. | library/app/api/toga2.php, _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Compass/Usa/PurchaseOrder.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
10
10
|
| [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 |
|
|
11
|
-
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
11
|
+
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Compass/Usa/PurchaseOrder.php, api2/Component/Api/V2/V2.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
12
12
|
| [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
|
|
13
13
|
| [Compass MITS PO rejection — zero line items (400 + Issue + PM alert email)](features/mits-po-zero-line-item-rejection.md) | 2.0 | MITS transmits Compass purchase orders that carry **`"purchaseOrderItems": []`** (real example: PO `50309196-1`, `mitsSalesOrder` `MR244630`). | _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Compass/Usa/PurchaseOrder.php, _underscore/Model/Compass/Canada/PurchaseOrder.php, worker2/Worker/Client/Compass/reports/PurchaseOrderRejectionEmail.php |
|
|
14
14
|
| [Compass MITS Sales-Order Transmit Alerting (US Issue/Event vs Canada in-cron email)](features/mits-sales-order-transmission-alerting.md) | 2.0 | How a failure in the `1_transmit_..._sales_orders_to_mits.php` cron reaches a human — for **both** Compass tenants, because **they no longer work the same way** | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compasscanada/workflow/1_transmit_compassCanada_sales_orders_to_mits.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/post.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql |
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
| [Persona Model & Levy-Sector Gating (worker2 PEOPLE cron)](features/persona-model-and-levy-gating.md) | 2.0 | Compass USA catalogue visibility is driven by **personas** in `Client_Compass`. | worker2/Worker/Client/Compass/PeopleFile.php |
|
|
22
22
|
| [Compass Sales-Order Line-Number Renumbering (and why it hangs off the SO hooks)](features/sales-order-line-renumbering.md) | 2.0 | Compass sales-order lines must stay numbered **1..N with no gaps** after any add, edit, or delete — downstream MITS/PO linking reads `lineNumber` as an identity | _underscore/Model/Compass/SalesOrderItem.php, _underscore/Model/Compass/SalesOrder.php |
|
|
23
23
|
| [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i | _underscore/Model/Compass/ApprovalDecision.php, worker2/Worker/Client/Compass/ApprovalReassignment.php, worker2/Worker/Client/Compass/PeopleFile.php |
|
|
24
|
+
| [Compass USA — intentional two-catalog Item model (store vs Agilant procurement)](features/two-catalog-item-model.md) | 2.0 | Compass USA keeps its items in **two intentional catalogs**, and the **same part number legitimately exists once per catalog**. | library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
24
25
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
26
|
+
| [Compass USA — reversing the 199-item catalog fold (MITS outage recovery)](workflows/catalog-fold-reversal.md) | 2.0 | The **root cause** of the Compass MITS transmission outage (the `assetType`-NULL and duplicate-vendor symptoms) was an ad-hoc cross-client **duplicate-items mer | test/team/2.0 deployment/duplicate_items_merge_ALL_CLIENTS.sql, test/team/2.0 deployment/duplicate_items_merge_analysis.sql, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php |
|
|
25
27
|
| [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 |
|
|
26
28
|
| [Granting a Compass user the same toga25-supply navigation as another user (role vs. SurfaceOverride)](workflows/granting-navigation-access.md) | 2.0 | "Give user X the same menu items user Y has" is the **navigation analog** of the recurring [bundle/persona grant](./granting-persona-bundle-access.md) request. | dbchanges2/Client_Compass/2026-08-12a - NavigationRoleGrants.sql, dbchanges2/Client_Compass/2026-02-25 - AgilantUsersViewAccess.sql, dbchanges2/Client_Compass/2026-08-11 - CompassODPViewVendorItemsAccess.sql, dbchanges2/Core/2026-08-12a - NavigationSurfaceSeed.sql |
|
|
27
29
|
| [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
|
|
@@ -10,10 +10,13 @@ updated: 2026-09-01
|
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
|
+
- _underscore/Model/Compass/PurchaseOrder.php
|
|
14
|
+
- _underscore/Model/Compass/Usa/PurchaseOrder.php
|
|
13
15
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
14
16
|
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
15
17
|
related:
|
|
16
18
|
- odp-edi-850-item-resolution.md
|
|
19
|
+
- mits-po-to-so-item-linking.md
|
|
17
20
|
- isfulfillable-data-quality-and-type-rule.md
|
|
18
21
|
- ../workflows/odp-duplicate-po-line-cleanup.md
|
|
19
22
|
- ../workflows/odp-order-pipeline-to-netsuite.md
|
|
@@ -30,7 +33,9 @@ two halves are routinely mistaken for one. **This is the first thing to check wh
|
|
|
30
33
|
item "exists but is not found."** A part number that is present, active and correct in
|
|
31
34
|
`Client_Compass.Items` will still fail a lookup that is scoped to the wrong catalog — which is
|
|
32
35
|
exactly how the 2026-08-31 Office Depot EDI import outage happened
|
|
33
|
-
([ODP EDI 850 line-item resolution](odp-edi-850-item-resolution.md))
|
|
36
|
+
([ODP EDI 850 line-item resolution](odp-edi-850-item-resolution.md)) and, the same night, how every
|
|
37
|
+
Compass **MITS MR purchase order** for an Agilant-catalog SKU started returning an opaque HTTP 500
|
|
38
|
+
([MITS PO → SO item linking](mits-po-to-so-item-linking.md)).
|
|
34
39
|
|
|
35
40
|
The second half of the same problem: the NetSuite sync **creates a second `Items` row** for a
|
|
36
41
|
product that already exists in the other catalog rather than matching it, so one physical product
|
|
@@ -85,14 +90,16 @@ in catalog 1 under a slightly different part number gets a **second `Items` row*
|
|
|
85
90
|
|
|
86
91
|
Plus `-1` / `-2` suffixed copies: items **2431, 2432, 2443, 2653**.
|
|
87
92
|
|
|
88
|
-
**Two distinct duplicate shapes
|
|
93
|
+
**Two distinct duplicate shapes, and only the first exists in prod today:**
|
|
89
94
|
- **Different part numbers** (the table above) — the two rows never match on part number, so each
|
|
90
|
-
pipeline keeps using "its own" item and quantities/fulfillments diverge.
|
|
91
|
-
- **Same part number, two catalogs
|
|
95
|
+
pipeline keeps using "its own" item and quantities/fulfillments diverge. This is the live shape.
|
|
96
|
+
- **Same part number, two catalogs — NOT present in prod.** This is the shape behind the
|
|
92
97
|
[dual-catalog duplicate PO-line bug](../workflows/odp-duplicate-po-line-cleanup.md) (4,886
|
|
93
|
-
duplicate lines across 1,320 Office Depot POs
|
|
94
|
-
**
|
|
95
|
-
|
|
98
|
+
duplicate PO lines across 1,320 Office Depot POs when that was investigated).
|
|
99
|
+
**Verified on prod `Client_Compass` 2026-09-01: zero part numbers are duplicated at all, and zero
|
|
100
|
+
span more than one catalog.** It matters only as a thing to re-check, because a part-number lookup
|
|
101
|
+
with no catalog filter has **no ambiguity check on its exact-match branch**
|
|
102
|
+
([850 line-item resolution](odp-edi-850-item-resolution.md)).
|
|
96
103
|
|
|
97
104
|
## "Who changed this part number?" — the audit trail says nobody
|
|
98
105
|
|
|
@@ -115,7 +122,18 @@ children" rule:
|
|
|
115
122
|
|
|
116
123
|
- **⚠ Never scope a Compass item lookup to one catalog** unless you have proved the item can only
|
|
117
124
|
come from that source. The correct default is `Items.partNumber IN (...)` with **no** catalog
|
|
118
|
-
condition, and then reference the resolved record **by `uuid`**.
|
|
125
|
+
condition, and then reference the resolved record **by `uuid`**. **The catalog filter never buys
|
|
126
|
+
uniqueness on Compass** — `Items.partNumber` is unique across the whole table (re-verified prod
|
|
127
|
+
2026-09-01) — so every catalog condition is pure downside.
|
|
128
|
+
- **⚠ The 2.0 side hardcodes the OTHER catalog, and it broke MR purchase orders.**
|
|
129
|
+
`_Model_Compass_PurchaseOrder::handleMrOrder` builds each SO item as
|
|
130
|
+
`['partNumber' => …, 'catalog' => ['uuid' => static::COMPASS_CATALOG_UUID]]`, and
|
|
131
|
+
`_Model_Compass_Usa_PurchaseOrder::COMPASS_CATALOG_UUID` is `961dbcbc…` = **catalog 1
|
|
132
|
+
(Compass)**. So the 1.0 sync can only ever *write* catalog 2 and the 2.0 MR handler can only ever
|
|
133
|
+
*read* catalog 1 — the two hardcodes point in opposite directions. Any MR order for a catalog-2
|
|
134
|
+
SKU (`C40QYUC`, `B97TGUC`, `4U555A-S`, …) resolves zero rows → api2 `EV-12` → opaque `EO-1` 500
|
|
135
|
+
with MITS retrying forever. Details, blast radius and fix:
|
|
136
|
+
[MITS PO → SO item linking](mits-po-to-so-item-linking.md).
|
|
119
137
|
- **⚠ A NULL `catalogId` is a real state** (27 items in prod, 22 ODP `VendorItems` point at such
|
|
120
138
|
items). Anything joining through `Catalogs` **drops those rows silently** — an inner join to
|
|
121
139
|
`Catalogs` is itself a filter.
|
|
@@ -127,6 +145,14 @@ children" rule:
|
|
|
127
145
|
([850 line-item resolution](odp-edi-850-item-resolution.md)).
|
|
128
146
|
|
|
129
147
|
## Change history
|
|
148
|
+
- 2026-09-01 — Recorded a **second, independent victim of the same catalog-scoping trap, on the 2.0
|
|
149
|
+
side**: `_Model_Compass_PurchaseOrder::handleMrOrder` references MR sales-order items with a
|
|
150
|
+
hardcoded `COMPASS_CATALOG_UUID` (`961dbcbc…` = catalog **1**), which is the mirror image of the
|
|
151
|
+
1.0 sync's hardcoded `'Agilant'` (catalog **2**) — one only writes catalog 2, the other only reads
|
|
152
|
+
catalog 1. MR POs for `C40QYUC` / `B97TGUC` / `4U555A-S` therefore failed api2 `EV-12` and surfaced
|
|
153
|
+
as an opaque `EO-1` 500 (`Logs.Issue` reference `QT`, 660 occurrences, 10 POs blocked overnight
|
|
154
|
+
2026-08-31). Also promoted "the catalog filter never buys uniqueness" into the gotchas, since
|
|
155
|
+
`Items.partNumber` uniqueness across the whole table was re-verified on prod the same day. (bala)
|
|
130
156
|
- 2026-09-01 — Documented the `Client_Compass` catalog split as its own subject after it caused the
|
|
131
157
|
2026-08-31 ODP EDI import outage: catalogs 1 "Compass" (Etilize, `c_etilizeProductId`) / 2
|
|
132
158
|
"Agilant" (1.0 NetSuite sync, `c_netsuiteInternalItemId`) / 3 with their uuids and prod counts
|
|
@@ -135,8 +161,10 @@ children" rule:
|
|
|
135
161
|
`syncItemFulfillmentFromNetsuite()`, `syncInventoryAdjustmentFromNetsuite()` and the
|
|
136
162
|
Agilant-only lookup built in `common_sync_togasupply.php` — which is **why** the sync creates a
|
|
137
163
|
duplicate instead of matching. Listed the observed duplicate pairs (1815/2839, 2216/2427,
|
|
138
|
-
2225/2430, plus 2431/2432/2443/2653) and separated the two duplicate shapes
|
|
139
|
-
numbers
|
|
164
|
+
2225/2430, plus 2431/2432/2443/2653) and separated the two duplicate shapes: **different** part
|
|
165
|
+
numbers is the live shape, while **same** part number in two catalogs is **not present in prod**
|
|
166
|
+
(verified on prod 2026-09-01 — zero duplicated part numbers, zero spanning two catalogs), so it
|
|
167
|
+
is a latent risk only.
|
|
140
168
|
Recorded the audit-trail method (`Logs_Compass.Record` ⋈ `RecordField`, `recordId` 21 = Items,
|
|
141
169
|
`recordFieldId` 108 = partNumber) and its verdict: every `partNumber` row is a create with a NULL
|
|
142
170
|
`oldValue` by `apiId = 1`, so **nobody renamed anything**. Data/investigation finding, no code
|
|
@@ -6,15 +6,20 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: ["jcardinal", "rgirish", "dfranks", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/PurchaseOrder.php
|
|
13
|
+
- _underscore/Model/Compass/Usa/PurchaseOrder.php
|
|
14
|
+
- api2/Component/Api/V2/V2.php
|
|
13
15
|
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
14
16
|
related:
|
|
15
17
|
- asn-to-item-fulfillment.md
|
|
16
18
|
- mits-po-transmission-to-vendors.md
|
|
17
19
|
- mits-po-zero-line-item-rejection.md
|
|
20
|
+
- item-catalogs-and-duplicate-items.md
|
|
21
|
+
- ../../../2.0/apps/api2/features/request-logging.md
|
|
22
|
+
- ../../../2.0/apps/api2/features/v2-api-error-codes.md
|
|
18
23
|
---
|
|
19
24
|
|
|
20
25
|
## Summary
|
|
@@ -109,7 +114,9 @@ per-api renames and interceptor scoping interact.
|
|
|
109
114
|
returns early. `handleMrOrder` creates/appends the SO from the PO items, **strips**
|
|
110
115
|
`createdFromSalesOrderItem`, and deliberately **aligns** SO and PO `lineNumber`s. It does
|
|
111
116
|
NOT build the item bridge — so the `postPost` lineNumber-match pass is the **only** (and
|
|
112
|
-
correct) MR linking mechanism.
|
|
117
|
+
correct) MR linking mechanism. Each SO item it builds references the item as
|
|
118
|
+
`partNumber` **plus a hardcoded catalog uuid**, which is the cause of the MR 500 outage below —
|
|
119
|
+
see [MR POs 500 when the item is outside the Compass catalog](#-mr-pos-500-when-the-item-lives-outside-the-compass-catalog-open-as-of-2026-09-01).
|
|
113
120
|
- **MA orders — item population in the ODP PO import cron:** the 3a cron
|
|
114
121
|
(`worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`) parses ODP
|
|
115
122
|
EDI 850 POs. Its "Non-SA order handling" block creates the Compass **MA** sales order. Line
|
|
@@ -140,6 +147,57 @@ per-api renames and interceptor scoping interact.
|
|
|
140
147
|
USA and Canada share the parent handler unchanged.
|
|
141
148
|
|
|
142
149
|
## Gotchas / known issues
|
|
150
|
+
### ⚠ MR POs 500 when the item lives outside the Compass catalog (OPEN as of 2026-09-01)
|
|
151
|
+
|
|
152
|
+
**Symptom.** `POST /v2/purchase-orders` on `api-writer.togahub.com` returns **HTTP 500** with the
|
|
153
|
+
generic envelope error **`EO-1`** ("Operation Failed: An internal server error occurred"). MITS has
|
|
154
|
+
no backoff, so it **retries every few seconds forever** — 66 attempts were observed for a single PO.
|
|
155
|
+
Sibling POs on the *same* MR sales order succeed, which makes it look intermittent. It is not: it is
|
|
156
|
+
**per part number**.
|
|
157
|
+
|
|
158
|
+
**Cause.** `_Model_Compass_PurchaseOrder::handleMrOrder` (≈L61) builds every `salesOrderItems`
|
|
159
|
+
item reference **scoped to one catalog**:
|
|
160
|
+
|
|
161
|
+
```php
|
|
162
|
+
'item' => [
|
|
163
|
+
'partNumber' => $partNumber,
|
|
164
|
+
'catalog' => ['uuid' => static::COMPASS_CATALOG_UUID],
|
|
165
|
+
]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`COMPASS_CATALOG_UUID` on `_Model_Compass_Usa_PurchaseOrder` is
|
|
169
|
+
`961dbcbc-1943-11ef-8e3b-0aae2980db55` = **`Client_Compass.Catalogs` id 1 ("Compass")**. But
|
|
170
|
+
`Client_Compass` has **three** catalogs (1 Compass, 2 Agilant, 3 Office Depot) and a large set of
|
|
171
|
+
MITS part numbers are `Items` rows under **catalog 2 (Agilant)** — observed: `C40QYUC`, `C40QYUC-1`,
|
|
172
|
+
`B97TGUC`, `B97TGUC-1`, `4U555A-S`. The catalog-scoped reference resolves **zero** rows, api2 raises
|
|
173
|
+
**`EV-12`** ("unable to find a unique match … requires an exact match for locating the child
|
|
174
|
+
record") on field `item`, `internalApiRequest` (`api2/Component/Api/V2/V2.php` ≈L2433) throws, and
|
|
175
|
+
the **whole outer purchase-order request rolls back as a 500**. The precise field and value never
|
|
176
|
+
reach the caller — only `EO-1` — which is why this reads as an unexplained platform fault.
|
|
177
|
+
|
|
178
|
+
**The catalog filter buys nothing.** `Client_Compass.Items.partNumber` is **unique across the entire
|
|
179
|
+
table** (verified on prod 2026-09-01 — zero duplicated part numbers, zero spanning two catalogs), so
|
|
180
|
+
scoping the lookup to one catalog adds **no** uniqueness and only manufactures false misses. This is
|
|
181
|
+
the same trap that caused the 2026-08-31 ODP EDI import outage — see
|
|
182
|
+
[Compass Item Catalogs](item-catalogs-and-duplicate-items.md).
|
|
183
|
+
|
|
184
|
+
**Fix (not yet applied).** Either drop the `catalog` key so the lookup is by `partNumber` alone, or
|
|
185
|
+
resolve the item **uuid** first with an internal `GET /items` and pass `['uuid' => …]`, throwing a
|
|
186
|
+
clear `_Exception_Business` when the part number matches nothing. The second shape is preferable: it
|
|
187
|
+
turns an opaque 500-and-retry-forever into a named business rejection.
|
|
188
|
+
|
|
189
|
+
**Blast radius observed (2026-08-31 23:00 → 2026-09-01).** `Logs.Issue` reference **`QT`**
|
|
190
|
+
(issue id 602) accumulated **660** occurrences. **Ten** Compass MITS POs were blocked — `50312113-1`,
|
|
191
|
+
`50312116-1`, `50312117-1`, `50312119-1`, `50312122-1`, `50312144-1`, `50312161-1`, `50312176-1`,
|
|
192
|
+
`50312180-1`, `50312182-1` (MR245464/65/66/69/70/72/73/76/79/80). All ten were re-submitted
|
|
193
|
+
successfully on 2026-09-01 between 11:24 and 11:43 once the items existed where the lookup looked.
|
|
194
|
+
Sibling POs on the same MR sales orders (`50312114-1`, `50312120-1`, …) went through fine because
|
|
195
|
+
their item `9X3V1UT` sits in catalog 1.
|
|
196
|
+
|
|
197
|
+
**Recurrence risk is high, not theoretical.** Any Compass MR order for an Agilant-catalog SKU hits
|
|
198
|
+
it — custom Azure-tenant SKUs and kitting SKUs land in catalog 2 by construction, because the 1.0
|
|
199
|
+
NetSuite sync can only create there.
|
|
200
|
+
|
|
143
201
|
- **SQL injection in `prePost` (OPEN as of 2026-08-03).** `_underscore/Model/Compass/PurchaseOrder.php`
|
|
144
202
|
**L232** interpolates the raw client payload straight into SQL:
|
|
145
203
|
`WHERE SalesOrders.number LIKE '{$payload->mitsSalesOrder}'` — no escape, no cast. The **same
|
|
@@ -227,6 +285,16 @@ USA and Canada share the parent handler unchanged.
|
|
|
227
285
|
need item backfill from the sibling ODP SO first).
|
|
228
286
|
|
|
229
287
|
## Change history
|
|
288
|
+
- 2026-09-01 — **Root-caused the MR PO `EO-1` 500 storm** (2026-08-31 23:00 → 2026-09-01,
|
|
289
|
+
`Logs.Issue` reference `QT`, issue id 602, 660 occurrences, 10 POs blocked, MITS retrying every few
|
|
290
|
+
seconds with no backoff). `handleMrOrder` (≈L61) references each SO item as `partNumber` **plus**
|
|
291
|
+
`static::COMPASS_CATALOG_UUID` (`961dbcbc…` = `Client_Compass.Catalogs` id 1 "Compass"), but
|
|
292
|
+
several MITS part numbers (`C40QYUC`, `C40QYUC-1`, `B97TGUC`, `B97TGUC-1`, `4U555A-S`) are catalog-2
|
|
293
|
+
**Agilant** items, so the scoped lookup matches nothing → api2 `EV-12` on field `item` →
|
|
294
|
+
`internalApiRequest` throws → the outer PO write rolls back as an opaque 500. The catalog filter
|
|
295
|
+
adds no uniqueness (`Items.partNumber` is unique across the whole table, verified on prod), so the
|
|
296
|
+
fix is to drop the `catalog` key or resolve the item uuid first and fail with a business
|
|
297
|
+
exception. **No code change yet — the ten POs were re-submitted manually.** (bala)
|
|
230
298
|
- 2026-08-13 — The zero-line-item rejection this doc's caller analysis was written to scope **has
|
|
231
299
|
shipped** as the first statement of `prePost` (TRUE-80412), so `handleMrOrder` can no longer create
|
|
232
300
|
a zero-item Sales Order for an MR PO. It is MITS-only by construction (`apiId = 2` interceptor rows
|
|
@@ -17,6 +17,8 @@ files:
|
|
|
17
17
|
- dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql
|
|
18
18
|
related:
|
|
19
19
|
- mits-po-transmission-to-vendors.md
|
|
20
|
+
- two-catalog-item-model.md
|
|
21
|
+
- ../workflows/catalog-fold-reversal.md
|
|
20
22
|
- ../../compass-canada/profile.md
|
|
21
23
|
- ../../../2.0/apps/worker2/features/error-escalation-cron.md
|
|
22
24
|
- ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
|
|
@@ -125,9 +127,20 @@ backlog (0 new, several >24h, one ~20 days old).
|
|
|
125
127
|
2. **HTTP 403 `SalesOrder:Exists`** — MITS already created the order on a prior attempt, but
|
|
126
128
|
our side never recorded `c_dtTransmittedToCompass`, so the cron **retries it forever**.
|
|
127
129
|
|
|
130
|
+
**Deeper root cause of the 2026-08-31 outage — a wrong catalog merge, not just missing data.**
|
|
131
|
+
The NULL-assetType items were largely created by an ad-hoc cross-client dedup that folded
|
|
132
|
+
Compass's **199 catalog-1 (store) items into their Agilant (catalog-2) canonical**, so Compass
|
|
133
|
+
order lines resolved to Agilant items that have no store classification. The same merge fused
|
|
134
|
+
their VendorItems, which is what triggered the SQL 1242 abort (see gotchas below). Full root
|
|
135
|
+
cause + the reversal playbook: [catalog-fold reversal](../workflows/catalog-fold-reversal.md).
|
|
136
|
+
Compass keeps two intentional catalogs and the customer→catalog rule is load-bearing — see
|
|
137
|
+
[two-catalog item model](./two-catalog-item-model.md).
|
|
138
|
+
|
|
128
139
|
**Fix the data:** assign the correct `assetTypeId` to the affected `Client_Compass.Items`
|
|
129
140
|
(business owns the mapping), and check whether the NetSuite item sync owns `assetType` — if it
|
|
130
|
-
does, it will revert the fix unless the source is corrected too.
|
|
141
|
+
does, it will revert the fix unless the source is corrected too. (Verified: the sync UPDATE path
|
|
142
|
+
never touches `assetType`/`category`/`type`, so a set value is durable — but new items are born
|
|
143
|
+
unclassified on the Agilant catalog. See the two-catalog doc.)
|
|
131
144
|
|
|
132
145
|
**Recommended code hardening (NOT yet implemented):** wrap each order's transmit in
|
|
133
146
|
`try/catch` so one MITS rejection raises the business issue but **continues to the next
|
|
@@ -158,6 +171,17 @@ pages even when the count is under threshold. See
|
|
|
158
171
|
name the order, not the person. (This is precisely what Canada still does wrong.)
|
|
159
172
|
- **The MITS crons hold their API credentials inline in the file.** Treat both files as
|
|
160
173
|
secret-bearing when copying snippets anywhere (tickets, PR bodies, knowledge docs).
|
|
174
|
+
- **The transmit item query's `GROUP BY SalesOrderItems.id` hides false "duplicates."** In
|
|
175
|
+
`1_transmit_compass_sales_orders_to_mits.php` (~L178-251, `GROUP BY` at L247) the
|
|
176
|
+
`GROUP BY SalesOrderItems.id` collapses the `LEFT JOIN VendorItems` / `LEFT JOIN
|
|
177
|
+
VendorItemCosts` fan-out to one row per line. Running that SELECT **without** the GROUP BY
|
|
178
|
+
shows FALSE duplicate fee lines (fees have 2 `VendorItemCosts` rows) — the real cron sends
|
|
179
|
+
one line per SalesOrderItem. Do not "diagnose" duplicates from the un-grouped query.
|
|
180
|
+
- **`c_preferredVendorId` scalar subquery has no `LIMIT` → SQL 1242 on multi-vendor items.**
|
|
181
|
+
Any item with >1 active `VendorItem` makes that scalar subquery return >1 row and **aborts
|
|
182
|
+
the whole run**. This was the latent bug the catalog merge exposed (it fused VendorItems).
|
|
183
|
+
A one-line `ORDER BY VendorItems.id LIMIT 1` guard fixes it, but the proper two-catalog data
|
|
184
|
+
fix makes Compass items single-vendor so the guard is not needed.
|
|
161
185
|
- **Adding a recipient does not turn ClickUp ticketing off.** Routing is both-and, not
|
|
162
186
|
either-or: `_Worker_Infrastructure_Errors` runs `if ($recipients !== null)` and
|
|
163
187
|
`if ((bool) $issue->createsClickupTask)` as **two independent** branches.
|
|
@@ -169,6 +193,14 @@ pages even when the count is under threshold. See
|
|
|
169
193
|
when this issue next fires. Watch that run rather than assuming it works.
|
|
170
194
|
|
|
171
195
|
## Change history
|
|
196
|
+
- 2026-09-01 — Traced the 2026-08-31 outage to its **deeper root cause**: an ad-hoc
|
|
197
|
+
cross-client dedup folded Compass's 199 catalog-1 store items into their Agilant canonical, so
|
|
198
|
+
order lines resolved to unclassified Agilant items (HTTP 400) and their fused VendorItems
|
|
199
|
+
tripped the transmit's un-`LIMIT`ed `c_preferredVendorId` scalar subquery (SQL 1242, whole run
|
|
200
|
+
aborts). Recorded the two transmit-query gotchas (`GROUP BY SalesOrderItems.id` hides false
|
|
201
|
+
duplicate fee lines from the VendorItemCosts fan-out; the `c_preferredVendorId` subquery needs
|
|
202
|
+
`ORDER BY VendorItems.id LIMIT 1` or single-vendor data). Cross-linked the new catalog-fold
|
|
203
|
+
reversal workflow and two-catalog item-model doc. (jcardinal)
|
|
172
204
|
- 2026-08-31 — Diagnosed the recurring backlog's **root cause**: not volume but **poison-order
|
|
173
205
|
starvation**. One MITS rejection `throw`s and aborts the whole run, and because the driver
|
|
174
206
|
query uses `ORDER BY RAND()`, a stuck order blocks a random set of good orders every run.
|
|
@@ -119,17 +119,19 @@ different one than the EDI importer posts against. Full catalog model, counts, a
|
|
|
119
119
|
duplicate-item problem behind it:
|
|
120
120
|
[Compass item catalogs & duplicate items](item-catalogs-and-duplicate-items.md).
|
|
121
121
|
|
|
122
|
-
###
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
- **The exact-match branch has no ambiguity check
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
### Two things the resolver does not check now that the catalog filter is out
|
|
123
|
+
Neither is a live problem — the first is latent, the second is safe by design:
|
|
124
|
+
|
|
125
|
+
- **The exact-match branch has no ambiguity check — latent, cannot fire today.**
|
|
126
|
+
`fetchPartNumbersCompassCarries()` keys its lookup **one entry per uppercased part number**
|
|
127
|
+
(`$lookup[strtoupper($partNumber)] = ['partNumber' => …, 'uuid' => …]`), so **if** the same part
|
|
128
|
+
number were ever created in two catalogs, this branch would silently take whichever row api2
|
|
129
|
+
returned **last** and use its uuid — unlike the vendor-SKU branch below, which counts candidates
|
|
130
|
+
and rejects an ambiguous match.
|
|
131
|
+
**Verified on prod `Client_Compass` 2026-09-01: zero part numbers are duplicated at all, and zero
|
|
132
|
+
span more than one catalog**, so the branch cannot pick a wrong sibling today. Re-check this if
|
|
133
|
+
duplicate part numbers ever reappear — the shape that would produce them is the
|
|
134
|
+
[dual-catalog duplicate PO-line bug](../workflows/odp-duplicate-po-line-cleanup.md).
|
|
133
135
|
- **Only the vendor-SKU branch counts candidates.** `count($vendorSkuMatches) == 1` still gates
|
|
134
136
|
step 2, so an ambiguous SKU is **reported, never guessed** — picking one silently would ship the
|
|
135
137
|
wrong item. Recorded ambiguous SKUs: `7128204` (`AW5M5UT` / `AW5M5UT-1`), `7423324`
|
|
@@ -269,8 +271,9 @@ internal ids **7437998, 7437898, 7438098**).
|
|
|
269
271
|
`partNumber` + `catalog` 400s **`EV-12`** when that pair is not unique (send the `uuid`
|
|
270
272
|
instead); a `where` with a **single** condition still needs the `'and' =>` wrapper or api2
|
|
271
273
|
returns **500 `EO-1`** with an empty `transactionId` (this cost a deploy cycle); and dropping the
|
|
272
|
-
catalog filter
|
|
273
|
-
|
|
274
|
+
catalog filter leaves the **exact-match branch without an ambiguity check** — latent only, since
|
|
275
|
+
prod has **zero** duplicated part numbers and **zero** spanning two catalogs (verified on prod
|
|
276
|
+
2026-09-01), unlike the vendor-SKU branch which counts candidates. Corrected the
|
|
274
277
|
"nothing is written" claim — the failing runs left zero-item ODP SOs `481137047001` /
|
|
275
278
|
`481143381001` despite the email saying nothing was created. Verified end to end on prod (all
|
|
276
279
|
three POs imported and reached NetSuite). (bala)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass USA — intentional two-catalog Item model (store vs Agilant procurement)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: compass-usa
|
|
6
|
+
type: client-feature
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-09-01
|
|
9
|
+
owners: ["jcardinal"]
|
|
10
|
+
files:
|
|
11
|
+
- library/app/api/toga2.php
|
|
12
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
13
|
+
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
14
|
+
related:
|
|
15
|
+
- ../workflows/catalog-fold-reversal.md
|
|
16
|
+
- ./mits-sales-order-transmission-alerting.md
|
|
17
|
+
- ../../../1.0/apps/worker/workflows/netsuite-duplicate-items-dedup.md
|
|
18
|
+
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
Compass USA keeps its items in **two intentional catalogs**, and the **same part number
|
|
24
|
+
legitimately exists once per catalog**. These are **not duplicates** — folding them together
|
|
25
|
+
broke MITS transmission platform-wide (see
|
|
26
|
+
[catalog-fold reversal](../workflows/catalog-fold-reversal.md)). `Client_Compass.Catalogs`:
|
|
27
|
+
|
|
28
|
+
| id | name | Purpose |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 1 | **Compass** | Customer / store catalog. Items carry store classification (`assetType`, `category`, `type`). |
|
|
31
|
+
| 2 | **Agilant** | Procurement / NetSuite catalog. Items are synced from NetSuite; **no** classification fields. |
|
|
32
|
+
| 3 | Office Depot | Empty. |
|
|
33
|
+
|
|
34
|
+
**Rule: a part number that appears in both catalogs is one item per catalog, on purpose.**
|
|
35
|
+
Any dedup/merge that treats cross-catalog same-part-number rows as duplicates is wrong for
|
|
36
|
+
this client.
|
|
37
|
+
|
|
38
|
+
## Customer → catalog rule (which catalog an order resolves to)
|
|
39
|
+
|
|
40
|
+
Confirmed by the developer and by pre-merge data:
|
|
41
|
+
|
|
42
|
+
- **`customerId = 2` (Compass) sales orders → catalog 1 (store) items.**
|
|
43
|
+
- Everyone else — **Office Depot `customerId = 1`, Agilant `customerId = 3`** → catalog 2
|
|
44
|
+
(Agilant) items.
|
|
45
|
+
|
|
46
|
+
Verified pre-merge (2026-08-31 snapshot): `customerId = 2` order lines were ~100% on
|
|
47
|
+
catalog 1 (**182,787 vs 1**).
|
|
48
|
+
|
|
49
|
+
**Every catalog re-point MUST be scoped by `customerId`.** An ad-hoc
|
|
50
|
+
`UPDATE ... WHERE itemId = <agilantId>` with no customer filter wrongly moved **687
|
|
51
|
+
Office-Depot lines** onto a Compass item and had to be corrected. Scope by `customerId = 2`.
|
|
52
|
+
|
|
53
|
+
## Why the store catalog matters for MITS
|
|
54
|
+
|
|
55
|
+
The MITS transmit cron needs the **store** classification. When a Compass order line resolves
|
|
56
|
+
to an **Agilant** (catalog-2) item, that item has `assetType`/`category`/`type` NULL, so the
|
|
57
|
+
payload `assetType` is null and **MITS rejects the whole order** (HTTP 400 "The AssetType
|
|
58
|
+
field is required"). So the customer→catalog mapping is load-bearing, not cosmetic. See
|
|
59
|
+
[MITS sales-order transmit alerting](./mits-sales-order-transmission-alerting.md).
|
|
60
|
+
|
|
61
|
+
## How NetSuite → TOGa Supply sync interacts with the catalogs
|
|
62
|
+
|
|
63
|
+
The NetSuite → TOGa Supply sync is **Agilant-catalog-only**. This has two durable
|
|
64
|
+
consequences for Compass:
|
|
65
|
+
|
|
66
|
+
- **Item CREATE always stamps `catalog: Agilant` and sets NO classification.**
|
|
67
|
+
`App_Api_Toga2::getCreateItem` (`library/app/api/toga2.php` ~L6194-6270) builds the create
|
|
68
|
+
payload with `catalog => Agilant` and no `assetType`/`category`/`type`; the item lookup in
|
|
69
|
+
`worker/crons/toga2/netsuite/common_sync_togasupply.php` is built off the Agilant catalog
|
|
70
|
+
only. So **new items are born on catalog 2, unclassified** — and a Compass order line landing
|
|
71
|
+
on such an item fails MITS.
|
|
72
|
+
- **Item UPDATE only PUTs `inventoryType` / `isFulfillable`, never `assetType`/`category`/
|
|
73
|
+
`type`.** So once classification is set on an item it is **durable across sync runs** — the
|
|
74
|
+
sync will not revert it. The problem is only **new** items, which arrive unclassified.
|
|
75
|
+
|
|
76
|
+
**Consequence for any data repair:** because the sync re-creates Compass order lines pointing
|
|
77
|
+
at Agilant items, a store-catalog reversal is only durable if the **Compass NetSuite sync is
|
|
78
|
+
stopped first**. Re-enabling it risks re-pointing new/re-synced Compass orders back to Agilant
|
|
79
|
+
(2 such lines were observed right after a test re-enable). A proper long-term fix is a
|
|
80
|
+
sync-lookup change so `customerId = 2` resolves to catalog 1 — outstanding.
|
|
81
|
+
|
|
82
|
+
## Gotchas / known issues
|
|
83
|
+
|
|
84
|
+
- **Same part number in both catalogs is NOT a duplicate.** The cross-client
|
|
85
|
+
[duplicate-items dedup](../../../1.0/apps/worker/workflows/netsuite-duplicate-items-dedup.md)
|
|
86
|
+
treated Compass's catalog-1 twins as dupes and folded 199 of them into Agilant, causing the
|
|
87
|
+
MITS outage. Any dedup on this client must be **catalog-aware**.
|
|
88
|
+
- **The MITS crons hold API credentials inline** in the transmit files — treat them as
|
|
89
|
+
secret-bearing when copying snippets anywhere.
|
|
90
|
+
- Catalog 3 (Office Depot) is empty — do not assume Office Depot orders use it; ODP customer
|
|
91
|
+
`customerId = 1` resolves to catalog 2.
|
|
92
|
+
|
|
93
|
+
## Change history
|
|
94
|
+
- 2026-09-01 — Documented Compass USA's intentional two-catalog item model (1 = Compass/store
|
|
95
|
+
with classification, 2 = Agilant/NetSuite procurement without), the `customerId = 2` →
|
|
96
|
+
catalog 1 rule (verified 182,787 vs 1 pre-merge), and the mandatory `customerId` scope on any
|
|
97
|
+
catalog re-point (an unscoped one moved 687 Office-Depot lines). Recorded that the NetSuite →
|
|
98
|
+
TOGa Supply sync is Agilant-catalog-only: CREATE stamps `catalog: Agilant` with no
|
|
99
|
+
classification (new items born unclassified → MITS 400), while UPDATE never touches
|
|
100
|
+
classification (so a set value is durable). Written after the cross-client dedup folded 199
|
|
101
|
+
catalog-1 twins into Agilant and broke MITS. (jcardinal)
|
|
102
|
+
</content>
|
|
103
|
+
</invoke>
|
|
@@ -31,6 +31,8 @@ related:
|
|
|
31
31
|
- workflows/granting-persona-bundle-access.md
|
|
32
32
|
- workflows/granting-navigation-access.md
|
|
33
33
|
- features/mits-sales-order-transmission-alerting.md
|
|
34
|
+
- features/two-catalog-item-model.md
|
|
35
|
+
- workflows/catalog-fold-reversal.md
|
|
34
36
|
- features/asn-to-item-fulfillment.md
|
|
35
37
|
- features/order-fulfillment-status-per-line.md
|
|
36
38
|
- features/cost-centers.md
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass USA — reversing the 199-item catalog fold (MITS outage recovery)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: compass-usa
|
|
6
|
+
type: workflow
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-09-01
|
|
9
|
+
owners: ["jcardinal"]
|
|
10
|
+
files:
|
|
11
|
+
- test/team/2.0 deployment/duplicate_items_merge_ALL_CLIENTS.sql
|
|
12
|
+
- test/team/2.0 deployment/duplicate_items_merge_analysis.sql
|
|
13
|
+
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
14
|
+
- library/app/api/toga2.php
|
|
15
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
16
|
+
related:
|
|
17
|
+
- ../features/two-catalog-item-model.md
|
|
18
|
+
- ../features/mits-sales-order-transmission-alerting.md
|
|
19
|
+
- ../../../1.0/apps/worker/workflows/netsuite-duplicate-items-dedup.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
The **root cause** of the Compass MITS transmission outage (the `assetType`-NULL and
|
|
25
|
+
duplicate-vendor symptoms) was an ad-hoc cross-client **duplicate-items merge** that wrongly
|
|
26
|
+
folded Compass's **two intentional catalogs** together. This workflow records the damage and
|
|
27
|
+
the **durable playbook** used to reverse it. See the
|
|
28
|
+
[two-catalog item model](../features/two-catalog-item-model.md) for why the catalogs are
|
|
29
|
+
intentional.
|
|
30
|
+
|
|
31
|
+
## What broke (root cause)
|
|
32
|
+
|
|
33
|
+
An ad-hoc merge run **2026-08-31 ~12:43** — `test/team/2.0 deployment/
|
|
34
|
+
duplicate_items_merge_ALL_CLIENTS.sql`, Compass block lines **1201-1297**, committed — treated
|
|
35
|
+
Compass's two catalogs as duplicates and folded **199 catalog-1 (store) items into their
|
|
36
|
+
catalog-2 (Agilant) canonical**. Its canonical rule was **Agilant-preferred**:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
ROW_NUMBER() OVER (PARTITION BY UPPER(partNumber)
|
|
40
|
+
ORDER BY (catalog='Agilant' AND manufacturerId IS NOT NULL) DESC,
|
|
41
|
+
(catalog='Agilant') DESC,
|
|
42
|
+
(manufacturerId IS NOT NULL) DESC, id)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For each of the ~35 reference tables it ran
|
|
46
|
+
`UPDATE IGNORE <t> SET fk=canon WHERE fk=dupe; DELETE <t> WHERE fk=dupe;` — so **collision
|
|
47
|
+
rows were permanently DELETED** — then `DELETE Items` on the Compass items. Its helper maps
|
|
48
|
+
(`_merge_map` / `_unit_map` / `_vi_map`) were dropped, so **prod cannot self-reverse**.
|
|
49
|
+
|
|
50
|
+
**Two failure signatures resulted:**
|
|
51
|
+
1. Compass sales orders resolved to Agilant items lacking store classification
|
|
52
|
+
(`assetType`/`category`/`type`) → **MITS HTTP 400** "The AssetType field is required".
|
|
53
|
+
2. The Agilant items now had **duplicate active VendorItems** (the merge fused them), so the
|
|
54
|
+
transmit's `c_preferredVendorId` scalar subquery returned **>1** → **SQL 1242**, aborting
|
|
55
|
+
the whole run.
|
|
56
|
+
|
|
57
|
+
**Not all of the merge was damage.** Of 550 total deleted dupes it correctly removed **150
|
|
58
|
+
NULL-catalog junk items** and **199 duplicate-Agilant items** (from the NetSuite
|
|
59
|
+
inventory-adjustment item-create bug). **Only the 199 catalog-1 → catalog-2 folds were the
|
|
60
|
+
damage.**
|
|
61
|
+
|
|
62
|
+
## The reversal playbook (durable)
|
|
63
|
+
|
|
64
|
+
1. **Restore a pre-merge snapshot as a scratch DB** — e.g. `Client_Compass_20260831_120000`
|
|
65
|
+
(taken ~12:00, before the ~12:43 merge). **Validate it is pre-merge**: catalog-1 twins
|
|
66
|
+
present, `customerId = 2` lines on catalog 1.
|
|
67
|
+
2. **Rebuild the merge's damage map from the snapshot** — the Agilant-preferred ranking above,
|
|
68
|
+
filtered to `dupe.catalog = 1 AND canon.catalog = 2` = the 199.
|
|
69
|
+
3. **Write a set-based, single-transaction, FK-checks-ON, NO `IGNORE`, COMMIT-left-commented
|
|
70
|
+
script** (`SNAP_DB.` / `TARGET_DB.` qualifiers) that:
|
|
71
|
+
- Restores the 199 Compass `Items` + their `VendorItems` / `VendorItemCosts` **by id**
|
|
72
|
+
(`NOT EXISTS` guard).
|
|
73
|
+
- **Re-points current `customerId = 2` `SalesOrderItems`** from the Agilant canonical back
|
|
74
|
+
to the Compass twin — **on CURRENT data, not a uuid-restore.** The sync re-created order
|
|
75
|
+
lines with **new uuids**, so restoring snapshot lines by uuid **collided 1062** on
|
|
76
|
+
`UNIQUE(salesOrderId, lineNumber)`.
|
|
77
|
+
4. **Rehearse on a same-day full prod clone** — this run: ~49,267 re-points, 199 items, 0
|
|
78
|
+
orphans, held items 0-missing-assetType / 0-multi-vendor.
|
|
79
|
+
5. **Apply to prod with a pre-run backup.** Result: queue drained to 0 pending; all
|
|
80
|
+
`customerId = 2` lines back on catalog 1.
|
|
81
|
+
|
|
82
|
+
**CTO review gated this** (rehearse-on-clone, fail-loud, customer-scoped, tenant-locked).
|
|
83
|
+
|
|
84
|
+
## Critical rules
|
|
85
|
+
|
|
86
|
+
- **Scope every catalog re-point by `customerId`.** An unscoped
|
|
87
|
+
`UPDATE ... WHERE itemId = <agilantId>` wrongly moved **687 Office-Depot lines** onto the
|
|
88
|
+
Compass item and had to be corrected.
|
|
89
|
+
- **Re-point on CURRENT data, never restore order lines by uuid** — the sync minted new uuids,
|
|
90
|
+
so a uuid-restore collides on `UNIQUE(salesOrderId, lineNumber)` (1062).
|
|
91
|
+
- **Stop the Compass NetSuite sync first.** The sync re-creates Compass order lines on Agilant
|
|
92
|
+
items, so the reversal is only durable if the sync is stopped; re-enabling risks re-pointing
|
|
93
|
+
new/re-synced orders back to Agilant (2 lines seen after a test re-enable). See the
|
|
94
|
+
[two-catalog item model](../features/two-catalog-item-model.md).
|
|
95
|
+
- **Never `UPDATE IGNORE` + delete-leftover for a catalog operation** — it permanently deletes
|
|
96
|
+
collision rows and cannot be reversed. Use FK-checks-ON, fail-loud, single transaction.
|
|
97
|
+
|
|
98
|
+
## Blast radius — other clients (OUTSTANDING)
|
|
99
|
+
|
|
100
|
+
The `duplicate_items_merge_ALL_CLIENTS.sql` ran `USE Client_<X>` on **17 clients**: Masonite,
|
|
101
|
+
Ncci, Erau, Shrss, Wmchealth, Trividiahealth, Prudential, Miamidade, Browardsheriff, Nychh,
|
|
102
|
+
Canon, Quad, **Compass**, Elite, Growrk, Spglobal, Endeavorhealth.
|
|
103
|
+
|
|
104
|
+
- **Not all are damaged.** The NYCHH run was a legitimate dedup.
|
|
105
|
+
- **At-risk = clients with the two-catalog + TOGa Commerce setup like Compass.** A per-client
|
|
106
|
+
assessment (customer-scoped, rehearsed) is **outstanding for a future session**.
|
|
107
|
+
- **Compass Canada was NOT damaged and is structurally immune** — it was not in the merge's
|
|
108
|
+
client list, has a single catalog, and does not use NetSuite (so no dupe-item bug).
|
|
109
|
+
|
|
110
|
+
## Change history
|
|
111
|
+
- 2026-09-01 — Recorded the root cause of the Compass MITS outage: the ad-hoc cross-client
|
|
112
|
+
`duplicate_items_merge_ALL_CLIENTS.sql` (2026-08-31 ~12:43) folded 199 Compass catalog-1
|
|
113
|
+
store items into their Agilant canonical, producing MITS HTTP 400 (NULL assetType) and SQL
|
|
114
|
+
1242 (fused duplicate VendorItems). Documented the durable reversal playbook (restore
|
|
115
|
+
pre-merge snapshot → rebuild damage map → set-based FK-checks-ON re-point of CURRENT
|
|
116
|
+
customerId=2 lines, NOT a uuid-restore → rehearse on a same-day clone → apply with backup),
|
|
117
|
+
the customer-scope rule (an unscoped re-point moved 687 Office-Depot lines), the
|
|
118
|
+
stop-the-sync-first requirement, and the 17-client blast radius with the outstanding
|
|
119
|
+
per-client assessment. CTO-gated. (jcardinal)
|
|
120
|
+
</content>
|
package/package.json
CHANGED