toga-ai 1.0.680 → 1.0.682
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/2.0/apps/_underscore/INDEX.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +77 -7
- package/knowledge/2.0/apps/_underscore/features/tracking-number-bridges.md +15 -3
- package/knowledge/2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md +28 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/request-logging.md +8 -1
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +33 -1
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +20 -0
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/workflows/porting-a-page-from-toga2-supply.md +78 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-canada/INDEX.md +1 -0
- package/knowledge/clients/compass-canada/features/item-fulfillment-tableviews.md +134 -0
- package/knowledge/clients/compass-canada/profile.md +16 -1
- package/knowledge/clients/prudential/INDEX.md +1 -0
- package/knowledge/clients/prudential/features/supply-orders-csv-export.md +147 -0
- package/knowledge/clients/prudential/profile.md +6 -1
- package/package.json +1 -1
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
44
44
|
| [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 |
|
|
45
45
|
| [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 |
|
|
46
|
-
| [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, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
46
|
+
| [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 |
|
|
47
47
|
| [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 |
|
|
48
|
-
| [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 |
|
|
48
|
+
| [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 |
|
|
49
49
|
| [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 |
|
|
50
50
|
| [USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)](features/usps-dpv-deliverability.md) | **USPS returning HTTP 200 with a populated address is NOT evidence that the address is deliverable.** The authoritative signal is USPS's **DPV (Delivery Point V | _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Model/Client/Address.php, _underscore/Model/Rate/Entitlement.php |
|
|
51
51
|
| [Refreshing a Local Dev Database from Beta (dev-sandbox)](workflows/local-db-refresh-from-beta.md) | How to reset a local 2.0 dev database from the **beta / dev-sandbox** environment: dump each schema (`Core`, `Client_<Id>`, `Logs_<Id>`, …) from the beta host, | api2/Config/, _underscore/Loader.php, _underscore/Model/Client/BundleTranslation.php, api2/Component/Api/V2/V2.php, toga25-supply/sync_compasscanada_schema.sql |
|
|
@@ -6,18 +6,23 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
13
|
+
- _underscore/Model.php
|
|
14
|
+
- api2/Component/Api/V2/V2.php
|
|
13
15
|
- dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
|
|
14
16
|
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
15
17
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
18
|
+
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
16
19
|
related:
|
|
17
20
|
- ../../api2/features/tableview-field-metadata.md
|
|
18
21
|
- ./page-meta-context-field-settings.md
|
|
19
22
|
- ./tableview-hyperlink-columns.md
|
|
20
23
|
- ../../api2/features/tableview-apiwhereclause-row-filtering.md
|
|
24
|
+
- ../../api2/features/request-logging.md
|
|
25
|
+
- ../../../../clients/compass-canada/features/item-fulfillment-tableviews.md
|
|
21
26
|
---
|
|
22
27
|
|
|
23
28
|
## Summary
|
|
@@ -89,18 +94,67 @@ the all-clients template `dbchanges2/Client/2026-08-11a - ServiceRequestsInvento
|
|
|
89
94
|
Only the second hop had to be created. Always check for a usable join first; a duplicate Items join
|
|
90
95
|
would also have broken the chain above.
|
|
91
96
|
|
|
92
|
-
##
|
|
97
|
+
## Two ways `meta()` 500s — and they are not the same bug
|
|
98
|
+
|
|
99
|
+
Both look like "the table view is broken for this user", but the trigger, the blast radius and the
|
|
100
|
+
fix are different. Tell them apart before touching config.
|
|
101
|
+
|
|
102
|
+
### (a) A join pointing at a DELETED Core record/field — fatal for every caller who can see the join
|
|
103
|
+
|
|
104
|
+
The join loop builds a model for **every surviving join, unconditionally**, before a single
|
|
105
|
+
`TableViewFields` row is looked at:
|
|
106
|
+
|
|
107
|
+
```php
|
|
108
|
+
$joinOnRecordField = new _Model_Core_RecordField($tableViewJoin->joinOnRecordFieldId);
|
|
109
|
+
$joinRecord = new _Model_Core_Record($tableViewJoin->joinRecordId);
|
|
110
|
+
$parentRecordField = new _Model_Core_RecordField($tableViewJoin->parentRecordFieldId);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`_Model::__construct` (`_underscore/Model.php:716`) throws on a primary key that returns nothing:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Error during Model build for primary key id '211' for '_Model_Core_RecordField'.
|
|
117
|
+
Exactly 1 row was expected to be returned but 0 were.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Consequences worth internalising:
|
|
121
|
+
|
|
122
|
+
- **It fires for ANY caller whose roles pass the `AclRecordPermissions` filter on that join** —
|
|
123
|
+
even when **no `TableViewField` references the join at all**. An orphaned dead join is not
|
|
124
|
+
inert.
|
|
125
|
+
- Conversely, a role that *cannot* see the dead join never constructs it, so the same view can 500
|
|
126
|
+
for one user and serve fine for another. That is the usual reason a report reads as
|
|
127
|
+
"intermittent".
|
|
128
|
+
- Nothing validates `joinRecordId` / `parentRecordFieldId` against live `Core` rows, so a client DB
|
|
129
|
+
that predates a platform migration sits broken until someone opens the view. The ten-client audit
|
|
130
|
+
and the per-client fixes are on
|
|
131
|
+
[tracking-number-bridges](./tracking-number-bridges.md).
|
|
132
|
+
|
|
133
|
+
### (b) A join DROPPED by the ACL filter — harmless until a column hangs off it
|
|
93
134
|
|
|
94
135
|
The joins query **INNER JOINs `AclRecordPermissions` on `joinRecordId`**. With no row for the
|
|
95
|
-
caller's role, the join is silently dropped from the result set
|
|
96
|
-
|
|
136
|
+
caller's role, the join is silently dropped from the result set. That on its own is **fine**; it only
|
|
137
|
+
throws when a `TableViewFields` row references the now-missing join:
|
|
97
138
|
|
|
98
139
|
```
|
|
99
140
|
There is no AclRecordPermissions record permitting access to TableViewJoins.id = N for this user
|
|
100
141
|
```
|
|
101
142
|
|
|
102
|
-
So a
|
|
103
|
-
|
|
143
|
+
So a role gap only 500s when a **visible column** hangs off the missing join — which is why one
|
|
144
|
+
client tolerates ACL gaps on records another client cannot. A new joined table (here
|
|
145
|
+
`Manufacturers`, record 20) needs its `AclRecordPermissions` grant in the same migration.
|
|
146
|
+
|
|
147
|
+
### The debugging signature: 500 + EMPTY body + NO row in `Logs_<client>.Api`
|
|
148
|
+
|
|
149
|
+
Mode (a) throws *before* the response is assembled, so the caller gets a **500 with an empty
|
|
150
|
+
response body** — no `error` code, no `messages`, nothing to grep — and **no row is written to
|
|
151
|
+
`Logs_<client>.Api`** either, because the V2 engine writes its transaction log near the END of
|
|
152
|
+
`execute()`. **The absence of an Api log row is itself the signature of this class of failure**, not
|
|
153
|
+
evidence the request never arrived. Confirmed again on Compass Canada 2026-08-27; the general rule
|
|
154
|
+
for fatals lives on [request-logging](../../api2/features/request-logging.md).
|
|
155
|
+
|
|
156
|
+
Contrast mode (b), which surfaces a readable message naming a `TableViewJoins.id` — which is exactly
|
|
157
|
+
why it reads like config corruption rather than an ACL gap.
|
|
104
158
|
|
|
105
159
|
## Context RecordFieldSettings are OPTIONAL for a joined column to render
|
|
106
160
|
|
|
@@ -153,12 +207,28 @@ Core-level reference ids used (identical across all clients and environments): *
|
|
|
153
207
|
not hand-write an alias from memory — check the counter logic before relying on one.
|
|
154
208
|
- **A missing `AclRecordPermissions` row for the joined record drops the join, not just the column** —
|
|
155
209
|
and the resulting error names a `TableViewJoins.id`, which is why it reads like config corruption
|
|
156
|
-
rather than an ACL gap.
|
|
210
|
+
rather than an ACL gap. It only throws if a column references that join.
|
|
211
|
+
- **A dead Core id on a join is fatal even if nothing uses the join.** Deleting the unused
|
|
212
|
+
`TableViewFields` row does not make the view load; the join row itself has to go.
|
|
213
|
+
- **A 500 with an empty body and no `Logs_<client>.Api` row is the dead-Core-id signature.** Do not
|
|
214
|
+
read the missing log row as "the request never happened".
|
|
157
215
|
- **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table
|
|
158
216
|
is what breaks chained joins.
|
|
159
217
|
|
|
160
218
|
## Change history
|
|
161
219
|
|
|
220
|
+
- 2026-08-28 — Separated the **two distinct ways `meta()` 500s**, after Compass Canada's
|
|
221
|
+
`item-fulfillments-for-sales-order(-item)s` views returned 500 while Compass USA's served fine.
|
|
222
|
+
(a) A join pointing at a **deleted `Core.Records` / `Core.RecordFields` row** throws from
|
|
223
|
+
`_Model::__construct` (`Model.php:716`, *"Exactly 1 row was expected to be returned but 0 were"*)
|
|
224
|
+
because the join loop constructs a model for **every** ACL-visible join **before** any
|
|
225
|
+
`TableViewFields` row is considered — so it fires even when nothing references that join, and only
|
|
226
|
+
for roles that can see it. (b) A join **dropped by the ACL filter** is harmless **unless** a
|
|
227
|
+
`TableViewField` references it. Recorded the mode-(a) debugging signature: **500 with an EMPTY
|
|
228
|
+
response body and NO row in `Logs_<client>.Api`**, because the throw unwinds before the V2 engine
|
|
229
|
+
writes its transaction log. Canada's fix:
|
|
230
|
+
[Compass Canada item-fulfillment TableViews](../../../../clients/compass-canada/features/item-fulfillment-tableviews.md).
|
|
231
|
+
(bala)
|
|
162
232
|
- 2026-08-18 — Documented how `TableViewJoins` compile into SQL after adding a chained
|
|
163
233
|
**Units → Items → Manufacturers** join for the Part Number / Manufacturer / Item Description columns
|
|
164
234
|
on `inventory_units` (Elite + NYCHH): `onB` is emitted with the **raw table name** (so a chain only
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "dfranks", "apeterson", "bala", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -29,9 +29,11 @@ files:
|
|
|
29
29
|
- dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql
|
|
30
30
|
- dbchanges2/Client_Quad/2026-08-26a - BackfillQuadUnitLevelTrackingSingleTn.sql
|
|
31
31
|
- dbchanges2/Client_Quad/2026-08-26b - BackfillQuadItemLevelTrackingSingleTn.sql
|
|
32
|
+
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
32
33
|
related:
|
|
33
34
|
- recursive-item-fulfillments.md
|
|
34
35
|
- ../../api2/features/cxml-shipnotice-gateway.md
|
|
36
|
+
- ../../../../clients/compass-canada/features/item-fulfillment-tableviews.md
|
|
35
37
|
---
|
|
36
38
|
|
|
37
39
|
## Summary
|
|
@@ -226,7 +228,7 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
226
228
|
| Client | 318 (item) | 319 (unit) | Which the views should read |
|
|
227
229
|
|---|---|---|---|
|
|
228
230
|
| Compass | 4,312 | 10,144 | 318 |
|
|
229
|
-
| CompassCanada | 169 | 138 | 318 |
|
|
231
|
+
| CompassCanada | 169 | 138 | **318** — views rebuilt onto 318 **2026-08-27** |
|
|
230
232
|
| Elite | **0** | 109 | **319** (fixed 2026-08-17) |
|
|
231
233
|
| Prudential | **0** | 380 | **319** |
|
|
232
234
|
| Quad | 0 → +5,129¹ | 633 → +779¹ | **318** — writer + backfill **fixed 2026-08-26** |
|
|
@@ -261,7 +263,10 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
261
263
|
- **The dead-id breakage spans TEN clients, not just Elite (measured 2026-08-17).**
|
|
262
264
|
`item-fulfillments-for-sales-orders` / `item-fulfillments-for-sales-order-items` still carry
|
|
263
265
|
dangling `joinRecordId` **41** and dangling `parentRecordFieldId`s **211**, **321** in: **Elite**
|
|
264
|
-
(fixed 2026-08-17), **CompassCanada
|
|
266
|
+
(fixed 2026-08-17), **CompassCanada** (the two fulfillment views fixed **2026-08-27**, but its
|
|
267
|
+
`sales-order-shipments` and `units` views still carry the same dead ids — see
|
|
268
|
+
[Compass Canada item-fulfillment TableViews](../../../../clients/compass-canada/features/item-fulfillment-tableviews.md)),
|
|
269
|
+
**Prudential, Nychh, Aig, Growrk, Managelife, Rate, SPGlobal**
|
|
265
270
|
(which has a fourth dangling id, **1431**) and **True**. **Only Compass and Quad are clean**,
|
|
266
271
|
because those two were migrated to base record 29. The per-client fix is **not a blind copy** -
|
|
267
272
|
the re-root is portable but the bridge (318 vs 319) must be chosen from that client's own row
|
|
@@ -271,6 +276,13 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
271
276
|
for record 41.
|
|
272
277
|
|
|
273
278
|
## Change history
|
|
279
|
+
- 2026-08-28 — **Compass Canada's two item-fulfillment views are off the dead-id list** — rebuilt onto
|
|
280
|
+
base record 29 with tracking through the item-level bridge **318** (Canada measures 169 rows in 318
|
|
281
|
+
vs 138 in 319, so 318 is right on its own numbers, not by copying USA):
|
|
282
|
+
`Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql`, applied to prod and
|
|
283
|
+
verified 200. Canada's `sales-order-shipments` and `units` views still carry dead record 41 /
|
|
284
|
+
fields 211+321. Also recorded that **Compass Canada's `Roles` ids are NOT Compass USA's**, so
|
|
285
|
+
`AclRecordPermissions` rows cannot be copied between the two tenants when porting a view. (bala)
|
|
274
286
|
- 2026-08-26 — **Quad's empty-Tracking-# live bug RESOLVED** — by populating **318**, not repointing
|
|
275
287
|
views to 319. Root: Quad's IFs come from NetSuite (one tracking number per IF) and 318/319 are
|
|
276
288
|
meant to **mirror** the single 317 header number; `App_Api_Toga2::syncItemFulfillmentFromNetsuite`
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: ["bala", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ServiceRequest.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
related:
|
|
15
15
|
- ./tracking-number-bridges.md
|
|
16
16
|
- ../../../clients/elite/features/supply2-tableview-config-drift.md
|
|
17
|
+
- ../../../clients/prudential/features/supply-orders-csv-export.md
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
## Summary
|
|
@@ -93,6 +94,26 @@ Coverage on `Client_Elite`: **224 of 229** units. The shipped expression selects
|
|
|
93
94
|
`IN (<fulfillment chain> UNION <ServiceRequests_Units>)` so the bridge still counts once it is
|
|
94
95
|
populated - keep the UNION rather than choosing one side.
|
|
95
96
|
|
|
97
|
+
### `ServiceRequests_Units` population is per-client - and it is many-to-many
|
|
98
|
+
|
|
99
|
+
**"0 rows" is an Elite fact, not a schema fact.** On `Client_Prudential` (measured 2026-08-27)
|
|
100
|
+
the same bridge holds **30,268 rows** and is the *only* working route from a unit to its order for
|
|
101
|
+
units that never shipped: `SalesOrderItems_CommittedUnits`, the obvious alternative, has **0 rows**
|
|
102
|
+
there. Count the rows in the client DB before deciding a bridge is dead.
|
|
103
|
+
|
|
104
|
+
Where it is populated, it is genuinely **many-to-many** and **fans rows out**. Generic/template units
|
|
105
|
+
(docks, monitors with no serial number) link to very many ServiceRequests, so
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Units -> ServiceRequests_Units -> ServiceRequests -> SalesOrders
|
|
109
|
+
-> SalesOrders_PurchaseOrders -> PurchaseOrders
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
returns one row **per distinct PurchaseOrder** the unit resolves to - on Prudential, 9,846 distinct
|
|
113
|
+
never-shipped units expand to **21,970 rows**. Any consumer that wants one row per unit must dedupe
|
|
114
|
+
by `Units.uuid`, and must accept that which PurchaseOrder "wins" is **arbitrary**. See
|
|
115
|
+
[Prudential Sales Orders CSV export](../../../clients/prudential/features/supply-orders-csv-export.md).
|
|
116
|
+
|
|
96
117
|
## Data model
|
|
97
118
|
Header-level vs item-level bridges, and the level each transaction is anchored at:
|
|
98
119
|
|
|
@@ -151,6 +172,12 @@ None — uniform. The linkage structure is framework-level and identical across
|
|
|
151
172
|
rather than reading the SQL.
|
|
152
173
|
|
|
153
174
|
## Change history
|
|
175
|
+
- 2026-08-28 - Corrected the `ServiceRequests_Units` "0 rows" note: that is an **Elite** measurement,
|
|
176
|
+
not a schema fact - `Client_Prudential` has **30,268 rows** and the bridge is the only unit-to-order
|
|
177
|
+
route there for never-shipped units (`SalesOrderItems_CommittedUnits` is the empty one on Prudential,
|
|
178
|
+
0 rows). Added that the bridge is genuinely **many-to-many and fans rows out** (9,846 units ->
|
|
179
|
+
21,970 rows on Prudential), so consumers must dedupe by `Units.uuid` and the winning PurchaseOrder
|
|
180
|
+
is arbitrary. (bala)
|
|
154
181
|
- 2026-08-18 - Added the **third linkage path, `Units.itemId`** - the only *complete* item-to-unit
|
|
155
182
|
path (Elite: 229/229 units, vs 97/229 through item receipts) - and the all-clients
|
|
156
183
|
**`inventory_units`** view built on it (base 31, Items as spine via joinRecordId 21 /
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
| [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
|
|
20
20
|
| [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
21
21
|
| [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
|
|
22
|
-
| [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
22
|
+
| [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.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 |
|
|
23
23
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
24
24
|
| [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/api/toga.ts, _underscore/Model/Client/TrackingNumber.php |
|
|
25
25
|
| [V2 request deadlock-retry — route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller can **detect a MySQL deadlock (1213) / lock-wait timeout (1205) and replay the whole request in-process**, so a transient lock collision | api2/Controller/Index.php, _underscore/Database.php, _underscore/Query.php |
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -117,6 +117,13 @@ Practical consequences:
|
|
|
117
117
|
[Compass Order Lifecycle & Data-Integrity Invariants](../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md#detecting-empty-shell-orders-left-by-a-fatal-mid-sequence).
|
|
118
118
|
- **Orphan sweeps only find PARTIAL casualties.** An attempt that rolled back completely leaves no
|
|
119
119
|
row in any table and, with no API log row, no trail at all.
|
|
120
|
+
- **It applies to plain GETs too, and the response body comes back EMPTY.** Re-confirmed 2026-08-27:
|
|
121
|
+
a `GET /v2/table-views/meta` that throws inside `_Model_Client_TableView::meta()` (a
|
|
122
|
+
`TableViewJoins` row pointing at a deleted `Core` record) returns **500 with no body at all** — no
|
|
123
|
+
`error` code, no `messages` — and `Logs_CompassCanada.Api` holds no row for it. **An empty 500
|
|
124
|
+
body plus a missing Api log row is a usable fingerprint** for "a fatal was thrown before the
|
|
125
|
+
response was assembled", and is the fastest way to rule out a handled rejection. See
|
|
126
|
+
[tableview-joins](../../_underscore/features/tableview-joins.md).
|
|
120
127
|
|
|
121
128
|
## ⚠ apiproxy's 5xx retry distorts BOTH logs — always read a 500 through `transactionId`
|
|
122
129
|
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
- dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql
|
|
19
19
|
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
20
20
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
21
|
+
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
21
22
|
related:
|
|
22
23
|
- tableview-apiwhereclause-row-filtering.md
|
|
23
24
|
- ../../_underscore/features/page-meta-context-field-settings.md
|
|
@@ -144,6 +145,29 @@ Notes that matter when writing it:
|
|
|
144
145
|
[tableview-joins](../../_underscore/features/tableview-joins.md). If the script uses `@` session
|
|
145
146
|
variables, all statements must run in **one connection**.
|
|
146
147
|
|
|
148
|
+
> **⚠ `DESCRIBE` / `information_schema` will tell you `index` is unconstrained — it is lying.**
|
|
149
|
+
> A **composite** unique key is only recorded against its **FIRST** column, so
|
|
150
|
+
> `information_schema.COLUMNS` reports `COLUMN_KEY = ''` for `index` (and for `slug`) and the column
|
|
151
|
+
> reads as free. The constraint is real, and you find out mid-script with
|
|
152
|
+
> `Duplicate entry '13-2' for key 'tableviewfields.tableViewId_index'`. Read
|
|
153
|
+
> `information_schema.STATISTICS` (or `SHOW INDEX FROM TableViewFields`) instead — that is the only
|
|
154
|
+
> place a composite key's second column appears.
|
|
155
|
+
|
|
156
|
+
### Rebuilding a whole view — order of operations
|
|
157
|
+
|
|
158
|
+
Deleting and re-creating a view's columns (not just reordering them) has two FKs to respect and one
|
|
159
|
+
lookup rule:
|
|
160
|
+
|
|
161
|
+
1. `TableViews.sortPrimaryTableViewFieldId` is an FK onto `TableViewFields.id` — `SET NULL` first.
|
|
162
|
+
2. `TableViewFields.tableViewJoinId` is an FK onto `TableViewJoins.id` — detach the columns
|
|
163
|
+
(`SET tableViewJoinId = NULL`) or delete them before deleting any join.
|
|
164
|
+
3. **Resolve `TableViews` by `slug`, never by `id`.** `TableViews` / `TableViewJoins` /
|
|
165
|
+
`TableViewFields` ids differ per client database. Two clients sharing a view id is a coincidence
|
|
166
|
+
and says nothing about the joins and fields underneath it — Compass USA and Compass Canada both
|
|
167
|
+
number these views 12 and 13, with completely different children.
|
|
168
|
+
|
|
169
|
+
Worked example: [Compass Canada item-fulfillment TableViews](../../../../clients/compass-canada/features/item-fulfillment-tableviews.md).
|
|
170
|
+
|
|
147
171
|
## Hidden (projected-but-not-rendered) columns
|
|
148
172
|
|
|
149
173
|
A `TableViewFields` row with `isVisible=0` is still selected into the row's data projection but
|
|
@@ -225,6 +249,14 @@ SalesOrderListingSortByDateOrderDesc.sql`).
|
|
|
225
249
|
*and* that query — and a grep for the property alone under-reports what a missing column breaks.
|
|
226
250
|
|
|
227
251
|
## Change history
|
|
252
|
+
- 2026-08-28 — Recorded that **`DESCRIBE` / `information_schema.COLUMNS` hides a composite unique
|
|
253
|
+
key's second column** (`COLUMN_KEY = ''` for `TableViewFields.index`), so `index` looks free right
|
|
254
|
+
up until `Duplicate entry '13-2' for key 'tableviewfields.tableViewId_index'` — read
|
|
255
|
+
`information_schema.STATISTICS` / `SHOW INDEX` instead. Added the **full-view-rebuild order of
|
|
256
|
+
operations** (`sortPrimaryTableViewFieldId` FK `SET NULL`, then detach `tableViewJoinId`, then
|
|
257
|
+
delete joins) and the rule to **resolve `TableViews` by `slug`, never by `id`** (ids differ per
|
|
258
|
+
client DB; Compass USA and Compass Canada both use 12/13 with different children). Found while
|
|
259
|
+
rebuilding Compass Canada's two item-fulfillment views. (bala)
|
|
228
260
|
- 2026-08-27 - Added **`isGroupable`** as a fourth per-view presentation flag on `TableViewFields`,
|
|
229
261
|
with the warning that it is declared in `_underscore` (`9f296a71`, 2026-08-26 — model property
|
|
230
262
|
**and** `TableView::meta()`'s hand-written SELECT) while **no `dbchanges2` migration adds it on any
|
|
@@ -171,6 +171,22 @@ phases leaves `@parentId` `NULL` and the child inserts either fail on the FK or
|
|
|
171
171
|
in the file header, and make the unguarded parent insert the one phase you skip on a re-run (set
|
|
172
172
|
`@parentId` to the existing row by hand instead).
|
|
173
173
|
|
|
174
|
+
## Dry-running a destructive migration before it goes near an environment
|
|
175
|
+
|
|
176
|
+
A rebuild file (DELETE + re-INSERT, FK detaches, index renumbers) cannot be proved safe by reading
|
|
177
|
+
it. Two cheap local checks catch almost everything, and neither needs a sandbox:
|
|
178
|
+
|
|
179
|
+
1. **Replay it against a local copy of the schema wrapped in `START TRANSACTION; … ROLLBACK;`.**
|
|
180
|
+
Every FK violation, unique-key collision and typo surfaces; nothing persists, so you can iterate
|
|
181
|
+
in seconds.
|
|
182
|
+
2. **Prove re-runnability by concatenating the file to itself** and running *both copies inside one
|
|
183
|
+
transaction*. If the second pass errors — or changes anything the first pass did not — the file is
|
|
184
|
+
not idempotent, and the executor will eventually run it twice.
|
|
185
|
+
|
|
186
|
+
Both are local-only evidence: pair them with the
|
|
187
|
+
[local-vs-prod config parity check](../workflows/local-vs-prod-mysql-config-parity.md) before
|
|
188
|
+
concluding a migration is production-safe.
|
|
189
|
+
|
|
174
190
|
## Gotchas
|
|
175
191
|
|
|
176
192
|
- **A "re-run safe" file is usually only re-run safe *per phase*.** If Phase 1 creates the parent row
|
|
@@ -185,6 +201,10 @@ in the file header, and make the unguarded parent insert the one phase you skip
|
|
|
185
201
|
[dbchanges2 architecture](../architecture.md).
|
|
186
202
|
|
|
187
203
|
## Change history
|
|
204
|
+
- 2026-08-28 — Added the **local dry-run pattern for destructive/rebuild migrations**: replay the
|
|
205
|
+
file against a local copy inside `START TRANSACTION; … ROLLBACK;`, then prove idempotence by
|
|
206
|
+
concatenating the file to itself and running both copies in one transaction. Used while writing
|
|
207
|
+
`Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql`. (bala)
|
|
188
208
|
- 2026-08-28 — **Corrected the error-1093 claim.** The doc previously said a self-referencing
|
|
189
209
|
`INSERT … NOT EXISTS` guard trips 1093 and must be wrapped in a derived table. 1093 is an
|
|
190
210
|
**UPDATE/DELETE** restriction only; tested on MySQL 8.0.30, both the `NOT IN` and `NOT EXISTS`
|
|
@@ -16,3 +16,4 @@
|
|
|
16
16
|
| [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
|
|
17
17
|
| [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
|
|
18
18
|
| [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
|
|
19
|
+
| [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Porting a page (or query) from toga2-supply to toga25-supply
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga25-supply
|
|
5
|
+
project: TOGa 2.5 Supply
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-28
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json
|
|
13
|
+
- toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts
|
|
14
|
+
- toga2-supply/src/pages/Orders/api/OrdersApi.ts
|
|
15
|
+
- toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx
|
|
16
|
+
related:
|
|
17
|
+
- ../features/client-api-fetch-fields.md
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../../../clients/prudential/features/supply-orders-csv-export.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
`toga25-supply` re-implements pages that already exist in `toga2-supply`. A port is not a copy:
|
|
25
|
+
the two repos structure API queries differently, and the upstream file **keeps moving** after the
|
|
26
|
+
port is taken. This is the procedure that catches both — written after the Prudential Sales Orders
|
|
27
|
+
export shipped with two upstream fixes silently missing.
|
|
28
|
+
|
|
29
|
+
## The structural difference (why it is not a copy/paste)
|
|
30
|
+
|
|
31
|
+
- **`toga2-supply`** inlines the query options as **JS object literals inside the API function**
|
|
32
|
+
(e.g. `getPrudentialOrders` in `src/pages/Orders/api/OrdersApi.ts`).
|
|
33
|
+
- **`toga25-supply`** externalises query projections into **JSON sidecars** under
|
|
34
|
+
`src/pages/<Page>/viewModel/FIELDS/<CLIENT_SLUG>/*.json` (per-client folder) or
|
|
35
|
+
`viewModel/FIELDS/apiFields.json` (the slug-keyed `DEFAULT` + delta map described in
|
|
36
|
+
[Client API-Fetch Fields](../features/client-api-fetch-fields.md)).
|
|
37
|
+
|
|
38
|
+
So porting a query means **editing JSON**, and every key used by the sidecar — `fields`, `join`,
|
|
39
|
+
`ojoin`, `where`, `recordsPerPage` — must also be **mirrored into the TS query type**, or it is
|
|
40
|
+
silently dropped on the way to `apiGet` and the request goes out without it.
|
|
41
|
+
|
|
42
|
+
## Steps
|
|
43
|
+
|
|
44
|
+
1. **Find the upstream source.** Locate the `toga2-supply` function/hook the 2.5 code came from
|
|
45
|
+
(the ported files should say so in a header comment — add one if they don't).
|
|
46
|
+
2. **`git log --oneline -- <upstream file>` before you trust the port.** This is the step that
|
|
47
|
+
matters. A port is a snapshot; upstream keeps getting fixed. On the Prudential export this
|
|
48
|
+
surfaced two later commits the port never had (`bfaae3a9c` "Fix Prudential contact mapping",
|
|
49
|
+
`d4c0a612f` "Link units via ServiceRequests_Units and dedupe by uuid"). Read every commit dated
|
|
50
|
+
after the port and decide, per commit, whether it applies.
|
|
51
|
+
3. **Move the query into a JSON sidecar** and mirror its keys into the TS query type.
|
|
52
|
+
4. **Diff the projection mechanically, not by eye.** Brace-match the object literals out of the
|
|
53
|
+
`toga2-supply` `.ts`, `eval` them in node, normalise each join entry to a
|
|
54
|
+
`"Table ON left=right"` string, and set-diff against the 2.5 JSON. That proves
|
|
55
|
+
`fields` / `join` / `ojoin` / `where` / `order` match exactly. The same set-diff over the output
|
|
56
|
+
column headers proves the produced file is identical (22 columns, for the Prudential export).
|
|
57
|
+
5. **Decide the deliberate divergences explicitly** and record them in the feature doc. 2.5 is
|
|
58
|
+
allowed to be better than 2.0 — e.g. the export's `formatContactName()` emits an empty cell where
|
|
59
|
+
2.0 emits the literal `"undefined undefined"` — but a divergence nobody wrote down reads as a
|
|
60
|
+
port bug six months later.
|
|
61
|
+
6. **Re-check the join semantics against the client's data**, don't inherit them. An upstream join
|
|
62
|
+
can be correct for one client's data and empty for another's (`SalesOrderItems_CommittedUnits`
|
|
63
|
+
has 0 rows in `Client_Prudential`). Count rows in the client DB before shipping.
|
|
64
|
+
|
|
65
|
+
## Gotchas
|
|
66
|
+
|
|
67
|
+
- **A port with no provenance comment cannot be audited.** If the 2.5 file doesn't name its
|
|
68
|
+
upstream source path, step 2 is impossible and the next fix upstream will be missed again.
|
|
69
|
+
- **`git log` on the *file*, not the repo.** The upstream fixes here were small commits buried in
|
|
70
|
+
unrelated history; only the path-scoped log surfaced them.
|
|
71
|
+
- **Mirroring a key into the JSON is not enough** — an unmirrored key on the TS type is dropped
|
|
72
|
+
silently, with no type error and no runtime error. The request just goes out under-specified.
|
|
73
|
+
|
|
74
|
+
## Change history
|
|
75
|
+
- 2026-08-28 — Created after the Prudential Sales Orders export shipped as a stale port of
|
|
76
|
+
`toga2-supply`, missing two later upstream fixes. Records the JSON-sidecar vs. inline-literal
|
|
77
|
+
structural difference, the `git log -- <upstream file>` check, and the eval/normalise/set-diff
|
|
78
|
+
verification technique. (bala)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 70 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
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 14 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 5 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -30,7 +30,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
30
30
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
31
31
|
- **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
32
32
|
- **toga2-commerce** (TOGa Commerce) — 20 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
33
|
-
- **toga25-supply** (TOGa 2.5 Supply) —
|
|
33
|
+
- **toga25-supply** (TOGa 2.5 Supply) — 16 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
34
34
|
- **toga-blox** (TOGa Blox) — 14 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
35
35
|
- **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
|
|
36
36
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| [French (fr-CA) Item Feature Translations (Compass Canada)](features/french-item-feature-translations.md) | 2.0 | Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group headers (e.g. | _underscore/Model/Compass/Canada/Feature.php, _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, worker2/Worker/Etilize/ItemTranslations.php, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql, dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql, dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql, dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql |
|
|
6
6
|
| [French (fr-CA) Order Email Localization (Compass Canada)](features/french-order-email-localization.md) | 2.0 | Compass Canada order emails (order requested, manager-approval request, approved/rejected, in-transit, delivered, reminders) are sent in **each recipient's own | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/String.php, library/app/client/compasscanada.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_cancel_pending_approval_orders.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_email_previews_prod.php |
|
|
7
7
|
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | api2/Component/Api/Cxml/Cxml.php, worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
|
|
8
|
+
| [Compass Canada — Item-Fulfillment TableViews (dead-Core-id 500, rebuilt onto the USA shape)](features/item-fulfillment-tableviews.md) | 2.0 | `Client_CompassCanada`'s two item-fulfillment table views — **`item-fulfillments-for-sales-orders`** (`TableViews.id 12`) and **`item-fulfillments-for-sales-ord | dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql, dbchanges2/Client_CompassCanada/2026-08-26 - EnableTableViewFieldFilters.sql, _underscore/Model/Client/TableView.php |
|
|
8
9
|
| [Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)](features/order-fulfillment-status-per-line.md) | 2.0 | TOGa Supply showed **Fulfilled** on Compass Canada orders that were only partly shipped. | _underscore/Model/Compass/Canada/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php |
|
|
9
10
|
| [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
|
|
10
11
|
| [Grand & Toy ASN Backfill (cXML replay + CSV-to-JSON)](workflows/grand-and-toy-asn-backfill.md) | 2.0 | How to recover Compass Canada shipments whose ASN never landed — used on 2026-08-20 to backfill the 67-day Grand & Toy outage (**518 shipments**: 171 via cXML r | worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, api2/Component/Api/Cxml/Cxml.php, _underscore/Model/Compass/AdvanceShippingNotice.php |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Compass Canada — Item-Fulfillment TableViews (dead-Core-id 500, rebuilt onto the USA shape)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: compass-canada
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-28
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
13
|
+
- dbchanges2/Client_CompassCanada/2026-08-26 - EnableTableViewFieldFilters.sql
|
|
14
|
+
- _underscore/Model/Client/TableView.php
|
|
15
|
+
related:
|
|
16
|
+
- ../../compass-usa/features/item-fulfillment-tracking-tableview.md
|
|
17
|
+
- ../../elite/features/supply2-tableview-config-drift.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/tableview-joins.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
20
|
+
- ../../../2.0/apps/api2/features/tableview-field-metadata.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`Client_CompassCanada`'s two item-fulfillment table views —
|
|
26
|
+
**`item-fulfillments-for-sales-orders`** (`TableViews.id 12`) and
|
|
27
|
+
**`item-fulfillments-for-sales-order-items`** (`TableViews.id 13`) — returned **HTTP 500 on
|
|
28
|
+
`/v2/table-views/meta`** while the identically-named Compass USA views served fine. Canada was one of
|
|
29
|
+
the ten clients that **never got the fulfillment-model rework**: its `TableViewJoins` still pointed at
|
|
30
|
+
`Core.Records.id 41` and `Core.RecordFields.id 211` / `321`, all deleted when the platform moved
|
|
31
|
+
tracking onto the bridge records. Rebuilt onto the Compass USA shape on **2026-08-27**
|
|
32
|
+
(`dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql`), applied to
|
|
33
|
+
production by the developer, **verified 200 via Postman 2026-08-28**.
|
|
34
|
+
|
|
35
|
+
This closes Canada's entry on the ten-client dead-id list in
|
|
36
|
+
[tracking-number-bridges](../../../2.0/apps/_underscore/features/tracking-number-bridges.md).
|
|
37
|
+
**Two Canada views are still broken** — see [Remaining work](#remaining-work).
|
|
38
|
+
|
|
39
|
+
## How it works (the rebuilt definitions)
|
|
40
|
+
|
|
41
|
+
Both views now match Compass USA structurally:
|
|
42
|
+
|
|
43
|
+
- **Base record moves from `Units` (31) to `ItemFulfillmentItems` (29)** — the re-root that makes a
|
|
44
|
+
fulfilled line appear even when it has no serialized unit rows.
|
|
45
|
+
- **Tracking resolves through the item-level bridge:** `ItemFulfillmentItems_TrackingNumbers`
|
|
46
|
+
(record **318**) then `TrackingNumbers` (62) then `ShippingCarriers` (9). 318 is the correct bridge
|
|
47
|
+
for Canada on its own measured row counts (**169** rows in 318 vs **138** in 319) — not because USA
|
|
48
|
+
uses it. Never inherit the bridge choice; count both.
|
|
49
|
+
- **Serial # / Asset Tag resolve through** `ItemFulfillmentItemUnits` (30) then `Units` (31), OUTER so
|
|
50
|
+
a non-serialized line is not dropped.
|
|
51
|
+
- **`quantity-fulfilled` was missing from view 13** and was added.
|
|
52
|
+
- Column order was aligned to the USA views.
|
|
53
|
+
|
|
54
|
+
### Canada-only presentation is deliberately preserved
|
|
55
|
+
|
|
56
|
+
The rebuild is a structural port, **not** a wholesale copy of USA's rows. Three Canada-only things
|
|
57
|
+
survive the rebuild and must survive any future re-port:
|
|
58
|
+
|
|
59
|
+
- `Items._imageUrl` thumbnail on the **part-number** column.
|
|
60
|
+
- `ShippingCarriers.urlLogo` on the **shipping-carrier** column.
|
|
61
|
+
- The `isSortable` / `isCopyable` flags set by
|
|
62
|
+
`dbchanges2/Client_CompassCanada/2026-08-26 - EnableTableViewFieldFilters.sql`.
|
|
63
|
+
|
|
64
|
+
## Why it 500'd (and why USA did not)
|
|
65
|
+
|
|
66
|
+
`_Model_Client_TableView::meta()` constructs `_Model_Core_Record` / `_Model_Core_RecordField` for
|
|
67
|
+
**every join the caller's roles can see**, before any column is considered. A join pointing at a
|
|
68
|
+
deleted Core row makes that constructor throw
|
|
69
|
+
*"Exactly 1 row was expected to be returned but 0 were"* (`_underscore/Model.php:716`), which unwinds
|
|
70
|
+
past the response assembly — so api2 returns a **500 with an EMPTY body** and writes **nothing** to
|
|
71
|
+
`Logs_CompassCanada.Api`. The mechanism, and the second (ACL-driven) failure mode that looks similar
|
|
72
|
+
but is not, are documented on
|
|
73
|
+
[tableview-joins](../../../2.0/apps/_underscore/features/tableview-joins.md).
|
|
74
|
+
|
|
75
|
+
Compass USA was migrated onto record 29 long ago, so it carries no dead join at all.
|
|
76
|
+
|
|
77
|
+
## ⚠ Canada's role ids are NOT Compass USA's — never copy ACL rows between them
|
|
78
|
+
|
|
79
|
+
This is the trap that makes "just copy the USA `AclRecordPermissions` rows" wrong:
|
|
80
|
+
|
|
81
|
+
| `Roles.id` | Compass Canada | Compass USA |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| 1 | Base | Base |
|
|
84
|
+
| 7 | **Contractor** | **Compass Base** |
|
|
85
|
+
| 8 | **Compass Base** | **Manager** |
|
|
86
|
+
| 9 | Manager | — |
|
|
87
|
+
|
|
88
|
+
USA grants the fulfillment records to roles **3 + 7** (Compass Base). Canada grants most of them to
|
|
89
|
+
role **1** (Base) only. In practice nearly every logged-in Canada user carries role 1, so the rebuilt
|
|
90
|
+
views work — but the **~10 users who hold Compass Base (Canada role 8) without Base still 500**.
|
|
91
|
+
Whether to grant Canada role 8 the same record permissions USA gives role 7 is an **open decision**,
|
|
92
|
+
not a mechanical port.
|
|
93
|
+
|
|
94
|
+
## Writing this migration — what cost real time
|
|
95
|
+
|
|
96
|
+
Reusable beyond this client; the general index/FK notes live on
|
|
97
|
+
[tableview-field-metadata](../../../2.0/apps/api2/features/tableview-field-metadata.md).
|
|
98
|
+
|
|
99
|
+
- **Resolve `TableViews` by `slug`, never by `id`.** `TableViews` / `TableViewJoins` /
|
|
100
|
+
`TableViewFields` ids differ per client database. Canada's views happen to be 12 and 13 like USA's,
|
|
101
|
+
which is a coincidence — the joins and fields underneath them are different ids entirely.
|
|
102
|
+
- **Detach before delete.** `TableViewFields.tableViewJoinId` is a real FK onto `TableViewJoins.id`,
|
|
103
|
+
so columns must be `SET tableViewJoinId = NULL` before their joins can be deleted; and
|
|
104
|
+
`TableViews.sortPrimaryTableViewFieldId` is an FK onto `TableViewFields.id`.
|
|
105
|
+
- **Sequence the reorder so no two rows ever share an `index` mid-script** — `TableViewFields` has
|
|
106
|
+
`UNIQUE (tableViewId, index)` and `UNIQUE (tableViewId, slug)`. Getting this wrong produces
|
|
107
|
+
`Duplicate entry '13-2' for key 'tableviewfields.tableViewId_index'`.
|
|
108
|
+
- **`Core.Records` / `Core.RecordFields` ids are identical between prod and dev-sandbox** — verified
|
|
109
|
+
2026-08-27 for the whole fulfillment set (29, 30, 31, 62, 9, 318) — so one file serves both. They
|
|
110
|
+
must be **hardcoded**: `Core` and `Client_*` are separate clusters in production, so a join to
|
|
111
|
+
resolve them is unrunnable there.
|
|
112
|
+
|
|
113
|
+
## Remaining work
|
|
114
|
+
|
|
115
|
+
Not fixed; the developer has been told.
|
|
116
|
+
|
|
117
|
+
- **`sales-order-shipments` (`TableViews.id 6`)** and **`units` (`TableViews.id 11`)** in
|
|
118
|
+
`Client_CompassCanada` still carry the same dead references (join to Core record 41,
|
|
119
|
+
`parentRecordFieldId` 211 / 321), so both still 500.
|
|
120
|
+
- Canada's `units` view is **intentionally a different design from USA's**, so it needs its own
|
|
121
|
+
decision rather than a straight USA copy.
|
|
122
|
+
- The Canada role-8 (Compass Base) permission question above is unresolved.
|
|
123
|
+
|
|
124
|
+
## Change history
|
|
125
|
+
- 2026-08-28 — Rebuilt both Canada item-fulfillment views onto the Compass USA shape
|
|
126
|
+
(`2026-08-27a - ItemFulfillmentTableViewsRebuild.sql`): base record Units 31 to
|
|
127
|
+
ItemFulfillmentItems 29, tracking via the item-level bridge 318 to TrackingNumbers 62 to
|
|
128
|
+
ShippingCarriers 9, serial/asset tag via ItemFulfillmentItemUnits 30 to Units 31, added the missing
|
|
129
|
+
`quantity-fulfilled` column to view 13, column order aligned to USA, Canada-only `_imageUrl` /
|
|
130
|
+
`urlLogo` / sortable-copyable flags preserved. Fixes the `table-views/meta` 500 caused by dead
|
|
131
|
+
`Core.Records` 41 and `Core.RecordFields` 211 / 321. Applied to prod, verified 200 via Postman.
|
|
132
|
+
Also recorded that **Canada's role ids differ from USA's** (7 = Contractor here, Compass Base
|
|
133
|
+
there), so ACL rows cannot be copied between the two tenants, and that `sales-order-shipments` (6)
|
|
134
|
+
and `units` (11) remain broken. (bala)
|
|
@@ -16,7 +16,7 @@ project: _Underscore
|
|
|
16
16
|
client: compass-canada
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-08-
|
|
19
|
+
updated: 2026-08-28
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -30,6 +30,7 @@ related:
|
|
|
30
30
|
- ../compass-usa/features/isfulfillable-data-quality-and-type-rule.md
|
|
31
31
|
- features/french-order-email-localization.md
|
|
32
32
|
- features/order-fulfillment-status-per-line.md
|
|
33
|
+
- features/item-fulfillment-tableviews.md
|
|
33
34
|
- ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
|
|
34
35
|
- ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
35
36
|
- ../../2.0/apps/_underscore/features/surface-resolver.md
|
|
@@ -166,4 +167,18 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
166
167
|
- **Item titles in customer emails** come from the `ItemTranslations` sidecar (prod: 220 fr-CA rows,
|
|
167
168
|
100% coverage of every item ever ordered). Resolve the English/translated fallback **in PHP**, never
|
|
168
169
|
as a SQL `COALESCE` — the two columns' collations differ and differ *per client DB*.
|
|
170
|
+
- **Canada's TOGa Supply fulfillment table views were never migrated with the platform — two are
|
|
171
|
+
now fixed, two are still broken.** `item-fulfillments-for-sales-orders` (12) and
|
|
172
|
+
`item-fulfillments-for-sales-order-items` (13) 500'd on `/v2/table-views/meta` because their
|
|
173
|
+
`TableViewJoins` still referenced deleted `Core.Records` **41** and `Core.RecordFields` **211** /
|
|
174
|
+
**321**; rebuilt onto the Compass USA shape (base record 29, tracking via bridge 318) by
|
|
175
|
+
`dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql`, prod-applied
|
|
176
|
+
and verified. **`sales-order-shipments` (6) and `units` (11) still carry the same dead ids and
|
|
177
|
+
still 500**, and Canada's `units` view is intentionally a different design from USA's. See
|
|
178
|
+
[Item-Fulfillment TableViews](features/item-fulfillment-tableviews.md).
|
|
179
|
+
- **⚠ Canada's `Roles` ids are NOT Compass USA's — never copy ACL rows between the two tenants.**
|
|
180
|
+
Canada: 1 Base, 7 **Contractor**, 8 **Compass Base**, 9 Manager. USA: 1 Base, 7 **Compass Base**,
|
|
181
|
+
8 Manager. USA grants the fulfillment records to roles 3 + 7; Canada grants most of them to role 1
|
|
182
|
+
only. Nearly every logged-in Canada user carries role 1, so this is usually invisible — but ~10
|
|
183
|
+
users hold Compass Base without Base and still hit the gap.
|
|
169
184
|
- Related: [Compass USA](../compass-usa/profile.md).
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
| [Prudential: OneUptime order-pipeline backlog monitors (Monitor/Prudential/*)](features/oneuptime-order-pipeline-monitors.md) | 2.0 | Three worker2 OneUptime push monitors (TRUE-80587) that watch the Prudential/Dell order pipeline for **stalls**, which were previously invisible — a stuck pipel | worker2/Worker/Monitor/Prudential.php, worker2/Config/production.ini, dbchanges2/Core/2026-08-07a - Prudential AIG ServiceNow OneUptime monitor crons.sql, dbchanges2/Client_Prudential/2026-04-08 - Insert Customers.sql |
|
|
9
9
|
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer | _underscore/Model/Prudential/ServiceRequest.php, _underscore/Test/Prudential/ServiceRequestTest.php |
|
|
10
10
|
| [Prudential: Service Request rejection alert email](features/service-request-rejection-alert-email.md) | 2.0 | When a Prudential ServiceNow→TOGa service-request submission (`POST /v2/service-requests`) is **rejected by validation**, TOGa now sends a real-time internal al | worker2/Worker/Client/Prudential/reports/ReqRejectionEmail.php, _underscore/Model/Prudential/ServiceRequest.php |
|
|
11
|
+
| [Prudential: Sales Orders CSV export (TOGa 2.5 Supply)](features/supply-orders-csv-export.md) | 2.0 | The **Export** button on the TOGa 2.5 Supply *Sales Orders* page is **Prudential-only**: it downloads a 22-column CSV (`prudential-orders-<YYYY-MM-DD>.csv`) cov | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga25-supply/src/pages/SalesOrders/hooks/usePrudentialOrdersExport.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx |
|
|
11
12
|
| [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | Cron script that transmits "Order Shipped" updates to ServiceNow (RITM) and sends a shipped notification email to the end user. | worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php, worker/crons/toga2/prudential/transmit_rejected_cancelled_updates_prudential.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/notifications/reports/prudential_exception_report.php |
|
|
12
13
|
| [Prudential Financial](profile.md) | 2.0 | Prudential is a TOGA client whose device-fulfillment flow is driven by **Dell** via the Dell API (`Client_Prudential.Apis.id = 2`). | |
|
|
13
14
|
| [Prudential: Dell ASN failed POST backfill replay](workflows/dell-asn-backfill-replay.md) | 2.0 | When Dell ASN POSTs fail in bulk (e.g. | |
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Prudential: Sales Orders CSV export (TOGa 2.5 Supply)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga25-supply
|
|
5
|
+
project: TOGa 2.5 Supply
|
|
6
|
+
client: prudential
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-28
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json
|
|
13
|
+
- toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts
|
|
14
|
+
- toga25-supply/src/pages/SalesOrders/hooks/usePrudentialOrdersExport.ts
|
|
15
|
+
- toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
|
|
16
|
+
related:
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
|
|
19
|
+
- ../../../2.0/apps/toga25-supply/workflows/porting-a-page-from-toga2-supply.md
|
|
20
|
+
- ../../../2.0/apps/toga25-supply/features/client-api-fetch-fields.md
|
|
21
|
+
- ../../../1.0/apps/worker/features/send-shipments-for-the-day.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
The **Export** button on the TOGa 2.5 Supply *Sales Orders* page is **Prudential-only**: it
|
|
27
|
+
downloads a 22-column CSV (`prudential-orders-<YYYY-MM-DD>.csv`) covering every Prudential unit —
|
|
28
|
+
both the units shipped on a Dell ASN and the units that have **never** shipped. It is a port of
|
|
29
|
+
`toga2-supply`'s `getPrudentialOrders` + `useExportableData`, and the column set and file name are
|
|
30
|
+
deliberately identical so Prudential's downstream consumers get the same file from either app.
|
|
31
|
+
|
|
32
|
+
This doc records the **two queries** it runs, the two join mistakes that made large parts of the
|
|
33
|
+
CSV blank, and the fan-out/dedupe rule that comes with the fix.
|
|
34
|
+
|
|
35
|
+
## Key files
|
|
36
|
+
|
|
37
|
+
| File | Role |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `viewModel/FIELDS/PRUDENTIAL/exportApiFields.json` | The two query projections (`fields` / `join` / `ojoin` / `where` / `recordsPerPage`) |
|
|
40
|
+
| `api/prudentialExportApi.ts` | Paginates each query, reshapes orphan units, dedupes, merges |
|
|
41
|
+
| `hooks/usePrudentialOrdersExport.ts` | Formats the 22 columns, CSV-escapes, triggers the download |
|
|
42
|
+
| `SalesOrders.tsx` | Renders the button (unmodified by this work) |
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
**Two queries, because one cannot see the other's rows.**
|
|
47
|
+
|
|
48
|
+
1. **`advanceShippingNotices`** (`/advance-shipping-notices`) — rooted at ASNs; every unit that
|
|
49
|
+
has shipped, with tracking number + status and `dateShipped`.
|
|
50
|
+
2. **`orphanUnits`** (`/units`, `where AdvanceShippingNoticeItemUnits.id = null`) — units that
|
|
51
|
+
were never shipped on an ASN. These are **structurally absent** from an ASN-rooted result (no
|
|
52
|
+
`AdvanceShippingNoticeItemUnits` row to join through), not merely null, which is why a second
|
|
53
|
+
query is required rather than an outer join on the first.
|
|
54
|
+
|
|
55
|
+
Each query is paged at `recordsPerPage: 1000` by `fetchAllPages()`, which loops
|
|
56
|
+
`while (allRows.length < totalRecordCount)` and takes the first array value out of the response
|
|
57
|
+
envelope rather than hardcoding the route key.
|
|
58
|
+
|
|
59
|
+
Orphan-unit rows are **reshaped** before merging: `/units` returns the unit's own fields at the
|
|
60
|
+
row top level (`name`, `serialNumber`) while `/advance-shipping-notices` nests them under `Units`,
|
|
61
|
+
so the reshape re-nests them and stubs `dateShipped` / `TrackingNumbers` to null — the formatter
|
|
62
|
+
then sees one uniform row shape.
|
|
63
|
+
|
|
64
|
+
The merged rows are formatted into 22 columns and written as UTF-8 CSV **with a BOM** (so Excel
|
|
65
|
+
does not mangle it); cells containing `,` `"` or a newline are quoted with doubled quotes.
|
|
66
|
+
|
|
67
|
+
## Data model (join chains)
|
|
68
|
+
|
|
69
|
+
**Shipped units** — the same chain the 1.0 cron
|
|
70
|
+
[`send_shipments_for_the_day.php`](../../../1.0/apps/worker/features/send-shipments-for-the-day.md)
|
|
71
|
+
walks:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
AdvanceShippingNotices
|
|
75
|
+
-> AdvanceShippingNoticeItems (advanceShippingNoticeId)
|
|
76
|
+
-> PurchaseOrders (AdvanceShippingNotices.purchaseOrderId)
|
|
77
|
+
-> SalesOrders_PurchaseOrders -> SalesOrders
|
|
78
|
+
-> ServiceRequests (SalesOrders.serviceRequestId) -> ServiceRequestTypes
|
|
79
|
+
-> AdvanceShippingNoticeItemUnits -> ..._TrackingNumbers -> TrackingNumbers
|
|
80
|
+
-> Units -> Items
|
|
81
|
+
-> Contacts (SalesOrders.contactId)
|
|
82
|
+
-> Addresses (SalesOrders.shipToAddressId) -> States
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Never-shipped units** — rooted at `Units`, INNER joins out to the order:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Units
|
|
89
|
+
-> ServiceRequests_Units (unitId) [INNER]
|
|
90
|
+
-> ServiceRequests [INNER]
|
|
91
|
+
-> SalesOrders (SalesOrders.serviceRequestId = ServiceRequests.id) [INNER]
|
|
92
|
+
-> SalesOrders_PurchaseOrders -> PurchaseOrders [INNER]
|
|
93
|
+
... plus OUTER joins to Items / Contacts / Addresses / States / PurchaseOrderStages
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
INNER is deliberate: a unit with no resolvable ServiceRequest chain has no order data at all and
|
|
97
|
+
does not belong in a **purchase-order-centric** export.
|
|
98
|
+
|
|
99
|
+
## Decisions
|
|
100
|
+
|
|
101
|
+
- **Contacts join `SalesOrders.contactId`, never `Units.contactId`.** `Units.contactId` stays NULL
|
|
102
|
+
until Dell assigns the asset. Measured on prod `Client_Prudential` (2026-08-27, read-only):
|
|
103
|
+
**12,327 of 29,685** ASN unit rows (~42%) have `Units.contactId` NULL, while
|
|
104
|
+
`SalesOrders.contactId` is NULL on **zero** rows.
|
|
105
|
+
- **Order data for never-shipped units resolves via `ServiceRequests_Units`, not
|
|
106
|
+
`SalesOrderItems_CommittedUnits`.** That table has **0 rows** in `Client_Prudential`, so the old
|
|
107
|
+
path blanked PO number, SO number, REQ, RITM and contact on every never-shipped row.
|
|
108
|
+
`ServiceRequests_Units` has 30,268 rows; of 39,121 never-shipped units, **9,846** resolve to a
|
|
109
|
+
full PO chain through it.
|
|
110
|
+
- **Contact name renders blank, not `"undefined undefined"`.** `formatContactName()` filters falsy
|
|
111
|
+
parts before joining. This is a **deliberate divergence** from `toga2-supply`, which interpolates
|
|
112
|
+
`${firstName} ${lastName}` and emits the literal text `undefined undefined` into the CSV when a
|
|
113
|
+
sales order has no contact.
|
|
114
|
+
- **Ship-to address comes from the SalesOrder, not the ASN.** Dell drops `line2` (apartment/floor)
|
|
115
|
+
from the `shipToAddress` it posts back on the ASN; the SalesOrder address keeps it as originally
|
|
116
|
+
captured. Revert to `AdvanceShippingNotices.shipToAddressId` only once Dell sends `line2`.
|
|
117
|
+
|
|
118
|
+
## Gotchas
|
|
119
|
+
|
|
120
|
+
- **`ServiceRequests_Units` is many-to-many and fans units out.** Generic/template units (docks,
|
|
121
|
+
monitors with no serial number) link to very many ServiceRequests, so the join produces one row
|
|
122
|
+
per distinct PurchaseOrder the unit resolves to — **9,846 distinct units become 21,970 rows** in
|
|
123
|
+
prod. `dedupeUnitsByUuid()` collapses them; which PurchaseOrder "wins" is **arbitrary** (first row
|
|
124
|
+
returned). This is why `uuid` had to be added to the `orphanUnits` field list.
|
|
125
|
+
- **Dedupe must run AFTER the pagination loop, never inside it.** `fetchAllPages()` terminates on
|
|
126
|
+
`while (allRows.length < totalRecordCount)`. Removing rows mid-loop means the accumulated count
|
|
127
|
+
never reaches `totalRecordCount` — the pager spins forever / over-fetches.
|
|
128
|
+
- **The button is gated on `clientSlug === "PRUDENTIAL"`** (from `useClientFields()`).
|
|
129
|
+
`SalesOrders.tsx` calls `usePrudentialOrdersExport()` **unconditionally for every client**, but
|
|
130
|
+
the hook has no `useEffect`/`useQuery` — the only fetch lives inside the `exportToCsv` callback —
|
|
131
|
+
so non-Prudential tenants fire **zero** requests. Keep it that way: adding a query hook here would
|
|
132
|
+
make every tenant pay for a Prudential-only export.
|
|
133
|
+
- **Blast radius is a closed chain:** `exportApiFields.json` → `prudentialExportApi.ts` →
|
|
134
|
+
`usePrudentialOrdersExport.ts` → `SalesOrders.tsx`. No other client and no other page is affected.
|
|
135
|
+
- **This export was a stale port.** The 2.5 version was taken from a `toga2-supply` revision that
|
|
136
|
+
predated both fixes above (upstream `bfaae3a9c`, `d4c0a612f`). See
|
|
137
|
+
[porting a page from toga2-supply](../../../2.0/apps/toga25-supply/workflows/porting-a-page-from-toga2-supply.md)
|
|
138
|
+
before porting anything else.
|
|
139
|
+
|
|
140
|
+
## Change history
|
|
141
|
+
- 2026-08-28 — Fixed two blank-column bugs carried in from a stale port: Contacts now join
|
|
142
|
+
`SalesOrders.contactId` (was `Units.contactId`, NULL on ~42% of ASN unit rows), and never-shipped
|
|
143
|
+
units resolve order data through `ServiceRequests_Units → ServiceRequests → SalesOrders →
|
|
144
|
+
SalesOrders_PurchaseOrders → PurchaseOrders` (was `SalesOrderItems_CommittedUnits`, 0 rows in
|
|
145
|
+
`Client_Prudential`). Added `dedupeUnitsByUuid()` for the many-to-many fan-out (9,846 units →
|
|
146
|
+
21,970 rows) and `formatContactName()` so a contact-less order exports an empty cell instead of
|
|
147
|
+
`"undefined undefined"`. (bala)
|
|
@@ -10,11 +10,12 @@ apps:
|
|
|
10
10
|
- worker2
|
|
11
11
|
- library
|
|
12
12
|
- toga2-commerce
|
|
13
|
+
- toga25-supply
|
|
13
14
|
project: _Underscore
|
|
14
15
|
client: prudential
|
|
15
16
|
type: profile
|
|
16
17
|
status: active
|
|
17
|
-
updated: 2026-08-
|
|
18
|
+
updated: 2026-08-28
|
|
18
19
|
owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
|
|
19
20
|
files: []
|
|
20
21
|
related:
|
|
@@ -25,6 +26,7 @@ related:
|
|
|
25
26
|
- features/dell-lch-iop-transmissions.md
|
|
26
27
|
- features/device-information-import-and-contact-linking.md
|
|
27
28
|
- workflows/http2-alb-workaround.md
|
|
29
|
+
- features/supply-orders-csv-export.md
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
## Summary
|
|
@@ -96,3 +98,6 @@ order-status transmissions.
|
|
|
96
98
|
- features/transmit-ordershipped-email.md
|
|
97
99
|
- features/device-information-import-and-contact-linking.md — device-sync cron, unit→contact
|
|
98
100
|
linking, the "null null" contact-export incident + dedup/backfill remediation.
|
|
101
|
+
- features/supply-orders-csv-export.md — the Prudential-only Sales Orders CSV export in TOGa 2.5
|
|
102
|
+
Supply (`toga25-supply`): its two queries, why contacts join `SalesOrders.contactId` (not
|
|
103
|
+
`Units.contactId`, NULL until Dell assigns the asset), and the `ServiceRequests_Units` fan-out.
|
package/package.json
CHANGED