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.
@@ -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-27
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 |