toga-ai 1.0.712 → 1.0.714

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.
Files changed (22) hide show
  1. package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
  2. package/knowledge/2.0/apps/_underscore/features/sales-order-denial-reason.md +190 -26
  3. package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +25 -2
  4. package/knowledge/2.0/apps/dbchanges2/INDEX.md +3 -2
  5. package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +2 -3
  6. package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +94 -3
  7. package/knowledge/2.0/apps/dbchanges2/workflows/targeting-the-right-database.md +99 -0
  8. package/knowledge/2.0/apps/toga-blox/workflows/dynamic-publish-pipeline.md +13 -1
  9. package/knowledge/2.0/apps/toga-blox/workflows/local-link-into-a-consumer-app.md +32 -1
  10. package/knowledge/2.0/apps/toga25-supply/INDEX.md +2 -2
  11. package/knowledge/2.0/apps/toga25-supply/features/action-button-rule-engine.md +52 -3
  12. package/knowledge/2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md +13 -0
  13. package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +126 -8
  14. package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +23 -4
  15. package/knowledge/2.0/apps/toga25-supply/workflows/amplify-deployment.md +85 -1
  16. package/knowledge/INDEX.md +1 -1
  17. package/knowledge/clients/compass-usa/INDEX.md +1 -1
  18. package/knowledge/clients/compass-usa/features/approval-decision-flow.md +51 -1
  19. package/knowledge/clients/compass-usa/workflows/granting-navigation-access.md +10 -3
  20. package/knowledge/clients/nychh/INDEX.md +1 -1
  21. package/knowledge/clients/nychh/profile.md +26 -6
  22. package/package.json +1 -1
