toga-ai 1.0.682 → 1.0.684
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 -1
- package/knowledge/2.0/apps/_underscore/features/sales-order-denial-reason.md +222 -0
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +37 -0
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +48 -1
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +10 -0
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +48 -1
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +167 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +4 -3
- package/knowledge/2.0/apps/toga25-supply/features/meta-driven-table-data.md +40 -1
- package/knowledge/2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md +22 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +51 -1
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +255 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/features/approval-decision-flow.md +13 -1
- package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +24 -2
- package/package.json +1 -1
|
@@ -37,13 +37,14 @@
|
|
|
37
37
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
38
38
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
39
39
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
40
|
+
| [Sales-order denial reason (`ApprovalDecisions.note`) — captured everywhere, displayed nowhere](features/sales-order-denial-reason.md) | The **denial reason** an approver types when rejecting a sales-order approval stage. | _underscore/Model/Client/ApprovalDecision.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Quad/ApprovalDecision.php, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts |
|
|
40
41
|
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
41
42
|
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php |
|
|
42
43
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
|
|
43
44
|
| [_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
45
|
| [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
46
|
| [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, _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
|
+
| [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 |
|
|
47
48
|
| [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
49
|
| [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
50
|
| [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 |
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sales-order denial reason (`ApprovalDecisions.note`) — captured everywhere, displayed nowhere
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-28
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/ApprovalDecision.php
|
|
13
|
+
- _underscore/Model/Compass/ApprovalDecision.php
|
|
14
|
+
- _underscore/Model/Quad/ApprovalDecision.php
|
|
15
|
+
- toga25-supply/src/layout/RecordApprovalModal/
|
|
16
|
+
- toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts
|
|
17
|
+
- toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts
|
|
18
|
+
related:
|
|
19
|
+
- ../../../clients/compass-usa/features/approval-decision-flow.md
|
|
20
|
+
- ../../../clients/compass-usa/features/mr-ma-order-approval-and-status.md
|
|
21
|
+
- acl-permission-chain.md
|
|
22
|
+
- surface-resolver.md
|
|
23
|
+
- ../../dbchanges2/features/surface-layer-schema.md
|
|
24
|
+
- ../../toga25-supply/features/surface-frontend.md
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## What it is
|
|
28
|
+
|
|
29
|
+
The **denial reason** an approver types when rejecting a sales-order approval stage. It is a single
|
|
30
|
+
free-text field — **`ApprovalDecisions.note`** — and this doc is the map of where it is written,
|
|
31
|
+
where it is read, and (as of 2026-08-28) where it is **not** read.
|
|
32
|
+
|
|
33
|
+
The headline: **every tenant already captures it, and no frontend anywhere displays it.** Its one
|
|
34
|
+
and only consumer today is the rejection email. So a "show the denial reason" ticket is a
|
|
35
|
+
**read/display** feature — there is no data-capture, no API and no schema work in it.
|
|
36
|
+
|
|
37
|
+
## Where the value lives
|
|
38
|
+
|
|
39
|
+
| Thing | Value |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Column | `Client_<Tenant>.ApprovalDecisions.note` |
|
|
42
|
+
| Model | `_Model_Client_ApprovalDecision`, `FIELD_CHAR` |
|
|
43
|
+
| Core `Records` id | **179** (`aclDatabase = CLIENT`) |
|
|
44
|
+
| Core `RecordFields` id for `note` | **1551** |
|
|
45
|
+
|
|
46
|
+
`ApprovalDecisions` holds **at most one row per stage per approval** (see
|
|
47
|
+
[approval-decision-flow](../../../clients/compass-usa/features/approval-decision-flow.md)), so a
|
|
48
|
+
denial reason is per-stage, not per-order — an order can in principle carry a note on stage 1 and
|
|
49
|
+
another on stage 2.
|
|
50
|
+
|
|
51
|
+
## The write side already exists — all three tenants
|
|
52
|
+
|
|
53
|
+
In `toga25-supply` the approval modal already captures the note for Compass USA, Compass Canada and
|
|
54
|
+
Quad:
|
|
55
|
+
|
|
56
|
+
- `RecordApprovalModal/view/ApprovalFlowDetailInputs.tsx` renders the reason input.
|
|
57
|
+
- It is **config-driven** by the per-client `approvalActionFields.json`
|
|
58
|
+
(`deniedLabel` / `deniedRequired` / `approvedLabel` / `approvedRequired`) — deny is required,
|
|
59
|
+
approve optional, per the Core decision-action surfaces (see
|
|
60
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)).
|
|
61
|
+
- `RecordApprovalModal/api/approvalDecisionsApi.ts` POSTs it.
|
|
62
|
+
|
|
63
|
+
Nothing on the write path needs changing.
|
|
64
|
+
|
|
65
|
+
## Who reads it today — the rejection email, and only the rejection email
|
|
66
|
+
|
|
67
|
+
Both consumers read `$payload->note` and fall back to
|
|
68
|
+
`SELECT note FROM ApprovalDecisions WHERE uuid = …` when the payload does not carry it.
|
|
69
|
+
|
|
70
|
+
- **Compass USA + Compass Canada** — `_Model_Compass_ApprovalDecision::sendRejectionEmailToUser()`
|
|
71
|
+
→ `buildRejectionNoteHtml()` (~L493, ~L904, ~L934). Canada machine-translates EN↔FR through
|
|
72
|
+
TogaIQ; **US passes the note through verbatim** because the client gate fails closed (see the
|
|
73
|
+
"Compass US is deliberately excluded from the rejection-note translation" gotcha in
|
|
74
|
+
[approval-decision-flow](../../../clients/compass-usa/features/approval-decision-flow.md)).
|
|
75
|
+
- **Quad** — `_Model_Quad_ApprovalDecision::handleApprovalDecision()` sends
|
|
76
|
+
`TEMPLATE_REQUEST_DENIED` with a `{note}` token. **Quad does not extend the Compass parent** —
|
|
77
|
+
it is a separate implementation, so a change to the Compass rejection-note path does not reach
|
|
78
|
+
Quad and vice versa.
|
|
79
|
+
|
|
80
|
+
**No other reader exists.** Neither `toga25-supply` nor `toga2-commerce` renders it.
|
|
81
|
+
|
|
82
|
+
### toga2-commerce already fetches it and throws it away
|
|
83
|
+
|
|
84
|
+
`toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts` (~L225) already lists
|
|
85
|
+
`"ApprovalDecisions.note"` in the `fields` of `getCurrentApprovalStage()`. It is dropped on the
|
|
86
|
+
floor downstream: the `ApprovalDecision` type in `globalTypes.ts` is only `{currentStage, stages}`,
|
|
87
|
+
and the OrderDetails view renders `salesOrderNotes` — a **different table**, not the denial reason.
|
|
88
|
+
|
|
89
|
+
So the commerce half of a display feature is **type + formatting + render + per-tenant copy**
|
|
90
|
+
(including French for Canada) with **no API work**. Do not add the field to the request; it is
|
|
91
|
+
already there.
|
|
92
|
+
|
|
93
|
+
## ACL — `RecordFields` 1551 (prod, 2026-08-28)
|
|
94
|
+
|
|
95
|
+
Point-in-time grant state on `AclFieldPermissions` for the `note` field:
|
|
96
|
+
|
|
97
|
+
| Tenant | Roles holding a grant |
|
|
98
|
+
|---|---|
|
|
99
|
+
| Compass USA | 3 (API), 4 (Admin), 7 (Compass Base), 8 (Manager) |
|
|
100
|
+
| Compass Canada | 1 (Base), 8 (Compass Base), 9 (Manager), 10 (Agilant-Administrators) |
|
|
101
|
+
| Quad | **1 (Base) and 3 (API) only** |
|
|
102
|
+
|
|
103
|
+
**Quad's Supply roles 7 (Global Admin) / 8 (IT-Shopper) / 9 (Buyer) have no direct grant** — but
|
|
104
|
+
this is **not a blocker today**, because every one of those users also holds role 1 and ACL
|
|
105
|
+
**unions** across a user's roles (see
|
|
106
|
+
[acl-permission-chain](acl-permission-chain.md)). No ACL migration is needed to ship a display
|
|
107
|
+
feature.
|
|
108
|
+
|
|
109
|
+
**Latent risk worth knowing:** a future Quad user granted role 7 *without* role 1 would fail the
|
|
110
|
+
**entire** request with `EZ-2`, not merely lose the field — one unreadable field named in `fields=`
|
|
111
|
+
403s the whole GET. That turns a missing grant on an obscure field into a blank page.
|
|
112
|
+
|
|
113
|
+
## How much denial data actually exists (prod, 2026-08-28)
|
|
114
|
+
|
|
115
|
+
| Tenant | Denied decisions | With a note |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| Compass USA | 2,579 (across 2,570 orders) | 2,357 (**91%**) |
|
|
118
|
+
| Compass Canada | 34 | 9 (26%) |
|
|
119
|
+
| Quad | 3 | 3 |
|
|
120
|
+
|
|
121
|
+
**Recorded recommendation:** ship **Compass USA first**. Canada and Quad get the surface rows for
|
|
122
|
+
consistency, but no per-tenant polish is justified by 34 and 3 rows. All figures are point-in-time
|
|
123
|
+
measurements, not standing facts.
|
|
124
|
+
|
|
125
|
+
## Planned display design (decided 2026-08-28 — nothing built yet)
|
|
126
|
+
|
|
127
|
+
Three decisions were taken during the investigation. They are recorded here so the build does not
|
|
128
|
+
re-litigate them.
|
|
129
|
+
|
|
130
|
+
### 1. The denial-details block gets its OWN surface
|
|
131
|
+
|
|
132
|
+
**Planned:** `Core.Surfaces` **47** `sales-order-denial-details`, `SurfaceElements` **170–174**, plus
|
|
133
|
+
a **175** marker — with **reserved literal ids**, not `AUTO_INCREMENT`, so client override files can
|
|
134
|
+
hardcode them and ship prod-ready. (Next free prod ids as of 2026-08-28: Surfaces **47**,
|
|
135
|
+
SurfaceElements **170**, Messages **227**, Actions **17** — re-verify before writing the seed; see
|
|
136
|
+
the verify-the-block-is-free rule in
|
|
137
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).)
|
|
138
|
+
|
|
139
|
+
Rejected: adding it to `sales-order-record-header` (surface 11). That surface is **top-bar chrome
|
|
140
|
+
only** — modal tag / order date / status badge / title, consumed by `SalesOrderTopBar.tsx`. Both
|
|
141
|
+
comparable read-only cards already have their own surface (`sales-order-decision-summary` 41,
|
|
142
|
+
`sales-order-approval-details` 38, each inheriting `appId`/`recordId` from the `order-details`
|
|
143
|
+
sibling), a conditional section needs its own layout-rule marker, and mixing it into an always-on
|
|
144
|
+
surface complicates every header override.
|
|
145
|
+
|
|
146
|
+
### 2. "View only" mode is a MARKER ELEMENT carrying a rule — never per-element `IS_ENABLED`
|
|
147
|
+
|
|
148
|
+
The approval-workflow modal's read-only mode is expressed as a **hidden marker element whose
|
|
149
|
+
resolved rule the FE reads as a MODE**. Precedent is already live in prod: element **108** on
|
|
150
|
+
surface 38, `config.role = 'sectionLayoutRule'`, `renderType TEXT`, `isVisible = 0` — it never
|
|
151
|
+
renders; the FE evaluates its resolved `visibilityRule` against the record to choose a layout. Both
|
|
152
|
+
Compass DBs override it client-wide with `VISIBILITY_RULE`. Same family as element 24
|
|
153
|
+
(`approvalsGate`) on surface 10.
|
|
154
|
+
|
|
155
|
+
**Why not `IS_ENABLED`:** that attribute is a **static boolean** per (element, role, persona,
|
|
156
|
+
language) with **no order-status axis**. `VISIBILITY_RULE` and `ENABLED_RULE` are the *only*
|
|
157
|
+
status-aware attributes in the `SurfaceOverrides` ENUM. Separately, the workflow **button must stay
|
|
158
|
+
clickable** so the modal can open at all — read-only cannot be expressed by disabling the button.
|
|
159
|
+
|
|
160
|
+
**Planned home for the marker:** surface **41** (`sales-order-decision-summary`) — the only surface
|
|
161
|
+
all three modal modes (approve / deny / approvalWorkflow) already fetch.
|
|
162
|
+
|
|
163
|
+
**Half the rendering already exists.** `ApprovalFlowDetailInputs.tsx` computes
|
|
164
|
+
`areInputsDisabled = !isPreApproved && stageHasApprovalDecision`, and `StageHasDecisionView.tsx`
|
|
165
|
+
already renders a decided stage's decision label plus the note in a disabled/readOnly textarea.
|
|
166
|
+
Remaining gaps: stages with **no** decision still render editable; `ApprovalStageCard`'s
|
|
167
|
+
approve/deny radios are not status-gated; the submit path is not gated.
|
|
168
|
+
|
|
169
|
+
### 3. Gate the card with a plain Tier-1 rule + a component emptiness guard
|
|
170
|
+
|
|
171
|
+
**Decision:** `{"all":[{"field":"order._status","op":"in","value":["canceled"]}]}` plus a
|
|
172
|
+
don't-render-an-empty-card guard in the component. **Not** a new Tier-2 named predicate.
|
|
173
|
+
|
|
174
|
+
This **reverses** the initial recommendation. Compass `_status` reaches `canceled` two ways — an
|
|
175
|
+
explicit `salesOrderStageId` (stage 8, Canceled) *or* any denied `ApprovalDecision` (see
|
|
176
|
+
[mr-ma-order-approval-and-status](../../../clients/compass-usa/features/mr-ma-order-approval-and-status.md))
|
|
177
|
+
— so a Tier-1 rule also fires on administratively-canceled orders with no denial behind them. A
|
|
178
|
+
`hasDeniedDecision` predicate was proposed to close that hole, then dropped once the hole was
|
|
179
|
+
**measured**: prod 2026-08-28, **1,592** Compass orders sit on stage 8, **1,569** of them also have
|
|
180
|
+
a denied decision, so only **23** — **0.9%** of ~2,593 — would render an empty card. That does not
|
|
181
|
+
justify growing `SURFACE_NAMED_RULES`, which the codebase itself calls an *"INTENTIONAL DEVIATION"*
|
|
182
|
+
from the frozen grammar (see
|
|
183
|
+
[surface-frontend](../../toga25-supply/features/surface-frontend.md)). Comment the guard with why it
|
|
184
|
+
exists, and revisit if admin/NetSuite cancellation volume rises.
|
|
185
|
+
|
|
186
|
+
The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`nin`/`gt`/`gte`/`lt`/
|
|
187
|
+
`lte` — `RULE_OPS` at `_underscore/Model/Core/Surface.php:233` and `FROZEN_OPS` in
|
|
188
|
+
`toga25-supply/src/surface/evaluateSurfaceRule.ts`.
|
|
189
|
+
|
|
190
|
+
## Gotchas
|
|
191
|
+
|
|
192
|
+
- **Quad is not a Compass tenant on this path.** `_Model_Quad_ApprovalDecision` implements its own
|
|
193
|
+
denial email (`TEMPLATE_REQUEST_DENIED` + `{note}`) and does **not** extend
|
|
194
|
+
`_Model_Compass_ApprovalDecision`. Any "fix the denial note for everyone" change is two edits.
|
|
195
|
+
- **Quad cannot open the approval-workflow modal at all today.** Quad has **no `SurfaceOverride` on
|
|
196
|
+
element 19 (`approvalWorkflow`) or 21 (`editOrder`)** — its roles 7/8/9 are opted into approve +
|
|
197
|
+
deny only. An `IS_VISIBLE` opt-in for element 19 is a **prerequisite** for any Quad work on the
|
|
198
|
+
workflow modal, and it is easy to miss because Compass/Compass Canada both have it.
|
|
199
|
+
- **`salesOrderNotes` is not the denial reason.** The commerce OrderDetails view renders
|
|
200
|
+
`salesOrderNotes` (request/shipping/delegate note types on a different table). Reading that code
|
|
201
|
+
and concluding "the note is already displayed" is the trap.
|
|
202
|
+
- **"Canceled" does not mean "denied."** ~1% of Compass canceled orders have no denial behind them.
|
|
203
|
+
Anything keyed on `_status in ["canceled"]` must tolerate a missing decision/note.
|
|
204
|
+
|
|
205
|
+
## Change history
|
|
206
|
+
- 2026-08-28 — Documented the denial reason end-to-end for the forthcoming display feature
|
|
207
|
+
(investigation only; **no code written, no migration run**; all prod figures read-only). Findings:
|
|
208
|
+
the value is `ApprovalDecisions.note` (Core record **179**, RecordField **1551**); the **write
|
|
209
|
+
side already exists for all three tenants** in `toga25-supply`; its **only** consumer is the
|
|
210
|
+
rejection email (Compass parent for USA/Canada, a **separate** Quad implementation); and
|
|
211
|
+
`toga2-commerce` **already requests the field** in `getCurrentApprovalStage()` but drops it (narrow
|
|
212
|
+
type + the view renders the unrelated `salesOrderNotes`). Recorded prod ACL for field 1551 (Quad's
|
|
213
|
+
Supply roles 7/8/9 pass only because they also hold role 1 and ACL unions — latent whole-request
|
|
214
|
+
`EZ-2` if that ever stops being true), prod denial volumes (Compass USA 2,579 denied / 91% with a
|
|
215
|
+
note; Canada 34/9; Quad 3/3 → ship Compass USA first), and three design decisions: an **own
|
|
216
|
+
surface** (planned 47 / elements 170–175, reserved literal ids) rather than an addition to the
|
|
217
|
+
header surface; **view-only expressed as a rule-carrying marker element** (precedent el 108
|
|
218
|
+
`sectionLayoutRule`) because `IS_ENABLED` has no status axis and the button must stay clickable;
|
|
219
|
+
and a **plain Tier-1 `canceled` rule + component emptiness guard** instead of a new Tier-2 named
|
|
220
|
+
predicate — the initial Tier-2 recommendation was reversed after measuring the gap at 23 orders
|
|
221
|
+
(0.9%). Also flagged that **Quad has no override on element 19**, so it cannot open the approval
|
|
222
|
+
workflow modal at all today. (apeterson)
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- toga25-supply/surface-layer-guide.md
|
|
60
60
|
related:
|
|
61
61
|
- ../../dbchanges2/features/surface-layer-schema.md
|
|
62
|
+
- sales-order-denial-reason.md
|
|
62
63
|
- ../../api2/features/surface-meta-option.md
|
|
63
64
|
- acl-permission-chain.md
|
|
64
65
|
- sales-order-status-filter-surface.md
|
|
@@ -246,6 +247,32 @@ cluster-isolation rule.
|
|
|
246
247
|
exact ids to a *different* element. See the NYCHH ship-together constraint in
|
|
247
248
|
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
248
249
|
|
|
250
|
+
### 🚨 …but the REPO still does not describe prod. Read production. (2026-08-28)
|
|
251
|
+
|
|
252
|
+
The reserved-literal house style fixes **new** files. It did **not** retroactively fix the ~51
|
|
253
|
+
existing `Client_Compass`/`Client_CompassCanada`/`Client_Quad` override files, which still carry
|
|
254
|
+
cross-cluster `FROM Core.SurfaceElements JOIN Core.Surfaces` reads and are **physically unrunnable
|
|
255
|
+
in prod** (verified empirically 2026-08-28: the Core reader exposes only
|
|
256
|
+
Core/Forecast/Forecast_Archive/Team, the client reader only `Client_*`). Prod's rows were **hand-written
|
|
257
|
+
by an operator** — id resolution on prod-core, then literal INSERTs against prod-client, per
|
|
258
|
+
`toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md` — and never written back to the repo.
|
|
259
|
+
|
|
260
|
+
**Standing rule (developer ruling, 2026-08-28): establish surface state by READING PRODUCTION, never
|
|
261
|
+
by reading `dbchanges2`. The stale files are NOT to be back-ported, reconciled, re-run or modified.**
|
|
262
|
+
Full detail, the prod element/override sweep, and the next-free prod ids live in
|
|
263
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
264
|
+
|
|
265
|
+
### Only `VISIBILITY_RULE` / `ENABLED_RULE` are status-aware — everything else is a static boolean
|
|
266
|
+
|
|
267
|
+
Worth stating outright because it decides how a per-status behavior gets modeled: `IS_VISIBLE` and
|
|
268
|
+
`IS_ENABLED` are static booleans per **(element, role, persona, language)** — there is **no
|
|
269
|
+
order-status axis** anywhere in the `SurfaceOverrides` attribute ENUM except the two rule attributes.
|
|
270
|
+
Any "behave differently when the order is in state X" requirement is therefore a **rule**, and if the
|
|
271
|
+
target is a whole section/mode rather than one control it belongs on a **hidden marker element whose
|
|
272
|
+
rule the FE reads as a MODE** (el 108 `sectionLayoutRule`; el 24 `approvalsGate`) — see the marker
|
|
273
|
+
flavors in [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md) and the applied
|
|
274
|
+
design in [sales-order-denial-reason](sales-order-denial-reason.md).
|
|
275
|
+
|
|
249
276
|
### Prior (2026-08-21) framing — kept for context, now historical
|
|
250
277
|
|
|
251
278
|
The per-client surface **override** files could not run in production under the dbchanges2
|
|
@@ -698,6 +725,16 @@ full stop. See
|
|
|
698
725
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
699
726
|
|
|
700
727
|
## Change history
|
|
728
|
+
- 2026-08-28 — Recorded that the reserved-literal house style fixed **new** authoring only: the ~51
|
|
729
|
+
existing `Client_Compass`/`Client_CompassCanada`/`Client_Quad` override files still carry
|
|
730
|
+
cross-cluster `Core.*` joins and are **unrunnable in prod** (verified empirically — the Core and
|
|
731
|
+
client readers expose disjoint schema sets), while prod's rows were **hand-written by an operator**
|
|
732
|
+
per `SURFACE-FEATURE-RUNBOOK.md` and never written back. **Developer ruling: read PRODUCTION for
|
|
733
|
+
surface state; the stale files are not to be back-ported, reconciled, re-run or modified.** Also
|
|
734
|
+
stated the attribute axis plainly — `IS_VISIBLE`/`IS_ENABLED` are static booleans per
|
|
735
|
+
(element, role, persona, language) with **no status axis**, so `VISIBILITY_RULE`/`ENABLED_RULE` are
|
|
736
|
+
the only status-aware attributes, and a status-aware *mode* belongs on a hidden rule-carrying marker
|
|
737
|
+
element (el 108 `sectionLayoutRule`, el 24 `approvalsGate`). (apeterson)
|
|
701
738
|
- 2026-08-28 — **Corrected the route-level gate's role-id space.** The dispatch check is keyed on
|
|
702
739
|
the role space that `Core.Records.aclDatabase` selects, not always `id.client.roles`: `surfaces`
|
|
703
740
|
is a **CORE** record, so `V2.php` (~L3090) takes `$roleIds` from **`id.core.roles`** and
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-28
|
|
10
|
-
owners: [bala]
|
|
10
|
+
owners: [bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
13
13
|
- _underscore/Model.php
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
17
17
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
18
18
|
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
19
|
+
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
19
20
|
related:
|
|
20
21
|
- ../../api2/features/tableview-field-metadata.md
|
|
21
22
|
- ./page-meta-context-field-settings.md
|
|
@@ -23,6 +24,8 @@ related:
|
|
|
23
24
|
- ../../api2/features/tableview-apiwhereclause-row-filtering.md
|
|
24
25
|
- ../../api2/features/request-logging.md
|
|
25
26
|
- ../../../../clients/compass-canada/features/item-fulfillment-tableviews.md
|
|
27
|
+
- ./acl-permission-chain.md
|
|
28
|
+
- ../../toga25-supply/features/transfer-orders-page.md
|
|
26
29
|
---
|
|
27
30
|
|
|
28
31
|
## Summary
|
|
@@ -84,6 +87,27 @@ first Items join on the view and therefore aliased `Items`. So:
|
|
|
84
87
|
- Before this change there were **zero** rows with a non-null `joinOnTableViewJoinId` anywhere in
|
|
85
88
|
`Client_Elite`, so chained joins were previously unused in these schemas.
|
|
86
89
|
|
|
90
|
+
### Joining the same table twice is SAFE — until something chains off it
|
|
91
|
+
|
|
92
|
+
The hazard is often stated as "never join a table twice." That is stricter than the mechanism. The
|
|
93
|
+
alias counter starts at `65` (`A`) **already seeded with the base table**, so the **first** join of a
|
|
94
|
+
table gets the **bare table name** and the **second** becomes `<Table>_C` (not `_B` — see the
|
|
95
|
+
gotcha). Since `onB` always renders the raw table name, the precise rule is:
|
|
96
|
+
|
|
97
|
+
> **Duplicating a table is harmless while no join's `onB` names that table.** It breaks only when the
|
|
98
|
+
> duplicated table is the **parent** of a chained join, because the chain silently binds to whichever
|
|
99
|
+
> copy took the bare name.
|
|
100
|
+
|
|
101
|
+
Worked example — the `transfer-orders` view (2026-08-28) joins **`Locations` twice**, for the origin
|
|
102
|
+
and destination location, plus `TransferOrderStages`. That is safe because **all three ON clauses
|
|
103
|
+
reference the base table `TransferOrders`**, so nothing hangs off the `Locations` alias at all.
|
|
104
|
+
|
|
105
|
+
The corollary shapes design decisions well beyond the view: because a second `Locations` hop *would*
|
|
106
|
+
be unsafe, the transfer-order record modal reads its **addresses out of the record fetch** (FK
|
|
107
|
+
expansion + `calcDepth`), not out of a third `TableViewJoins` row. When a column needs data two hops
|
|
108
|
+
past an already-duplicated table, the answer is usually the record fetch, not another join. See
|
|
109
|
+
[Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
110
|
+
|
|
87
111
|
## Look for an existing unused join before adding one
|
|
88
112
|
|
|
89
113
|
The `inventory_units` view already carried a Units → Items join
|
|
@@ -156,6 +180,19 @@ for fatals lives on [request-logging](../../api2/features/request-logging.md).
|
|
|
156
180
|
Contrast mode (b), which surfaces a readable message naming a `TableViewJoins.id` — which is exactly
|
|
157
181
|
why it reads like config corruption rather than an ACL gap.
|
|
158
182
|
|
|
183
|
+
**The drop happens first; whether you get an error depends on the field path.** The join disappearing
|
|
184
|
+
from the result set is the primary effect — a view whose column resolves through the missing join can
|
|
185
|
+
therefore simply **render without that column** rather than throwing. Do not treat "no error" as
|
|
186
|
+
evidence the grant exists. Either way the grant is mandatory: seed it with the view.
|
|
187
|
+
|
|
188
|
+
**A grant is the whole 4-table chain, not one row.** `AclRecordPermissions` →`AclLogicGroups` →
|
|
189
|
+
`AclLogicGroupExpressions` → `AclRecordExpressions` (see
|
|
190
|
+
[acl-permission-chain](./acl-permission-chain.md)); an `AclRecordPermissions` row with no logic group
|
|
191
|
+
is the failure mode already recorded for the NYCHH tracking bridges. Newer records can have **zero**
|
|
192
|
+
grants of any kind — `Records` **351** (`transfer-order-stages`) had none before
|
|
193
|
+
`Client_Nychh/2026-08-28a`, so the Status column of a brand-new view would have vanished silently.
|
|
194
|
+
Check the joined record's grants before assuming an existing record is grant-covered.
|
|
195
|
+
|
|
159
196
|
## Context RecordFieldSettings are OPTIONAL for a joined column to render
|
|
160
197
|
|
|
161
198
|
`meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the
|
|
@@ -229,6 +266,16 @@ Core-level reference ids used (identical across all clients and environments): *
|
|
|
229
266
|
writes its transaction log. Canada's fix:
|
|
230
267
|
[Compass Canada item-fulfillment TableViews](../../../../clients/compass-canada/features/item-fulfillment-tableviews.md).
|
|
231
268
|
(bala)
|
|
269
|
+
- 2026-08-28 — Sharpened the duplicate-join rule from "never join a table twice" to **"duplication is
|
|
270
|
+
harmless unless the duplicated table is a chained join's parent"** (the alias counter seeds `A` with
|
|
271
|
+
the base table, so the first join takes the bare name that `onB` always emits). Worked example: the
|
|
272
|
+
new `transfer-orders` view joins `Locations` twice safely because all three ON clauses reference the
|
|
273
|
+
base table — and the corollary that data two hops past a duplicated table belongs in the record
|
|
274
|
+
fetch (FK expansion + `calcDepth`), which is why the transfer-order modal's addresses are not a
|
|
275
|
+
third join. Also refined the ACL note: the missing grant **drops the join first**, so a column can
|
|
276
|
+
silently disappear with **no** error (no error ≠ grant exists), the grant means the full 4-table
|
|
277
|
+
chain, and a new record can have **zero** grants — `Records` 351 (`transfer-order-stages`) had none.
|
|
278
|
+
(apeterson)
|
|
232
279
|
- 2026-08-18 — Documented how `TableViewJoins` compile into SQL after adding a chained
|
|
233
280
|
**Units → Items → Manufacturers** join for the Part Number / Manufacturer / Item Description columns
|
|
234
281
|
on `inventory_units` (Elite + NYCHH): `onB` is emitted with the **raw table name** (so a chain only
|
|
@@ -58,6 +58,12 @@ it.
|
|
|
58
58
|
> `TINYINT UNSIGNED NULL … AFTER isFilterable` (matching its sibling flags); the real fan-out
|
|
59
59
|
> migration is still outstanding. Background:
|
|
60
60
|
> [framework branch ahead of schema](../../dbchanges2/workflows/framework-branch-ahead-of-schema.md).
|
|
61
|
+
>
|
|
62
|
+
> **Where the column exists, verified 2026-08-28:** present on **local** and **sandbox-client**
|
|
63
|
+
> (both hand-applied), **absent on production and beta**. Operational consequence for authors: a
|
|
64
|
+
> `TableViewFields` INSERT that **names `isGroupable`** runs fine locally and **fails on prod**. Omit
|
|
65
|
+
> the column from new TableView migrations until the fan-out migration lands — the
|
|
66
|
+
> `Client_Nychh/2026-08-28a` transfer-orders view does exactly that.
|
|
61
67
|
|
|
62
68
|
### A column's `type` / `precision` are NOT per-view — they come from `Core.RecordFields`
|
|
63
69
|
|
|
@@ -249,6 +255,10 @@ SalesOrderListingSortByDateOrderDesc.sql`).
|
|
|
249
255
|
*and* that query — and a grep for the property alone under-reports what a missing column breaks.
|
|
250
256
|
|
|
251
257
|
## Change history
|
|
258
|
+
- 2026-08-28 — Pinned **where `isGroupable` actually exists**: present on local and sandbox-client
|
|
259
|
+
(both hand-applied), **absent on production and beta**. So a `TableViewFields` INSERT naming the
|
|
260
|
+
column passes locally and fails on prod — omit it from new TableView migrations until the fan-out
|
|
261
|
+
migration lands (`Client_Nychh/2026-08-28a - TransferOrdersTableView.sql` does). (apeterson)
|
|
252
262
|
- 2026-08-28 — Recorded that **`DESCRIBE` / `information_schema.COLUMNS` hides a composite unique
|
|
253
263
|
key's second column** (`COLUMN_KEY = ''` for `TableViewFields.index`), so `index` looks free right
|
|
254
264
|
up until `Duplicate entry '13-2' for key 'tableviewfields.tableViewId_index'` — read
|
|
@@ -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: [tcox, bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -16,6 +16,8 @@ related:
|
|
|
16
16
|
- v2-api-error-codes.md
|
|
17
17
|
- tableview-apiwhereclause-row-filtering.md
|
|
18
18
|
- nested-fk-acl-embedding.md
|
|
19
|
+
- ../../_underscore/features/calculated-sql-fields.md
|
|
20
|
+
- ../../toga25-supply/features/transfer-orders-page.md
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Summary
|
|
@@ -90,6 +92,43 @@ response that walks that collection can 500. Two defences, both cheap:
|
|
|
90
92
|
The fragility itself is **unfixed** — any depth ≥ 2 response is exposed. Lowering depth removes
|
|
91
93
|
*your* exposure, not the defect.
|
|
92
94
|
|
|
95
|
+
### ⚠ `calcDepth` defaults to **1** — a calculated field one FK hop out comes back NULL, silently
|
|
96
|
+
|
|
97
|
+
`calcDepth` bounds how deep `FIELD_SQL` **calculated** fields are evaluated, independently of
|
|
98
|
+
`depth`. At the default of **1** only the requested record's own calculated fields resolve; a
|
|
99
|
+
calculated field on a record reached through an FK returns **`null` with no message** — no `EV-8`,
|
|
100
|
+
no warning, a 200 that merely looks like empty data.
|
|
101
|
+
|
|
102
|
+
Worked example: `_Model_Client_Location` exposes
|
|
103
|
+
`_addressLine1/_addressLine2/_addressCity/_addressState/_addressZip` as `FIELD_SQL`. Requested from
|
|
104
|
+
a record that only *points at* a Location (`GET /v2/transfer-orders/{uuid}` with the location
|
|
105
|
+
expanded), they need **`calcDepth: 2`**. Rule of thumb: **raise `calcDepth` by one for every FK hop
|
|
106
|
+
between the requested record and the calculated field's model**, and suspect it first whenever a
|
|
107
|
+
calculated field is null on a nested object but correct when that object is fetched directly.
|
|
108
|
+
Definition of the fields themselves:
|
|
109
|
+
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
110
|
+
|
|
111
|
+
### What nests by default is `InherentRecordChildren` — everything else needs the nested ROUTE
|
|
112
|
+
|
|
113
|
+
A record's one-to-many children arrive inside the record response **only** if `Core.
|
|
114
|
+
InherentRecordChildren` declares them. `sales-orders` (record 14) has **seven**; plenty of records
|
|
115
|
+
have **none** — `transfer-orders` (312) had **zero** as of 2026-08-28, so `transferOrderItems` never
|
|
116
|
+
appears in the record payload no matter what `fields`/`depth` ask for. The client must call the
|
|
117
|
+
**nested route** instead:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
GET /v2/transfer-orders/{uuid}/transfer-order-items
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **The JSON key of an inherent child is the camelCase of the child ROUTE**, not of the table or the
|
|
124
|
+
model (`V2.php` ~L6310, via `_String::CASE_KEBAB` → `CASE_CAMEL`). `transfer-order-items` →
|
|
125
|
+
`transferOrderItems`.
|
|
126
|
+
- **Adding an `InherentRecordChildren` row is a breaking-shape change, not a feature flag.** It
|
|
127
|
+
alters the default response for **every** existing caller of that route — including 1.0 importers
|
|
128
|
+
and NetSuite sync — so the blast radius has to be surveyed first. The transfer-order build
|
|
129
|
+
deliberately declined to add one and named the child route in the consuming surface's config
|
|
130
|
+
instead. See [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
131
|
+
|
|
93
132
|
### ⚠ `join` / `ojoin` drive the WHERE/filter — NOT the response shape
|
|
94
133
|
|
|
95
134
|
**`join` and `ojoin` change which rows come back, not what the JSON looks like.** The response
|
|
@@ -234,6 +273,14 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
|
|
|
234
273
|
invisible; re-request with `fields=` to force `EZ-2` and see the denied list.
|
|
235
274
|
|
|
236
275
|
## Change history
|
|
276
|
+
- 2026-08-28 — Documented two response-shape facts hit while building the Transfer Orders modal.
|
|
277
|
+
**`calcDepth` defaults to 1**, so a `FIELD_SQL` calculated field on a record one FK hop out returns
|
|
278
|
+
`null` with **no** message (Location's `_address*` fields needed `calcDepth: 2`) — raise it one per
|
|
279
|
+
FK hop. And **what nests by default is `InherentRecordChildren`**: `transfer-orders` (312) has
|
|
280
|
+
**zero** rows, so `transferOrderItems` never appears in the record payload at any `depth` and the
|
|
281
|
+
client must call the nested route; the inherent-child JSON key is the **camelCase of the child
|
|
282
|
+
ROUTE** (`V2.php` ~L6310). Adding an `InherentRecordChildren` row changes the default shape for
|
|
283
|
+
every existing caller of that route, so it is a breaking change, not a flag. (apeterson)
|
|
237
284
|
- 2026-08-27 — Pinned the **mechanism** behind the silently-ignored uuid-READ `where` to exact code:
|
|
238
285
|
a uuid'd route is classified `ACTION__READ` (`V2.php:3022-3026`) and that branch
|
|
239
286
|
(`V2.php:4399` → ~`5478`) has **no `where` handling at all**; `where` is applied only in
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
6
|
| [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Client_Compass/2026-08-28 - GrantViewOrdersRoleSalesOrdersNavigation.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
|
|
7
|
-
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.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, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.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, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql |
|
|
7
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.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, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.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, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
|
|
8
8
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
9
9
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
10
10
|
| [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
|