toga-ai 1.0.723 → 1.0.725
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 +18 -4
- package/knowledge/2.0/apps/_underscore/INDEX.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/calculated-sql-fields.md +9 -1
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +35 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/workflows/catalog-fold-reversal.md +31 -4
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +17 -1
- package/knowledge/clients/quad/INDEX.md +1 -0
- package/knowledge/clients/quad/profile.md +9 -1
- package/knowledge/clients/quad/workflows/catalog-fold-reversal.md +130 -0
- package/package.json +1 -1
|
@@ -111,13 +111,27 @@ duplicates** and must be deployed first — see
|
|
|
111
111
|
- **`UPDATE IGNORE ...; DELETE ... WHERE fk=dupe` permanently DELETES collision rows** and drops its
|
|
112
112
|
helper maps, so prod cannot self-reverse. A reversal needs a **pre-merge snapshot** restored as a
|
|
113
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
|
|
115
|
-
(Masonite, Ncci, Erau, Shrss, Wmchealth, Trividiahealth,
|
|
116
|
-
Nychh, Canon, Quad, Compass, Elite, Growrk, Spglobal,
|
|
117
|
-
|
|
114
|
+
- **Blast radius of the 2026-08-31 `duplicate_items_merge_ALL_CLIENTS.sql` (assessment complete
|
|
115
|
+
2026-09-01):** it ran on 17 clients (Masonite, Ncci, Erau, Shrss, Wmchealth, Trividiahealth,
|
|
116
|
+
Prudential, Miamidade, Browardsheriff, Nychh, Canon, Quad, Compass, Elite, Growrk, Spglobal,
|
|
117
|
+
Endeavorhealth). **Only 2 were damaged — Compass and Quad, both two-catalog TOGa Commerce
|
|
118
|
+
clients.** The other 15 needed no reversal: 13 single-catalog "Agilant"-only clients were a
|
|
119
|
+
legitimate within-catalog dedup, NYCHH mixed Etilize + NetSuite in one catalog (no fold
|
|
120
|
+
possible), and Prudential was cleared (single populated catalog; NULL `assetType` is its normal
|
|
121
|
+
state). Triage per client with `Catalogs` LEFT JOIN `Items` (empty catalogs still show) + the
|
|
122
|
+
pipeline signal (`c_etilizeProductId` = store vs `c_netsuiteInternalItemId` = Agilant); a client
|
|
123
|
+
is damaged only if it has a two-catalog store/procurement design AND catalog-1 was folded into
|
|
124
|
+
catalog-2 AND storefront/transmit depends on catalog-1. See the Compass
|
|
125
|
+
[catalog-fold reversal](../../../clients/compass-usa/workflows/catalog-fold-reversal.md) and
|
|
126
|
+
[Quad catalog-fold reversal](../../../clients/quad/workflows/catalog-fold-reversal.md).
|
|
118
127
|
|
|
119
128
|
## Change history
|
|
120
129
|
|
|
130
|
+
- 2026-09-01 — **Blast-radius assessment complete.** Of the 17 clients the merge ran on, only 2
|
|
131
|
+
were damaged — Compass and Quad (both two-catalog TOGa Commerce). The other 15 needed no
|
|
132
|
+
reversal (13 single-catalog "Agilant"-only = legitimate within-catalog dedup; NYCHH mixed
|
|
133
|
+
Etilize + NetSuite in one catalog; Prudential cleared — single populated catalog, NULL
|
|
134
|
+
assetType is normal). Added the durable triage method and linked the Quad reversal. (jcardinal)
|
|
121
135
|
- 2026-09-01 — **⚠ Corrected the success claim.** "0 duplicate part numbers" is NOT a clean-run proof
|
|
122
136
|
for a multi-catalog client. This merge is catalog-blind (groups on `UPPER(partNumber)` only), so for
|
|
123
137
|
Compass it folded 199 intentional catalog-1 store items into their Agilant canonical, broke MITS
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
|
|
11
11
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
12
12
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
13
|
-
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, api2/Component/Api/V2/V2.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
13
|
+
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/PurchaseOrder.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, api2/Component/Api/V2/V2.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
14
14
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
15
15
|
| [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
|
|
16
16
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
| [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | A resolver capability that makes a surface `FIELD` element carry its **input contract** — max length, validator name, and whether the caller may write it — inst | _underscore/Model/Core/Surface.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/AclFieldPermission.php, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, api2/Component/Api/V2/V2.php |
|
|
47
47
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Client/Language.php, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, toga25-supply/src/App.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/contexts/AuthContext.tsx, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Core/2026-08-21a - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Compass/2026-08-04a - ApprovalDetailsAssignedManagerPreferredStage.sql, _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-12a - NavigationSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-12a - NavigationRoleGrants.sql, toga25-supply/surface-layer-guide.md |
|
|
48
48
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
49
|
-
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, _underscore/Model.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql |
|
|
49
|
+
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, _underscore/Model.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql |
|
|
50
50
|
| [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
|
|
51
51
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Client_Quad/2026-08-26a - BackfillQuadUnitLevelTrackingSingleTn.sql, dbchanges2/Client_Quad/2026-08-26b - BackfillQuadItemLevelTrackingSingleTn.sql, dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql |
|
|
52
52
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | _underscore/Model/Client/ServiceRequest.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql |
|
|
@@ -6,10 +6,11 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [snaredla, jcardinal, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model.php
|
|
13
|
+
- _underscore/Model/Client/PurchaseOrder.php
|
|
13
14
|
- _underscore/Model/Client/SalesOrder.php
|
|
14
15
|
- _underscore/Model/Client/ServiceRequest.php
|
|
15
16
|
- _underscore/Model/Elite/SalesOrder.php
|
|
@@ -210,6 +211,13 @@ characters — that is the standard of evidence for touching a field that runs o
|
|
|
210
211
|
[save-cascade stored-field deadlocks](./model-save-parent-cascade-stored-field-deadlock.md).
|
|
211
212
|
|
|
212
213
|
## Change history
|
|
214
|
+
- 2026-09-01 — Added `_shipTo` to the base `_Model_Client_PurchaseOrder` (`public $_shipTo =
|
|
215
|
+
self::FIELD_SQL;` + static `_shipTo($table = self::TABLE)` returning a correlated subquery
|
|
216
|
+
`SELECT Addresses.addressee FROM Addresses WHERE Addresses.id = $table.shipToAddressId`),
|
|
217
|
+
paralleling `SalesOrders._shipTo`. Registered here via a **`Core.RecordFields`** row (id 2555,
|
|
218
|
+
recordId 17 `purchase-orders`, type STRING, mirroring `SalesOrders._shipTo` id 626) —
|
|
219
|
+
i.e. the Core-registration path, not the `CustomRecordFields`-only path this doc shows is also
|
|
220
|
+
sufficient. Additive and shared: it renders only where a view references it. (jcardinal)
|
|
213
221
|
- 2026-08-26 - Documented **how to override a calculated field for one client / sub-client**: api2
|
|
214
222
|
resolves the model class by `str_replace('_Model_Client_', '_Model_' . $clientIdentifier . '_', ...)`
|
|
215
223
|
(`Component/Api/V2/V2.php`), and because the expression is invoked as
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [bala, apeterson]
|
|
9
|
+
updated: 2026-09-01
|
|
10
|
+
owners: [bala, apeterson, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
13
13
|
- _underscore/Model.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
18
18
|
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
19
19
|
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
20
|
+
- dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
|
|
20
21
|
related:
|
|
21
22
|
- ../../api2/features/tableview-field-metadata.md
|
|
22
23
|
- ./page-meta-context-field-settings.md
|
|
@@ -193,6 +194,28 @@ grants of any kind — `Records` **351** (`transfer-order-stages`) had none befo
|
|
|
193
194
|
`Client_Nychh/2026-08-28a`, so the Status column of a brand-new view would have vanished silently.
|
|
194
195
|
Check the joined record's grants before assuming an existing record is grant-covered.
|
|
195
196
|
|
|
197
|
+
## A third mode: an INNER join to an EMPTY FK returns an EMPTY GRID — not an error
|
|
198
|
+
|
|
199
|
+
The two `meta()` failures above both throw. There is a third, quieter mode that is **not an
|
|
200
|
+
error at all**: a view loads, the header/meta looks right, and the data grid shows **zero rows**.
|
|
201
|
+
|
|
202
|
+
Cause: an **INNER** `TableViewJoins` on a foreign-key column that is **unpopulated for that
|
|
203
|
+
tenant** drops every row, because an inner join with no matching parent row returns nothing.
|
|
204
|
+
Confirmed 2026-09-01 on the NYCHH `purchase-orders` view: `Client_Nychh.PurchaseOrders` had **0 of
|
|
205
|
+
175 rows** with a populated `locationId`, so an INNER `Locations` join
|
|
206
|
+
(`Locations.id = PurchaseOrders.locationId`) emptied the whole grid. The meta API looked correct;
|
|
207
|
+
the data API just returned 0 results.
|
|
208
|
+
|
|
209
|
+
- **The blast radius is per tenant.** The same view can serve fine for a client whose FK is
|
|
210
|
+
populated and go empty for one whose FK is not — so it reads as "the grid is broken for this
|
|
211
|
+
client only", exactly like an ACL gap, but the join type is the cause.
|
|
212
|
+
- **Fix / avoid:** for an **optional / possibly-empty** FK, use **LEFT OUTER** (rows survive with
|
|
213
|
+
nulls in the joined columns) or omit the column entirely. Only use INNER when the FK is
|
|
214
|
+
guaranteed populated for the tenant.
|
|
215
|
+
- **Check before you add:** verify FK population in the target tenant
|
|
216
|
+
(`SELECT COUNT(*) ... WHERE <fk> IS NOT NULL`) before choosing INNER. NYCHH's `purchase-orders`
|
|
217
|
+
ultimately **dropped** the Location (and Ship To) column rather than switch to OUTER.
|
|
218
|
+
|
|
196
219
|
## Context RecordFieldSettings are OPTIONAL for a joined column to render
|
|
197
220
|
|
|
198
221
|
`meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the
|
|
@@ -251,9 +274,19 @@ Core-level reference ids used (identical across all clients and environments): *
|
|
|
251
274
|
read the missing log row as "the request never happened".
|
|
252
275
|
- **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table
|
|
253
276
|
is what breaks chained joins.
|
|
277
|
+
- **An INNER join to an EMPTY FK empties the whole grid with NO error.** Third failure mode: an
|
|
278
|
+
inner join on an FK that is unpopulated for the tenant drops every row (meta looks fine, data
|
|
279
|
+
returns 0). Use LEFT OUTER for an optional FK, or verify FK population before choosing INNER.
|
|
254
280
|
|
|
255
281
|
## Change history
|
|
256
282
|
|
|
283
|
+
- 2026-09-01 — Added a **third `meta()` failure mode that is not an error**: an **INNER** join to a
|
|
284
|
+
foreign key that is **unpopulated for the tenant** returns an **empty grid** (meta correct, data
|
|
285
|
+
0 rows), because an inner join with no matching parent row drops everything. Found on NYCHH's
|
|
286
|
+
`purchase-orders` view — `Client_Nychh.PurchaseOrders` had 0 of 175 rows with a populated
|
|
287
|
+
`locationId`, so an INNER `Locations` join emptied the grid. Rule: use LEFT OUTER (or omit the
|
|
288
|
+
column) for an optional/possibly-empty FK, and verify FK population in the target tenant before
|
|
289
|
+
using INNER. (jcardinal)
|
|
257
290
|
- 2026-08-28 — Separated the **two distinct ways `meta()` 500s**, after Compass Canada's
|
|
258
291
|
`item-fulfillments-for-sales-order(-item)s` views returned 500 while Compass USA's served fine.
|
|
259
292
|
(a) A join pointing at a **deleted `Core.Records` / `Core.RecordFields` row** throws from
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 75 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 58 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 15 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -95,19 +95,46 @@ damage.**
|
|
|
95
95
|
- **Never `UPDATE IGNORE` + delete-leftover for a catalog operation** — it permanently deletes
|
|
96
96
|
collision rows and cannot be reversed. Use FK-checks-ON, fail-loud, single transaction.
|
|
97
97
|
|
|
98
|
-
## Blast radius — other clients (
|
|
98
|
+
## Blast radius — other clients (ASSESSMENT COMPLETE)
|
|
99
99
|
|
|
100
100
|
The `duplicate_items_merge_ALL_CLIENTS.sql` ran `USE Client_<X>` on **17 clients**: Masonite,
|
|
101
101
|
Ncci, Erau, Shrss, Wmchealth, Trividiahealth, Prudential, Miamidade, Browardsheriff, Nychh,
|
|
102
102
|
Canon, Quad, **Compass**, Elite, Growrk, Spglobal, Endeavorhealth.
|
|
103
103
|
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
**Verdict (assessment complete 2026-09-01): only 2 of 17 were damaged — Compass (reversed prior
|
|
105
|
+
session) and Quad. The other 15 needed no reversal.**
|
|
106
|
+
|
|
107
|
+
- **13 single-catalog "Agilant"-only clients** (Masonite, Ncci, Erau, Shrss, Wmchealth,
|
|
108
|
+
Trividiahealth, Miamidade, Browardsheriff, Canon, Elite, Growrk, Spglobal, Endeavorhealth):
|
|
109
|
+
the merge was a **legitimate within-catalog dedup** — no reversal needed.
|
|
110
|
+
- **NYCHH:** confirmed the legit-dedup pilot — one catalog "Agilant" mixing 221 Etilize +
|
|
111
|
+
NetSuite items in the **same** catalog, so no cross-catalog fold was possible.
|
|
112
|
+
- **Prudential: CLEARED** — all 50k+ order lines were always on its single populated "Agilant"
|
|
113
|
+
catalog; its "USA"/"India"/"Ireland" catalogs are empty placeholders; uniform NULL-`assetType`
|
|
114
|
+
is its **normal** state, not damage.
|
|
115
|
+
- **Quad: DAMAGED** — a two-catalog TOGa Commerce client like Compass, but the damage was
|
|
116
|
+
**storefront-only** (not an order-transmission outage). Reversed with a self-contained
|
|
117
|
+
sandbox-sourced patch. See the
|
|
118
|
+
[Quad catalog-fold reversal](../../quad/workflows/catalog-fold-reversal.md).
|
|
107
119
|
- **Compass Canada was NOT damaged and is structurally immune** — it was not in the merge's
|
|
108
120
|
client list, has a single catalog, and does not use NetSuite (so no dupe-item bug).
|
|
109
121
|
|
|
122
|
+
**Triage method (durable).** Per `Client_<X>`, compare `Catalogs` LEFT JOIN `Items` (so EMPTY
|
|
123
|
+
catalogs still show) with item counts per catalog plus the pipeline signal
|
|
124
|
+
(`c_etilizeProductId` = Etilize/store vs `c_netsuiteInternalItemId` = NetSuite/Agilant). A
|
|
125
|
+
client is **DAMAGED only if** it has an intentional two-catalog store/procurement design **AND**
|
|
126
|
+
catalog-1 items were folded into catalog-2 **AND** storefront/transmit depends on catalog-1.
|
|
127
|
+
NULL-`assetType` order lines alone are **not** damage (Prudential and Quad show that as normal).
|
|
128
|
+
|
|
110
129
|
## Change history
|
|
130
|
+
- 2026-09-01 — **Blast-radius assessment complete.** Resolved the outstanding per-client
|
|
131
|
+
assessment: only **2 of 17** clients were damaged — Compass (reversed prior session) and Quad
|
|
132
|
+
(storefront-only, reversed this session). The other 15 needed no reversal: 13 single-catalog
|
|
133
|
+
"Agilant"-only clients were a legitimate within-catalog dedup, NYCHH mixed Etilize + NetSuite
|
|
134
|
+
in one catalog (no fold possible), and Prudential was cleared (single populated catalog; NULL
|
|
135
|
+
assetType is its normal state). Added the durable triage method (Catalogs LEFT JOIN Items +
|
|
136
|
+
per-catalog pipeline signal) and the damaged-only test. See the
|
|
137
|
+
[Quad catalog-fold reversal](../../quad/workflows/catalog-fold-reversal.md). (jcardinal)
|
|
111
138
|
- 2026-09-01 — Recorded the root cause of the Compass MITS outage: the ad-hoc cross-client
|
|
112
139
|
`duplicate_items_merge_ALL_CLIENTS.sql` (2026-08-31 ~12:43) folded 199 Compass catalog-1
|
|
113
140
|
store items into their Agilant canonical, producing MITS HTTP 400 (NULL assetType) and SQL
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
| [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
|
|
7
7
|
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
8
8
|
| [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/PurchaseOrderItem.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
9
|
-
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
9
|
+
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
@@ -16,9 +16,10 @@ project: _Underscore
|
|
|
16
16
|
client: nychh
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-
|
|
19
|
+
updated: 2026-09-01
|
|
20
20
|
owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
|
|
21
21
|
files:
|
|
22
|
+
- dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
|
|
22
23
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
23
24
|
- dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql
|
|
24
25
|
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
@@ -48,6 +49,19 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
48
49
|
run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
|
|
49
50
|
|
|
50
51
|
## Key features (this client)
|
|
52
|
+
- **`purchase-orders` table view was rebased off the wrong record (2026-09-01).** The view was
|
|
53
|
+
based on **SalesOrders (Core record 14)** but had to be **PurchaseOrders (record 17)** — it was
|
|
54
|
+
mislabeled. `Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql` flips
|
|
55
|
+
`TableViews.recordId` 14→17, drops the SalesOrders→bridge→PurchaseOrders join chain, removes the
|
|
56
|
+
"Sales Order number" column (`SalesOrders.number`, RecordFields 49), and repoints the rest to PO
|
|
57
|
+
fields — final columns **number (base field 87), dateOrder (86), total (`_total` 1429)**. Location
|
|
58
|
+
and Ship To were intentionally **omitted**: PO `locationId` is empty for this tenant, so an INNER
|
|
59
|
+
Locations join would empty the grid (see the third failure mode in
|
|
60
|
+
[tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md)). The migration also
|
|
61
|
+
seeds a **Developer-role (role 2)** record ACL grant chain (`AclRecordPermissions` →
|
|
62
|
+
`AclLogicGroups` → `AclLogicGroupExpressions`, reusing record 17's `all` expression) and reads
|
|
63
|
+
`AclFieldPermissions` for the displayed fields, resolving `roleId` by role **name**. Written
|
|
64
|
+
idempotent / uuid-keyed; applied to **prod and client-sandbox**.
|
|
51
65
|
- **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
|
|
52
66
|
and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
|
|
53
67
|
Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
|
|
@@ -112,6 +126,8 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
112
126
|
[NYCHH PO Number direction](./features/po-number-upstream-direction.md).
|
|
113
127
|
|
|
114
128
|
## Notes
|
|
129
|
+
- **Role ids in `Client_Nychh` (verified 2026-09-01):** `1 = Base`, `2 = Developer`, `3 = API`.
|
|
130
|
+
Role ids differ per client DB — resolve by **name** in migrations, never hard-code these.
|
|
115
131
|
- **PO Number is upstream-sourced** (`PurchaseOrders_SalesOrders`), which is why the downstream
|
|
116
132
|
SO→PO route and the shared `_purchaseOrders` field came back empty. Details, and the
|
|
117
133
|
now-closed-on-`_beta` fix: [NYCHH PO Number](./features/po-number-upstream-direction.md).
|
|
@@ -6,3 +6,4 @@
|
|
|
6
6
|
| [Quad multi-currency item prices & vendor item costs](features/multi-currency-item-pricing.md) | 2.0 | Quad prices the **same item in several currencies** and costs the **same vendor item in the vendor's own currency**, so both listings must show a *set* of amoun | _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, toga2-supply/src/components/ui/CurrencyAmountLines.tsx, toga2-supply/src/pages/Items/api/itemsApi.ts, toga2-supply/src/pages/VendorItems/api/vendorItemsApi.ts, toga2-supply/src/pages/VendorItems/VendorItemsPage.tsx, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, worker/crons/toga2/quad/Imports/import_items.php |
|
|
7
7
|
| [Quad: PO + ASN email importer (import_po_and_asn.php)](features/po-asn-email-import.md) | 1.0 | Quad Graphics' supplier emails Purchase Order and Advance Shipping Notice CSVs into a monitored Microsoft 365 mailbox. | worker/crons/toga2/quad/import_po_and_asn.php |
|
|
8
8
|
| [Quad Graphics](profile.md) | 2.0 | Quad Graphics is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Quad`. | |
|
|
9
|
+
| [Quad Graphics — reversing the 30-item catalog fold (storefront-assortment recovery)](workflows/catalog-fold-reversal.md) | 2.0 | Quad is the **second and only other** client damaged by the 2026-08-31 catalog-blind `duplicate_items_merge_ALL_CLIENTS.sql`. | test/team/2.0 deployment/quad_catalog_fold_reversal.sql, test/team/2.0 deployment/duplicate_items_merge_ALL_CLIENTS.sql, worker/crons/toga2/quad/workflow/1_transmit_purchase_orders_to_vendors.php |
|
|
@@ -16,10 +16,11 @@ project: _Underscore
|
|
|
16
16
|
client: quad
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-
|
|
19
|
+
updated: 2026-09-01
|
|
20
20
|
owners: ["jcardinal", "bala", "apeterson", "ajean"]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
23
|
+
- ./workflows/catalog-fold-reversal.md
|
|
23
24
|
- ./features/po-asn-email-import.md
|
|
24
25
|
- ./features/enter-po-details-vendor-selection-ev12.md
|
|
25
26
|
- ../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
@@ -134,4 +135,11 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
|
|
|
134
135
|
`acl['vendor-items']['navigation-edit-on-vendor-items']` (`Core.AclActions` id 7 on record 19);
|
|
135
136
|
Quad had id 5 only. Granted id 7 to role 7 (Global Admin). UI gate only — record 19 already
|
|
136
137
|
granted role 1 full CRUD.
|
|
138
|
+
- **Two-catalog TOGa Commerce client (like Compass).** Catalog 1 "Quad" = storefront (Etilize,
|
|
139
|
+
carries `assetType`); catalog 2 "Agilant" = procurement (NetSuite, NULL `assetType`). Unlike
|
|
140
|
+
Compass, the storefront customer `customerId = 8` has its **orders on Agilant by design**, so
|
|
141
|
+
the "Quad" catalog is display-only. This is why the 2026-08-31 catalog-blind merge damaged
|
|
142
|
+
Quad's **storefront only** (not orders/transmit), and Quad was the **only other client besides
|
|
143
|
+
Compass** that needed a reversal. See
|
|
144
|
+
[Quad catalog-fold reversal](./workflows/catalog-fold-reversal.md).
|
|
137
145
|
- This profile is a starting point; expand as more Quad-specific behavior is captured.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Quad Graphics — reversing the 30-item catalog fold (storefront-assortment recovery)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: quad
|
|
6
|
+
type: workflow
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-09-01
|
|
9
|
+
owners: ["jcardinal"]
|
|
10
|
+
files:
|
|
11
|
+
- test/team/2.0 deployment/quad_catalog_fold_reversal.sql
|
|
12
|
+
- test/team/2.0 deployment/duplicate_items_merge_ALL_CLIENTS.sql
|
|
13
|
+
- worker/crons/toga2/quad/workflow/1_transmit_purchase_orders_to_vendors.php
|
|
14
|
+
related:
|
|
15
|
+
- ../../compass-usa/workflows/catalog-fold-reversal.md
|
|
16
|
+
- ../../compass-usa/features/item-catalogs-and-duplicate-items.md
|
|
17
|
+
- ../../../1.0/apps/worker/workflows/netsuite-duplicate-items-dedup.md
|
|
18
|
+
- ../profile.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
Quad is the **second and only other** client damaged by the 2026-08-31 catalog-blind
|
|
24
|
+
`duplicate_items_merge_ALL_CLIENTS.sql`. Like Compass, Quad is a **two-catalog TOGa Commerce**
|
|
25
|
+
client, so the merge folded catalog-1 store items into their catalog-2 Agilant twins. But
|
|
26
|
+
**Quad's damage shape is different from Compass's** — it was **storefront-only**, not an
|
|
27
|
+
order-transmission outage. This workflow records the damage and the self-contained reversal.
|
|
28
|
+
See the [Compass catalog-fold reversal](../../compass-usa/workflows/catalog-fold-reversal.md)
|
|
29
|
+
for the shared playbook this refines.
|
|
30
|
+
|
|
31
|
+
## Quad's catalog model (why the merge damaged it)
|
|
32
|
+
|
|
33
|
+
- **Catalog 1 "Quad"** — the storefront catalog. Etilize/store items, carries
|
|
34
|
+
`assetType`/classification.
|
|
35
|
+
- **Catalog 2 "Agilant"** — the procurement catalog. NetSuite items, **NULL `assetType`**.
|
|
36
|
+
- **`customerId = 8` IS the Quad storefront customer, but its ORDER LINES correctly resolve to
|
|
37
|
+
catalog-2 Agilant** (verified identical prod vs client-sandbox: ~8040 Agilant / 8 Quad on
|
|
38
|
+
both sides). So Quad's customer→catalog rule **differs from Compass**: the store customer's
|
|
39
|
+
*orders* live on Agilant; the "Quad" catalog is **storefront display**. This is why orders
|
|
40
|
+
were **not** damaged for Quad, unlike Compass (whose store customer belonged on catalog-1 and
|
|
41
|
+
whose MITS transmit broke).
|
|
42
|
+
|
|
43
|
+
## What broke — storefront only
|
|
44
|
+
|
|
45
|
+
The merge deleted **30 catalog-1 "Quad" store items** (each had an Agilant `partNumber` twin)
|
|
46
|
+
and re-pointed their references onto **NULL-`assetType` Agilant** items. Corrupted storefront
|
|
47
|
+
references:
|
|
48
|
+
|
|
49
|
+
- **48** `AssortmentItems` tiles
|
|
50
|
+
- **1185** `ItemFeatures`
|
|
51
|
+
- **49** `ItemPrices`
|
|
52
|
+
- **79** `ItemImages`
|
|
53
|
+
- **32** `Personas_Items`
|
|
54
|
+
- **49** `VendorItems`
|
|
55
|
+
- **1** `SalesOrderItem`
|
|
56
|
+
|
|
57
|
+
**What did NOT break:**
|
|
58
|
+
|
|
59
|
+
- **Orders** — `customerId = 8` lines were already on Agilant by design (see catalog model).
|
|
60
|
+
- **PO → vendor transmit** (`worker/crons/toga2/quad/workflow/1_transmit_purchase_orders_to_vendors.php`)
|
|
61
|
+
— multi-vendor items went **DOWN 176 → 154** (the merge reduced them; no VendorItem fusion,
|
|
62
|
+
so no SQL 1242 exposure — contrast Compass).
|
|
63
|
+
|
|
64
|
+
**Source of truth for the correct state = a client-sandbox copy of `Client_Quad`** (id-space
|
|
65
|
+
aligned with prod, verified): 100% of `AssortmentItems` sit on catalog "Quad"; prod had 48
|
|
66
|
+
wrongly on Agilant. No recent prod backup existed.
|
|
67
|
+
|
|
68
|
+
## The reversal (self-contained, sandbox-sourced)
|
|
69
|
+
|
|
70
|
+
Deliverable: `test/team/2.0 deployment/quad_catalog_fold_reversal.sql` — rehearsed,
|
|
71
|
+
CTO-reviewed (**AGREE**), applied to prod.
|
|
72
|
+
|
|
73
|
+
1. **Reinsert the 30 deleted catalog-1 `Items` verbatim from the sandbox at their original
|
|
74
|
+
ids.** The patch is **self-contained** — no cross-DB references; the sandbox rows are
|
|
75
|
+
inlined.
|
|
76
|
+
2. **Move the merge-relocated child rows back off the Agilant canonical** by one of two proven
|
|
77
|
+
mechanisms:
|
|
78
|
+
- **(a) Precise move by row `uuid`** — for tables whose canonical (Agilant) item
|
|
79
|
+
legitimately keeps its **own** native/sibling rows: `ItemImages`, `VendorItems`,
|
|
80
|
+
`SalesOrderItems`, and (after CTO review) `AssortmentItems`, `ItemPrices`,
|
|
81
|
+
`Personas_Items`. Moving by uuid touches only the rows that were actually relocated.
|
|
82
|
+
- **(b) Map-move** (`itemId` canon→dupe via a dupe→canon map) — used **ONLY for
|
|
83
|
+
`ItemFeatures`**, safe by a data-model invariant: **nothing writes `ItemFeatures` onto
|
|
84
|
+
catalog-2 items** (the Etilize feed targets catalog-1; NetSuite creates catalog-2 items
|
|
85
|
+
with no features; the sync is stopped; sandbox canon-native `ItemFeatures` = 0).
|
|
86
|
+
- **`VendorItemCosts` ride along unchanged** (VendorItems are moved, not deleted).
|
|
87
|
+
|
|
88
|
+
## Safety model (refines the Compass playbook)
|
|
89
|
+
|
|
90
|
+
- **Single transaction, `FOREIGN_KEY_CHECKS = 1`, NO `UPDATE IGNORE`, `COMMIT` left
|
|
91
|
+
commented.**
|
|
92
|
+
- **8 fail-loud CASE-based guards** — abort via a subquery-returns-row → forced error 1242.
|
|
93
|
+
- **Row-count-conservation guards** using pre-run `@vars`.
|
|
94
|
+
- **A preflight PK-absence guard.**
|
|
95
|
+
- **Rehearsed on a LOCAL `Client_Quad` freshly restored from prod** — repeatable because the
|
|
96
|
+
commented `COMMIT` + rollback-on-disconnect leaves the DB unchanged.
|
|
97
|
+
- **Perturbation-tested** — pre-inserted a restore id and confirmed the preflight guard aborts.
|
|
98
|
+
- **CTO review drove the hardening** of the four blind guards from count-equality to
|
|
99
|
+
identity-based uuid moves.
|
|
100
|
+
|
|
101
|
+
## Critical rules / gotchas
|
|
102
|
+
|
|
103
|
+
- **NULL `assetType` on order lines ALONE is NOT proof of catalog-fold damage.** Quad (and
|
|
104
|
+
Prudential) show 100% NULL-`assetType` Agilant order lines as their **normal** state. Damage
|
|
105
|
+
requires the two-catalog store/procurement split **AND** storefront/transmit dependence on
|
|
106
|
+
catalog-1.
|
|
107
|
+
- **The merge's damage is client-specific in SHAPE.** Compass = order-transmission break (MITS
|
|
108
|
+
HTTP 400 / SQL 1242); Quad = storefront-assortment corruption only. **Assess each client's
|
|
109
|
+
actual dependency; do not assume Compass's failure mode.**
|
|
110
|
+
- **MySQL reversal/rehearsal mechanics:**
|
|
111
|
+
- Temp-table `uuid` columns need `COLLATE utf8mb4_unicode_ci` (or an explicit COLLATE on the
|
|
112
|
+
join) to match the real tables.
|
|
113
|
+
- A `TEMPORARY` table **cannot be referenced twice in one query** — inline id-lists in guards
|
|
114
|
+
instead.
|
|
115
|
+
- A `QUOTE()`-generated multi-row `INSERT` can **silently drop a trailing field** for rows
|
|
116
|
+
with several trailing NULLs — **validate field counts before running.**
|
|
117
|
+
|
|
118
|
+
## Change history
|
|
119
|
+
|
|
120
|
+
- 2026-09-01 — Recorded Quad as the second (and only other) client damaged by the catalog-blind
|
|
121
|
+
`duplicate_items_merge_ALL_CLIENTS.sql`. Documented Quad's two-catalog model (store customer
|
|
122
|
+
`customerId=8` orders live on Agilant by design, so orders were NOT damaged — unlike Compass),
|
|
123
|
+
the storefront-only damage (30 deleted catalog-1 items; 48 AssortmentItems / 1185 ItemFeatures
|
|
124
|
+
/ 49 ItemPrices / 79 ItemImages / 32 Personas_Items / 49 VendorItems / 1 SalesOrderItem
|
|
125
|
+
re-pointed onto NULL-assetType Agilant), and the self-contained sandbox-sourced reversal
|
|
126
|
+
(`quad_catalog_fold_reversal.sql`): reinsert 30 items verbatim at original ids, then move child
|
|
127
|
+
rows back by uuid (precise) or by map (ItemFeatures only, safe by the no-features-on-catalog-2
|
|
128
|
+
invariant). Safety model: single transaction, FK-checks ON, no UPDATE IGNORE, 8 fail-loud CASE
|
|
129
|
+
guards, count-conservation guards, preflight PK guard; rehearsed on a prod-restored local,
|
|
130
|
+
perturbation-tested, CTO-reviewed AGREE. (jcardinal)
|
package/package.json
CHANGED