@@ -37,7 +37,7 @@
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
+ | [Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode](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, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx, toga25-supply/src/utils/formatDateTime.ts, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts |
41
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 |
42
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 |
43
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 |
@@ -1,19 +1,30 @@
1
1
  ---
2
- title: Sales-order denial reason (`ApprovalDecisions.note`) — captured everywhere, displayed nowhere
2
+ title: Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: [apeterson]
9
+ updated: 2026-08-31
10
+ owners: [apeterson, tcox]
11
11
  files:
12
12
  - _underscore/Model/Client/ApprovalDecision.php
13
13
  - _underscore/Model/Compass/ApprovalDecision.php
14
14
  - _underscore/Model/Quad/ApprovalDecision.php
15
15
  - toga25-supply/src/layout/RecordApprovalModal/
16
16
  - toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts
17
+ - toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx
18
+ - toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx
19
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx
20
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx
21
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
22
+ - toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
23
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts
24
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
25
+ - toga25-supply/src/utils/formatDateTime.ts
26
+ - dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
27
+ - dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
17
28
  - toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts
18
29
  related:
19
30
  - ../../../clients/compass-usa/features/approval-decision-flow.md
@@ -28,11 +39,17 @@ related:
28
39
 
29
40
  The **denial reason** an approver types when rejecting a sales-order approval stage. It is a single
30
41
  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.
42
+ where it is read, and what displays it.
32
43
 
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.
44
+ **Every tenant already captures it**, so a "show the denial reason" ticket is a pure
45
+ **read/display** feature — no data-capture, no API and no schema work.
46
+
47
+ **Status (2026-08-31).** In **production the rejection email is still the only consumer.** The
48
+ display feature — a **denial banner** on the SO record modal plus a read-only **approval timeline**
49
+ in the workflow modal — is built, committed and **confirmed rendering on `client-sandbox`**, but not
50
+ released: `toga25-supply` branch **`TRUE-80850`** (`ca6678f`, pushed, **no PR**) and `dbchanges2`
51
+ branch **`TRUE-80850`** (`38ef9ff`, **not merged to `_main`**). Seeds have run on `client-sandbox`
52
+ only. See [Built](#built-2026-08-31--denial-banner--read-only-approval-timeline).
36
53
 
37
54
  ## Where the value lives
38
55
 
@@ -77,7 +94,9 @@ Both consumers read `$payload->note` and fall back to
77
94
  it is a separate implementation, so a change to the Compass rejection-note path does not reach
78
95
  Quad and vice versa.
79
96
 
80
- **No other reader exists.** Neither `toga25-supply` nor `toga2-commerce` renders it.
97
+ **No other reader exists in any deployed environment.** `toga2-commerce` still renders nothing; the
98
+ `toga25-supply` banner + timeline exist only on the unmerged `TRUE-80850` branches (+ the
99
+ `client-sandbox` seeds).
81
100
 
82
101
  ### toga2-commerce already fetches it and throws it away
83
102
 
@@ -122,19 +141,73 @@ feature.
122
141
  consistency, but no per-tenant polish is justified by 34 and 3 rows. All figures are point-in-time
123
142
  measurements, not standing facts.
124
143
 
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.
144
+ ## Built (2026-08-31) — denial banner + read-only approval timeline
145
+
146
+ The mockup arrived mid-build and **changed the design**: there is **no denial-details card**. The
147
+ denial reason is not surfaced on the record modal at all — it lives one click away, in the workflow
148
+ modal's read-only timeline.
149
+
150
+ **1. Denial banner** (`SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx`) — full
151
+ width at the **top of the modal body**, rendered by `SalesOrderView`, not by the summary grid. Two
152
+ lines plus a **"View approval workflow"** pill (a native `button`; blox `BaseButton` styling
153
+ conflicted):
154
+
155
+ > This order was denied at {stage label} · step {N} of {M}
156
+ > {decider} ({role}) denied the order on {MM/DD/YYYY, h:mm A}.
157
+
158
+ **The reason is deliberately NOT in the banner.** The pill opens the workflow modal, which is
159
+ read-only on a canceled order.
160
+
161
+ **2. Read-only approval timeline** (`src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx`)
162
+ — in read-only mode `RecordApprovalModalLayout` renders the timeline **instead of**
163
+ `ApprovalFlowForm`: per-stage colored disc (green check approved / red x denied / gray step number
164
+ pending) on a connector rail, a status line "Approved by {name} · {role} · {datetime}", and note
165
+ boxes — gray **"Note"** for approvals, pink **"Reason for denial"** for denials. **No footer**; the
166
+ header X closes. Dialog variant `.approvalDialogReadOnly` (780px, auto height).
167
+
168
+ The read-only **summary** above the timeline drops `preferredStage`-bound fields (Assigned Manager —
169
+ "who acts next" is meaningless on a decided order) and strips trailing label colons.
170
+
171
+ **3. Surface 47 is still the gate — it just drives a banner instead of a card.** The
172
+ `salesOrderDenialDetails` section is **excluded from `getVisibleSections`** outright (never a
173
+ summary-grid card); `isDenialDetailsActive(tenantFields, order)` decides the banner: section visible
174
+ **AND** its Tier-1 layout rule passes **AND** `hasDenialDetails(order)` (the ~1% emptiness guard,
175
+ decision 3 below).
176
+
177
+ **4. Deleted as dead code:** `DenialDetailsCard.tsx`, its `denialDetails` renderer in
178
+ `sectionRenderers.tsx`, and the disabled-form read-only plumbing (`isReadOnly` on
179
+ `ApprovalFlowForm` / `ApprovalStageCard` / `ApprovalFlowDetailInputs`). **The timeline is the only
180
+ read-only path** — do not re-add a "disable the form" branch.
181
+
182
+ **5. Adapter** (`surfaceBundleToTenantFields.ts`): `config.role` carried onto fields;
183
+ `config.valueType` used **only when the enum column is NULL**; exports `isDenialDetailsActive`,
184
+ `getDeniedStages`, `hasDenialDetails`, `resolvePathInsensitive`, `DENIAL_DETAILS_KEY`,
185
+ `ApprovalStageRecord`. `SalesOrder` type gained `stages` / `currentStage`.
186
+
187
+ **6. Seeds** (`dbchanges2` branch `TRUE-80850`, `38ef9ff`):
188
+ `Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql` (surface **47**, elements **170–176**,
189
+ Messages **227–232**, plus the `readOnlyRule` marker on surface 41) and
190
+ `Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql` (two-stage widening +
191
+ the Admin **`ENABLED_RULE`** as well as `VISIBILITY_RULE` on element 19). Reserved literal ids, plain
192
+ `INSERT`s, verify-the-block SQL in the header. This feature **kept** 47 / 170–176 / 227–232; the
193
+ transfer-order seeds that had reserved the same snapshot were **renumbered** to 53–57 / 190–206 /
194
+ 261–284 on 2026-08-31 — see
195
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
196
+
197
+ Verified on `client-sandbox` 2026-08-31: banner + read-only timeline render for Compass. Test orders
198
+ SA136508, SA136480, SA136473, and **SA134872 (denial with a NULL note — placeholders render)**.
199
+ Tests: 5 vitest files (`ApprovalTimelineView`, `DenialBanner`, adapter, `getVisibleSections`,
200
+ decision-fields), tsc + eslint clean.
201
+
202
+ ## Design decisions (2026-08-28 plan, confirmed and extended by the 2026-08-31 build)
129
203
 
130
204
  ### 1. The denial-details block gets its OWN surface
131
205
 
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
206
+ Confirmed as built: `Core.Surfaces` **47** `sales-order-denial-details`, `SurfaceElements`
207
+ **170–176**, `Messages` **227–232** — with **reserved literal ids**, not `AUTO_INCREMENT`, so client
208
+ override files hardcode them and ship prod-ready. Re-verify the block is free before every run; see
136
209
  the verify-the-block-is-free rule in
137
- [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).)
210
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
138
211
 
139
212
  Rejected: adding it to `sales-order-record-header` (surface 11). That surface is **top-bar chrome
140
213
  only** — modal tag / order date / status badge / title, consumed by `SalesOrderTopBar.tsx`. Both
@@ -157,19 +230,23 @@ language) with **no order-status axis**. `VISIBILITY_RULE` and `ENABLED_RULE` ar
157
230
  status-aware attributes in the `SurfaceOverrides` ENUM. Separately, the workflow **button must stay
158
231
  clickable** so the modal can open at all — read-only cannot be expressed by disabling the button.
159
232
 
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.
233
+ **Home for the marker (built):** element **176** on surface **41**
234
+ (`sales-order-decision-summary`) — the only surface all three modal modes (approve / deny /
235
+ approvalWorkflow) already fetch. Core default rule: `_status nin ["pendingApproval"]`.
236
+
237
+ **Role markers are the convention for this whole family** — `sectionLayoutRule`, `titleSuffix`,
238
+ `readOnlyRule`, and the new **`denialReason`** on the note element. A marker is a `config.role`
239
+ string the FE looks for; it is **never** a magic `valueType`.
162
240
 
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.
241
+ **Use the real `valueType` ENUM member whenever one exists** (`DATETIME` for `dtDecision`). The
242
+ `config.valueType` bypass is reserved for genuinely-missing members, and the FE adapter now falls
243
+ back to it **only when the enum column is NULL**.
168
244
 
169
- ### 3. Gate the card with a plain Tier-1 rule + a component emptiness guard
245
+ ### 3. Gate it with a plain Tier-1 rule + a component emptiness guard
170
246
 
171
247
  **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.
248
+ don't-render-anything-empty guard in the component (now `isDenialDetailsActive` → `hasDenialDetails`).
249
+ **Not** a new Tier-2 named predicate.
173
250
 
174
251
  This **reverses** the initial recommendation. Compass `_status` reaches `canceled` two ways — an
175
252
  explicit `salesOrderStageId` (stage 8, Canceled) *or* any denied `ApprovalDecision` (see
@@ -187,6 +264,71 @@ The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`n
187
264
  `lte` — `RULE_OPS` at `_underscore/Model/Core/Surface.php:233` and `FROZEN_OPS` in
188
265
  `toga25-supply/src/surface/evaluateSurfaceRule.ts`.
189
266
 
267
+ ### 4. Decision dates display ABSOLUTE (2026-08-31)
268
+
269
+ New `src/utils/formatDateTime.ts` (dayjs, `MM/DD/YYYY, h:mm A`). Chosen over the existing
270
+ `formatTimestamp`, which is **relative** ("3 hours ago") and useless on a months-old denial, and over
271
+ native `Date` parsing — MySQL's `YYYY-MM-DD HH:mm:ss` is `Invalid Date` in Safari.
272
+
273
+ ### 5. A read-only widening must target the SAME override scope tier as the existing rows (2026-08-31)
274
+
275
+ Compass needed the read-only rule widened to two stages
276
+ (`nin ["pendingApproval","pendingInitialApproval"]`). For **element 19** (`approvalWorkflow`) the
277
+ widening had to be written as an **Admin role-scoped** override, not client-wide: a client-wide
278
+ (`roleId NULL`) row is **shadowed** by the pre-existing Admin role-scoped rows under the resolver
279
+ cascade (base < client-wide < persona < role). Element 19 also carries an Admin **`ENABLED_RULE`**
280
+ limited to pending statuses, so widening only `VISIBILITY_RULE` leaves the button visible-but-disabled
281
+ and the modal still cannot open — **both attributes** need widening. Caught by sql-reviewer plus a
282
+ sandbox check of the live override rows. Authoring detail:
283
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
284
+
285
+ ### 6. Stage DISPLAY labels come from client config, not the DB (2026-08-31)
286
+
287
+ The banner and every timeline row resolve the stage label from the per-client
288
+ `approvalViewFields.recordActionFields.approvalWorkflow.stages[]` entry matching the **step**
289
+ ("Admin Review" / "Manager Review"), falling back to `ApprovalTemplateStages.name`. Compass's DB
290
+ names are **status-flavored** ("Pending Initial Approval" / "Pending Approval") and read wrong in
291
+ past tense. **Do not rename the DB stages** — those names feed `_status` and the approval emails;
292
+ display naming belongs in config.
293
+
294
+ ### 7. Step totals come from the MAX step NUMBER, not `stages.length` (2026-08-31)
295
+
296
+ The per-stage meta fan-out makes `order.stages[]` **transiently partial**, so array length rendered
297
+ "step 2 of **1**" mid-load. Total = `Math.max(...steps)`.
298
+
299
+ ### 8. The stages fetch is gated on the APPROVALS SURFACE toggle (2026-08-31)
300
+
301
+ `useApprovalModalViewModel` now gates its stages fetch on `sales-order-record-approvals`
302
+ (`isApprovalsSurfaceEnabled`) — the same authoritative per-client gate the record-modal and
303
+ row-state view models use — **not** on `approvalActionStatuses`. The status list excludes canceled,
304
+ so a canceled order fetched no stages and the timeline had nothing to draw; the surface toggle also
305
+ keeps approvals-disabled clients from fetching at all.
306
+
307
+ ### 9. Read-only FAILS CLOSED on a fetch error (2026-08-31)
308
+
309
+ If the decision-surface fetch **errors**, the rule is unknown → the modal opens **read-only**; a
310
+ **successful** response with no marker (unseeded client) stays **editable**. A transient surface
311
+ outage must never unlock edit + submit on a decided order.
312
+
313
+ ### 10. Copy is config-first with FE fallbacks (2026-08-31)
314
+
315
+ `modalFields.timelineLabel` / `noteLabel` / `denialReasonLabel` and
316
+ `approvalWorkflow.viewWorkflowButton.label`. **Composed sentences** (the two banner lines, the
317
+ "Approved by … · … · …" status line) remain **FE English** — Compass Canada localization is an open
318
+ follow-up, not an oversight.
319
+
320
+ ## Open items (2026-08-31)
321
+
322
+ - **Nothing is merged.** `toga25-supply` `TRUE-80850` (`ca6678f`) is pushed but **not PR'd**;
323
+ `dbchanges2` `TRUE-80850` (`38ef9ff`) is **not merged to `_main`**.
324
+ - **Re-verify the id block immediately before any prod run** — a reservation is a snapshot, not a
325
+ lock (see [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)).
326
+ - **Compass Canada**: override file (same two-stage, role-scoped widening) **and** localization of
327
+ the FE-composed banner/timeline sentences.
328
+ - **Quad** needs an `IS_VISIBLE` opt-in on element 19 first — it cannot open the workflow modal at
329
+ all today.
330
+ - jcardinal still owes the `2.0/standards/framework-rules.md` reserved-id-exemption review.
331
+
190
332
  ## Gotchas
191
333
 
192
334
  - **Quad is not a Compass tenant on this path.** `_Model_Quad_ApprovalDecision` implements its own
@@ -203,6 +345,28 @@ The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`n
203
345
  Anything keyed on `_status in ["canceled"]` must tolerate a missing decision/note.
204
346
 
205
347
  ## Change history
348
+ - 2026-08-31 — **Built the display feature planned on 2026-08-28** (resumed apeterson's
349
+ `denial-reason-surface-plan` session), then **redesigned it mid-build when the mockup arrived**.
350
+ Final shape: a **denial banner** at the top of the record-modal body (stage label · step N of M,
351
+ decider + role + absolute datetime, and a "View approval workflow" pill) with the **reason
352
+ deliberately not in the banner**, plus a read-only **approval timeline**
353
+ (`ApprovalTimelineView.tsx`) that **replaces** the form in the workflow modal — per-stage discs on a
354
+ rail, gray "Note" / pink "Reason for denial" boxes, no footer. The planned `DenialDetailsCard` and
355
+ the disabled-form read-only plumbing were **deleted as dead code**; surface **47** now gates the
356
+ banner (`isDenialDetailsActive`) and is excluded from `getVisibleSections` entirely. Mode still
357
+ comes from the `config.role:'readOnlyRule'` marker (el **176** on surface 41), with the
358
+ decision-surfaces `isLoading` OR'd into `isApprovalLoading` (no editable→read-only flicker) and
359
+ **fail-closed on fetch error**. Other decisions: stage **display labels** come from client config
360
+ (Compass DB names are status-flavored — do not rename them); step totals from the **max step
361
+ number**, not `stages.length` (partial fan-out rendered "step 2 of 1"); the stages fetch is gated on
362
+ the **approvals surface toggle**, not `approvalActionStatuses` (canceled orders were fetching
363
+ nothing); `denialReason` is a **role marker, not a magic valueType**; the enum `valueType` wins over
364
+ `config.valueType`; dates are **absolute** (`formatDateTime`, `MM/DD/YYYY, h:mm A`); and the
365
+ element-19 widening must be **Admin role-scoped** covering **`ENABLED_RULE` as well as
366
+ `VISIBILITY_RULE`**. Seeds renamed `2026-08-28a` → **`2026-08-31a`** and committed
367
+ (`dbchanges2@TRUE-80850` `38ef9ff`); FE committed + pushed (`toga25-supply@TRUE-80850` `ca6678f`,
368
+ no PR). Verified rendering on `client-sandbox` (SA136508 / SA136480 / SA136473 / SA134872
369
+ null-note). Open: nothing merged, Compass Canada override + localization, Quad el-19 opt-in. (tcox)
206
370
  - 2026-08-28 — Documented the denial reason end-to-end for the forthcoming display feature
207
371
  (investigation only; **no code written, no migration run**; all prod figures read-only). Findings:
208
372
  the value is `ApprovalDecisions.note` (Core record **179**, RecordField **1551**); the **write
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: ["bala", "mhammontree", "apeterson", "jcardinal"]
9
+ updated: 2026-08-31
10
+ owners: ["bala", "mhammontree", "apeterson", "jcardinal", "tcox"]
11
11
  files:
12
12
  - api2/.ebextensions/git.php
13
13
  - api2/.platform/hooks/prebuild/git.sh
@@ -185,6 +185,19 @@ the tier trades a stale-code bug for a total outage. Full procedure:
185
185
  method/class that isn't on the box and fatal. Landing `_underscore` first is **safe on its own**:
186
186
  the old api2 controller simply doesn't use the new framework code yet. Order: merge `_underscore`
187
187
  → (verify it's on the branch) → deploy api2.
188
+ - **🚨 Worked instance — `client-sandbox` was 100% DOWN on this exact violation (2026-08-31,
189
+ since fixed by matching the branches and redeploying).** Every request returned 500 `Call to
190
+ undefined method
191
+ _Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client`
192
+ (`Component/Api/V2/V2.php:6198`, commit `0cc37fe` "Delegate ACL field perms to shared resolver",
193
+ jcardinal 2026-08-14) delegates to a resolver whose `_underscore` half (`a5e79448` "Surface field
194
+ binding and ACL field resolver", same author/date) exists **only on `_qa-alpha` and
195
+ `toga25-desk`** — it was never merged to **`_underscore@_sandbox-client`**. Resolved the same day
196
+ by the team (branches matched + redeployed). **Why a half-shipped ACL refactor is a TOTAL outage,
197
+ not a partial one:** field-ACL
198
+ resolution runs on essentially every authenticated GET, so there is no degraded mode — the whole
199
+ tier 500s and every unrelated feature under test looks broken. When a whole sandbox goes dark,
200
+ check the two-repo pairing before debugging your own feature.
188
201
  - **⚠ A "Could not find required file for `_Model_...`" fatal on prod can be a STALE-BUILD
189
202
  artifact, not a missing file.** `Logs.Issue #476`
190
203
  ("Could not find required file for '_Model_Client_ItemFulfillments_InventoryAdjustment'") fired
@@ -257,6 +270,16 @@ the tier trades a stale-code bug for a total outage. Full procedure:
257
270
 
258
271
  ## Change history
259
272
 
273
+ - 2026-08-31 — Recorded a **worked instance** of the cross-repo ordering rule: **`client-sandbox`
274
+ went entirely down** — every request 500'd with `Call to undefined method
275
+ _Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client` (`V2.php:6198`,
276
+ `0cc37fe`, 2026-08-14) delegates ACL field permissions to an `_underscore` resolver (`a5e79448`)
277
+ that lived only on `_qa-alpha` / `toga25-desk` and was never merged to
278
+ `_underscore@_sandbox-client`; fixed the same day by matching the branches and redeploying.
279
+ Durable point: a
280
+ half-shipped **ACL** refactor has no degraded mode — field-ACL resolution runs on nearly every
281
+ authenticated GET, so the tier is totally dark and unrelated features look broken. Found while
282
+ testing an unrelated toga25-supply feature. (tcox)
260
283
  - 2026-08-28 — Recorded two build-time-clone consequences: (1) **cross-repo deploy ordering** — api2
261
284
  clones `_underscore` (`_<ENVIRONMENT>`) at build via `.platform/hooks/prebuild/git.sh`, so for a
262
285
  change spanning both repos, `_underscore` must land on the deploy branch **before** api2 is
@@ -3,11 +3,12 @@
3
3
  | Doc | Summary | Files |
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
- | [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](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 | 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-28b - TransferOrderListActionsSurfaceSeed.sql, 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 |
6
+ | [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](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/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-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, 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-28b - TransferOrderListActionsSurfaceSeed.sql, 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 |
11
11
  | [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
12
12
  | [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Aig/_modules.txt, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql |
13
+ | [Targeting the right database (tenant restores, and running a change file against the wrong DB)](workflows/targeting-the-right-database.md) | Two ways to point at the wrong database — a dump taken from the wrong host, and a change file piped into the wrong schema. | api2/Config/production.ini, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql |
13
14
  | [Verifying whether a dbchanges2 migration actually ran in an environment](workflows/verifying-a-migration-ran.md) | dbchanges2 has no execution ledger you can query — "did this file run here?" has to be answered from the **rows the file would have produced**. | Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql |
@@ -6,11 +6,10 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: [bala, kyalamarthi]
9
+ updated: 2026-08-31
10
+ owners: [bala, kyalamarthi, apeterson]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
- - dbchanges2/Client_Compass/2026-08-28 - GrantViewOrdersRoleSalesOrdersNavigation.sql
14
13
  - dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql
15
14
  related:
16
15
  - ../architecture.md
@@ -6,9 +6,11 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: [jcardinal, apeterson]
9
+ updated: 2026-08-31
10
+ owners: [jcardinal, apeterson, tcox]
11
11
  files:
12
+ - dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
13
+ - dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
12
14
  - toga25-supply/db-migrations/PLAYBOOK.md
13
15
  - toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md
14
16
  - dbchanges2/Client_Compass/
@@ -563,7 +565,10 @@ All four gated buttons (17/18/19/21) carry the **Core** rule `order._status in [
563
565
  so on a **canceled** order the approval-workflow button is **hidden**.
564
566
 
565
567
  **Next free Core ids in PROD, 2026-08-28:** `Surfaces` **47**, `SurfaceElements` **170**, `Messages`
566
- **227**, `Actions` **17**. (Re-verify per the 🚨 rule below before writing a seed — "next free" vs
568
+ **227**, `Actions` **17**. **Superseded — on 2026-08-31 (sandbox-client, freshly reset from prod) the
569
+ MAX ids were `Surfaces` 52 / `SurfaceElements` 189 / `Messages` 260, and `Core/2026-08-28a`+`b` then
570
+ claimed `Surfaces` 53–57 / `SurfaceElements` 190–206 / `Messages` 261–284.** (Re-verify per the 🚨
571
+ rule below before writing a seed — "next free" vs
567
572
  "highest used" is exactly what caused the 2026-08-24 collision.)
568
573
 
569
574
  **Per-tenant overrides, as they actually exist in prod:**
@@ -605,6 +610,50 @@ the LIST surface + 4 record-modal SECTION surfaces + the `transfer-order-stage`
605
610
  verified free on **local AND production** before authoring. See
606
611
  [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
607
612
 
613
+ #### ⚠ Those blocks were RENUMBERED on 2026-08-31 — a reservation expires when someone else ships
614
+
615
+ The 2026-08-28 blocks above were reserved from a **2026-08-28 snapshot** of production, and were
616
+ **claimed by another feature before the transfer-order files shipped**: `sales-order-denial-details`
617
+ took `Surfaces` **47**, `SurfaceElements` **170–176** and `Messages` **227–232**. Re-verified against
618
+ sandbox-client (freshly reset from production) on 2026-08-31, where MAX ids were `Surfaces` **52** /
619
+ `SurfaceElements` **189** / `Messages` **260**, the two transfer-order files were renumbered to a
620
+ contiguous block above MAX:
621
+
622
+ | | Reserved 2026-08-28 (stale) | **Live, as committed 2026-08-31** |
623
+ |---|---|---|
624
+ | `Core.Messages` | 227–260 | **261–284** |
625
+ | `Core.Surfaces` | 47–52 | **53–57** |
626
+ | `Core.SurfaceElements` | 170–189 | **190–206** |
627
+
628
+ `Surfaces` 48–51, `SurfaceElements` 177–186 and `Messages` 233–250 are deliberately left as **unused
629
+ gaps** rather than back-filled — a contiguous block above MAX is trivially verifiable and trivially
630
+ reversible; hunting free holes is neither.
631
+
632
+ Two durable rules:
633
+
634
+ 1. **A reserved block is a snapshot, not a lock.** Nothing in Core enforces it. **Re-verify the block
635
+ is still free immediately before the file runs**, not just when it is written, and say so in the
636
+ file header. The longer a seed sits unmerged, the more likely its block is gone.
637
+ 2. **Renumbering is cheap; colliding is not.** Because these seeds use literal ids with plain
638
+ `INSERT`, a stale block fails loudly on the primary key — which is the designed behavior and the
639
+ reason `INSERT IGNORE` is banned here (see the 2026-08-24 incident below). Bump to a fresh block
640
+ above MAX rather than trying to reuse the gap.
641
+ 3. **`SELECT MAX(id)` cannot see a block reserved by a seed that has not RUN.** A DB check only
642
+ reflects applied migrations, so a same-day sibling sitting unmerged in another developer's tree is
643
+ invisible to it. Verifying a block means querying the target Core **and** checking the in-flight
644
+ `Core/*SurfaceSeed.sql` files (this doc's block list is the shared record of them).
645
+
646
+ **The filename suffix is a separate check from the id block.** The denial-details pair was authored
647
+ as `2026-08-28a` and renamed to **`2026-08-31a`** because a committed
648
+ `Core/2026-08-28a - API 2.0 Stability Monitor Cron.sql` already held that date+letter. A free
649
+ date+letter says nothing about a free id block, and vice versa — check both.
650
+
651
+ > These explicit ids are **correct and required**, not a portability bug. `Core.Surfaces` /
652
+ > `Core.SurfaceElements` / `Core.Messages` are reserved-literal tables precisely so a `Client_*`
653
+ > override file can inline them without a cross-cluster `Core.*` read. Do not "fix" a surface seed
654
+ > into a `messageKey`-resolving `INSERT … SELECT`: the client-side references have no way to resolve
655
+ > a Core id at runtime.
656
+
608
657
  **⚠ The reserved blocks are not an inventory of every surface id — pre-2026-08-21 surfaces sit
609
658
  outside them.** The reserved-id discipline begins at the reseed, so surfaces seeded before it took
610
659
  plain `AUTO_INCREMENT` ids: the **`navigation`** TAB_STRIP is `Core.SurfaceElements` **109–114**
@@ -692,6 +741,26 @@ Quad disabled-tooltip statement now does this). Relevant to `Client_Quad/2026-08
692
741
  SalesOrderDecisionSummaryOverride.sql`, whose header wrongly implies a pre-existing row is
693
742
  necessarily the correct row.
694
743
 
744
+ ### 🚨 A client-wide override is SHADOWED by an existing role-scoped row — widen at the SAME tier (2026-08-31)
745
+
746
+ The resolver cascade is **base < client-wide < persona < role** (last write wins — see
747
+ [surface-resolver](../../_underscore/features/surface-resolver.md)). So a `roleId NULL` (client-wide)
748
+ row **does nothing** for any user whose role already has a row for that `(element, attribute)`: the
749
+ role-scoped row is applied after it and wins. Writing the widening client-wide "for everyone" is the
750
+ natural authoring instinct and it is **inert for exactly the users who need it**.
751
+
752
+ Concrete: widening element **19** (`approvalWorkflow`) so Compass admins can open a canceled order
753
+ read-only had to be an **Admin role-scoped** `UPDATE`-then-`INSERT` (role resolved at runtime by
754
+ `Roles.name = 'Admin'`, guarded `IS NOT NULL`), because `Client_Compass` already holds Admin
755
+ role-scoped rows on that element (sandbox: id 3 `IS_VISIBLE`, id 10 `VISIBILITY_RULE`, id 20
756
+ `ENABLED_RULE`). Caught by sql-reviewer before the run.
757
+
758
+ **And widen every attribute that gates the control, not just visibility.** Element 19 also carries an
759
+ Admin **`ENABLED_RULE`** limited to pending statuses, so widening `VISIBILITY_RULE` alone leaves the
760
+ button visible-but-**disabled** and the modal still cannot open. Before authoring a rule widening,
761
+ `SELECT` the element's existing override rows and read **every** row's scope *and* attribute — the
762
+ answer is in the data, not in the base seed.
763
+
695
764
  ## Migration maintenance — fold-back (⚠ the local-only window has CLOSED)
696
765
 
697
766
  > **Superseded 2026-08-21/24.** The Surface layer **has been pushed to production** (a hand-picked
@@ -978,6 +1047,28 @@ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that kee
978
1047
  `config.valueKey` when it lives in the page.
979
1048
 
980
1049
  ## Change history
1050
+ - 2026-08-31 — Added the **denial-details** seed pair (`Core/2026-08-31a -
1051
+ SalesOrderDenialDetailsSurfaceSeed.sql` — Surface **47**, elements **170–176**, Messages
1052
+ **227–232**, plus a `readOnlyRule` marker on surface 41; and `Client_Compass/2026-08-31a -
1053
+ SalesOrderDenialDetailsReadOnlyRules.sql`). Committed on branch **`TRUE-80850`** (`38ef9ff`), **not
1054
+ merged to `_main`**; run on **`client-sandbox` only** and confirmed rendering there. (This is the
1055
+ feature that claimed 47/170–176/227–232 and forced the transfer-order renumber above.) Two
1056
+ authoring rules: **a client-wide (`roleId NULL`) override is shadowed by an existing role-scoped
1057
+ row**, so the Compass read-only widening on element 19 had to be an Admin **role-scoped**
1058
+ `UPDATE`-then-`INSERT` — and it had to widen the Admin **`ENABLED_RULE`** as well as
1059
+ `VISIBILITY_RULE`, or the button stays visible-but-disabled; and **`SELECT MAX(id)` cannot see a
1060
+ block reserved by a seed that has not run**, so a block check must also sweep in-flight seed files.
1061
+ Also noted the filename date+letter is an independent check from the id block (this pair was
1062
+ renamed off `2026-08-28a`, already taken by a committed Core file). (tcox)
1063
+ - 2026-08-31 — **Renumbered the transfer-order reserved blocks** to `Messages` **261–284**,
1064
+ `Surfaces` **53–57**, `SurfaceElements` **190–206**: the 2026-08-28 blocks were reserved from a
1065
+ snapshot and `sales-order-denial-details` claimed `Surfaces` 47 / `SurfaceElements` 170–176 /
1066
+ `Messages` 227–232 first. New rule — **a reserved block is a snapshot, not a lock; re-verify it is
1067
+ free immediately before the file RUNS, not just when it is written**, and bump to a contiguous
1068
+ block above MAX (leaving gaps) rather than back-filling. Also recorded explicitly that literal
1069
+ Core ids in a surface seed are **required, not a portability bug**, so nobody "fixes" one into a
1070
+ `messageKey`-resolving `INSERT … SELECT`. From `Core/2026-08-28a`/`b`, committed in `dbchanges2`
1071
+ `1c3fe2e`. (apeterson)
981
1072
  - 2026-08-28 — **Extended the transfer-order reserved blocks** to `Messages` **227–260**, `Surfaces`
982
1073
  **47–52**, `SurfaceElements` **170–189** (`Core/2026-08-28b`, the `transfer-orders-list-actions`
983
1074
  BUTTON_BAR; all ids verified free on local **and** production first). Three authoring rules added: