toga-ai 1.0.643 → 1.0.645
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/INDEX.md +1 -1
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +39 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +4 -3
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +40 -3
- package/knowledge/2.0/apps/_underscore/features/netsuite-rest-client.md +48 -1
- package/knowledge/2.0/apps/_underscore/features/sales-order-status-filter-surface.md +148 -0
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +37 -1
- package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +67 -1
- package/knowledge/2.0/apps/api2/features/v2-api-error-codes.md +15 -2
- package/knowledge/2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md +42 -2
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +87 -1
- package/knowledge/2.0/apps/toga-blox/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga-blox/features/table.md +23 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/quad/profile.md +2 -1
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding pages to one — the folder-based MVC routing, the page lifecycle, t | library/app/framework.php, library/app/frameworkindex.php, library/app/mvc.php, library/app/database.php, library/app/model.php, library/app/config.php |
|
|
17
17
|
| [isFulfillable from NetSuite during Item Sync (Phase 1)](features/netsuite-item-isfulfillable-sync.md) | This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite `isfulfillable` flag during item sync and stamping it onto the **Agilant | library/app/netsuite.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php |
|
|
18
18
|
| [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem, test/@dave/Junk Drawer/nsq.php |
|
|
19
|
-
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
|
|
19
|
+
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php, library/app/netsuite.php |
|
|
20
20
|
| [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | `App_SystemMonitor_NetSuiteIntegration` (`library/app/systemmonitor/netsuiteintegration.php`, title **"NetSuite Sync Alert"**) is a 1.0 system monitor that watc | library/app/systemmonitor/netsuiteintegration.php, worker/crons/infrastructure/system_monitors.php |
|
|
21
21
|
| [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. | library/app/api/toga2.php, library/app/api/startechticket.php, worker/crons/toga2/startech/common_import_supporting_records.php |
|
|
22
22
|
| [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` (`library/app/api/toga2.php`, ~8400 lines) is the **1.0-side client for the TOGa 2 (`_underscore`/api2) public API** *and* the home of the cross | library/app/api/toga2.php, worker/crons/toga2/aig/sync_togasupply_aig.php, worker/crons/toga2/wje/sync_togasupply_wje.php, test/@Mark/AIG/test_multi_email.php |
|
|
@@ -6,14 +6,16 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [dfranks]
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [dfranks, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
13
|
+
- library/app/netsuite.php
|
|
13
14
|
related:
|
|
14
15
|
- netsuite-suiteql-api-reference.md
|
|
15
16
|
- ../architecture.md
|
|
16
17
|
- ../../worker/features/forecast2-netsuite-reconciliation.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/netsuite-rest-client.md
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Summary
|
|
@@ -90,6 +92,35 @@ Forecast2 tables already store.
|
|
|
90
92
|
- **TO_DATE rejects impossible dates** (e.g. `2026-06-31`) with an opaque HTTP 400 — validate
|
|
91
93
|
calendar dates before building SuiteQL.
|
|
92
94
|
|
|
95
|
+
## Transaction status sourcing (1.0) — and the 2026.2 REST `status.id` change
|
|
96
|
+
|
|
97
|
+
1.0 reaches NetSuite over **two transports, and neither reads status off a REST record GET
|
|
98
|
+
body**:
|
|
99
|
+
|
|
100
|
+
- **REST adapter (`App_Api_Netsuite_Rest`, `rest.php`).** It *does* issue `/record/v1/...`
|
|
101
|
+
GETs, but it sources `status` from the **SuiteQL header row** (`$h->status`, from the
|
|
102
|
+
`SELECT ... status ... FROM ...` at ~L381/574/754 — already a letter) and maps that letter
|
|
103
|
+
to the SOAP-era text label via an internal **`$statusMap`** (~L471-472, L625-626,
|
|
104
|
+
L853-854/863). The REST GET body (`$rec`) is only read for shipMethod / opportunity / terms /
|
|
105
|
+
createdFrom / country — **never status**. Downstream forecast2 crons
|
|
106
|
+
(`worker/crons/toga2/forecast2/*`) compare `$nsRecord->status` to text, but that text is
|
|
107
|
+
*adapter-produced* (from the SuiteQL letter via `$statusMap`), not from the changing REST
|
|
108
|
+
field.
|
|
109
|
+
- **SOAP (`App_NetSuite`, `library/app/netsuite.php`).** Always uses `new NetSuiteService()`
|
|
110
|
+
(SuiteTalk toolkit) and reads transaction status off the SOAP record as text (e.g.
|
|
111
|
+
`$items->status != 'Billed'/'Cancelled'/'Closed'` ~L1151/L1434; `$nsInvoice->status ==
|
|
112
|
+
'Paid In Full'` ~L2546). SOAP is a different API and is unaffected. (The many
|
|
113
|
+
`->status->isSuccess` / `statusDetail` reads elsewhere are the SOAP **response envelope**,
|
|
114
|
+
not the record's business status.)
|
|
115
|
+
|
|
116
|
+
> **NetSuite 2026.2 impact: none for 1.0.** 2026.2 standardizes the `status.id` returned by
|
|
117
|
+
> **REST record GETs** for transactions to single-letter codes across all types (some types
|
|
118
|
+
> return it as text like `"Open"` today); `status.refName` and SuiteQL are unchanged, and
|
|
119
|
+
> SOAP is a separate API. Since no 1.0 code reads `status.id` off a REST GET body, no patch is
|
|
120
|
+
> needed. NetSuite SuiteAnswers ID **89313**. The 2.0 side of the same conclusion (shared REST
|
|
121
|
+
> client + worker2 consumers) is in
|
|
122
|
+
> [`netsuite-rest-client.md`](../../../2.0/apps/_underscore/features/netsuite-rest-client.md).
|
|
123
|
+
|
|
93
124
|
## Data model
|
|
94
125
|
|
|
95
126
|
Reads NetSuite `transaction`, `transactionline`, `previoustransactionlinelink` via SuiteQL and
|
|
@@ -108,6 +139,12 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
108
139
|
|
|
109
140
|
## Change history
|
|
110
141
|
|
|
142
|
+
- 2026-08-25 — Documented **1.0 transaction-status sourcing across both transports** and the
|
|
143
|
+
conclusion that the **NetSuite 2026.2 REST `status.id` text→letter standardization does not
|
|
144
|
+
affect 1.0** (SuiteAnswers 89313). `App_Api_Netsuite_Rest` derives status from the SuiteQL
|
|
145
|
+
letter and maps it via an internal `$statusMap` — never from the REST GET body; forecast2
|
|
146
|
+
crons see that adapter-produced text, not the changing field; `library/app/netsuite.php` is
|
|
147
|
+
pure SOAP (a different API). Discovery/impact-analysis only — no code changed. (jcardinal)
|
|
111
148
|
- 2026-06-11 — Documented bulk SuiteQL field semantics (tl.id==line, ShipItem NULL cost, iscogs
|
|
112
149
|
COGS filter, createdFrom non-determinism, foreignamountunpaid, sign conventions) surfaced while
|
|
113
150
|
moving the Forecast2 trueup tools off per-id REST GETs. (dfranks)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
| [Core.Domains — the app host registry (and why you cannot derive a host)](features/core-domains-app-host-registry.md) | `Core.Domains` is the **authoritative registry of which host serves which client's app in which environment**. | _underscore/Model/Core/Domain.php, api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, worker/config.beta.ini |
|
|
20
20
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
21
21
|
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
|
|
22
|
-
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
22
|
+
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Quad/ApprovalDecision.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
23
23
|
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/issue/get.php, tools/mvc/errors/post.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
24
24
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
25
25
|
| [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` is the 2.0 field type for blob-backed columns (S3 or local folder). | _underscore/Model.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/Invoice.php, dbchanges2/Core/HISTORIC/2024/2024-11b - item-fulfillments.sql |
|
|
@@ -30,15 +30,16 @@
|
|
|
30
30
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php, _underscore/Model.php, _underscore/Model/Rate/Subscription.php |
|
|
31
31
|
| [_Model::save() parent FK cascade — stored-SQL-field recompute deadlocks](features/model-save-parent-cascade-stored-field-deadlock.md) | `_Model::save()` runs a **generic parent foreign-key cascade**: inserting (or saving) a child row that carries an FK to a parent causes `_Model` to **re-load an | _underscore/Model.php, _underscore/Model/Client/PurchaseOrder.php, _underscore/Model/Client/AdvanceShippingNotice.php |
|
|
32
32
|
| [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php, _underscore/Model/Rate/Subscription.php |
|
|
33
|
-
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
33
|
+
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php, _underscore/Trait/Netsuite/SalesOrder.php, worker2/Worker/Netsuite/SalesOrder.php, worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Worker/Netsuite/Opportunity.php |
|
|
34
34
|
| [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | `_Trait_Netsuite_SalesOrder` is the **shared** sales-order importer composed into **22 client models** (every client on the dbchanges2 `netsuite` module). | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Model.php, dbchanges2/_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql |
|
|
35
35
|
| [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>` — still the live path for `toga2-supply` and other pre-Surf | _underscore/Model/Core/Page.php, _underscore/Model/Client/TableView.php, toga2-supply/src/components/ui/Tables/PrimaryTable/PrimaryTable.tsx |
|
|
36
36
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
|
|
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
|
+
| [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 |
|
|
40
41
|
| [_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 |
|
|
41
|
-
| [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 | 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 |
|
|
42
|
+
| [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 |
|
|
42
43
|
| [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 |
|
|
43
44
|
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
44
45
|
| [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 |
|
|
@@ -6,10 +6,12 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
13
|
+
- _underscore/Model/Quad/ApprovalDecision.php
|
|
14
|
+
- _underscore/Model/Quad/SalesOrder.php
|
|
13
15
|
- _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php
|
|
14
16
|
- _underscore/Email.php
|
|
15
17
|
related:
|
|
@@ -105,9 +107,31 @@ can keep using `sendEmail($api, ...)`.
|
|
|
105
107
|
→ `$args = ['userName'=>$name, ...]`). Passing the map as a single positional argument
|
|
106
108
|
(`sendEmail($api, $uuid, $to, [], [], $vars)`) makes `$args = [0 => [...]]` — a numeric key
|
|
107
109
|
whose value is an *array*. `replaceTemplateVariables()` then runs `str_replace('{0}', <array>, …)`
|
|
108
|
-
→ `TypeError: str_replace(): Argument #2 must be of type string
|
|
109
|
-
production 500 / **EO-1
|
|
110
|
+
→ `TypeError: str_replace(): Argument #2 ($replace) must be of type string when argument #1
|
|
111
|
+
($search) is a string`, surfacing as a production 500 / **EO-1** — it fails the **whole request**,
|
|
112
|
+
not just one placeholder. `Model/Compass/*` gets this right (spread); the four `Model/Quad/*`
|
|
110
113
|
call sites were copied from Compass but dropped the `...` — fixed 2026-08-18.
|
|
114
|
+
- **This contract has regressed TWICE.** `c22999d9` (2026-05-19) **removed** the spread and broke
|
|
115
|
+
Quad order-approval emails; `2b0c1470` (2026-08-18) restored it. `sendEmail` has been variadic
|
|
116
|
+
since **2024-12-24**, so the spread has **always** been the correct form and the May commit was
|
|
117
|
+
the regression, not a style change. The contract and both regressions are now recorded on
|
|
118
|
+
`sendEmail()`'s docblock, with a marker comment at each of the four Quad call sites — because
|
|
119
|
+
that is where the mistake actually gets made.
|
|
120
|
+
- **Hardened 2026-08-25 — a mis-spread caller now degrades instead of 500-ing.**
|
|
121
|
+
`replaceTemplateVariables()` (now typed) **skips non-scalar values**, logging the key and the
|
|
122
|
+
**type only — never the value** (template vars carry user names and order numbers, so logging
|
|
123
|
+
them would be a PII violation). Result: a bad caller leaves an **unreplaced placeholder** in the
|
|
124
|
+
email rather than taking down order approval. Verified behaviourally: the old code fatals on the
|
|
125
|
+
no-spread shape, the new code degrades, and the correct spread path is **byte-identical**. The
|
|
126
|
+
spread is still the contract — the guard is a safety net, not a licence to pass a bare array.
|
|
127
|
+
- **⚠ Still seeing this error in an environment does NOT mean the code is wrong — check the deploy
|
|
128
|
+
FIRST.** Verified 2026-08-25: Quad order placement was still 500-ing in the **commerce
|
|
129
|
+
sandbox-client** environment (`EmailTemplate.php:82` via `_Model_Quad_SalesOrder::postPost`)
|
|
130
|
+
even though the fix `2b0c1470` was already committed on `_sandbox-client`. `_underscore` is
|
|
131
|
+
**cloned at build time from a moving per-environment branch**, so the running artifact simply
|
|
132
|
+
predated the commit. That is a **deploy gap, not a code bug** — a redeploy fixes it, and no code
|
|
133
|
+
change should be made. Same known-issue as
|
|
134
|
+
[api2 environment-variable-drives-underscore-branch](../../api2/features/environment-variable-drives-underscore-branch.md).
|
|
111
135
|
- **`sendEmail()`'s signature is load-bearing for scripted APIs** — the Record Script engine
|
|
112
136
|
(`api2/Component/Api/V2/V2.php`, ~line 3594) calls the method with `api` as a named
|
|
113
137
|
argument, so the first param must stay `&$api`. Do not "clean it up" by removing it.
|
|
@@ -161,6 +185,19 @@ worker method) in-process instead.
|
|
|
161
185
|
|
|
162
186
|
## Change history
|
|
163
187
|
|
|
188
|
+
- 2026-08-25 — Added the **deploy-gap** caveat to the spread gotcha: Quad order placement was still
|
|
189
|
+
500-ing in the commerce **sandbox-client** environment after `2b0c1470` landed, because
|
|
190
|
+
`_underscore` is cloned at build from a moving per-environment branch and the running artifact
|
|
191
|
+
predated the commit. Diagnose the deploy before touching the code. (apeterson)
|
|
192
|
+
- 2026-08-25 — Hardened `replaceTemplateVariables()` (signature typed) to **skip non-scalar values**,
|
|
193
|
+
logging key + **type only** (never the value — template vars carry user names and order numbers), so
|
|
194
|
+
a mis-spread caller degrades to an unreplaced placeholder instead of a 500 that kills the whole
|
|
195
|
+
request. Verified the old code fatals on the no-spread shape, the new code degrades, and the correct
|
|
196
|
+
spread path is byte-identical. Recorded that the spread contract has regressed **twice** —
|
|
197
|
+
`c22999d9` (2026-05-19) removed it and broke Quad order-approval emails, `2b0c1470` (2026-08-18)
|
|
198
|
+
restored it — and that `sendEmail` has been variadic since 2024-12-24, so the spread was always
|
|
199
|
+
correct and May was the regression. Documented the contract on `sendEmail()`'s docblock plus a marker
|
|
200
|
+
comment at each of the four Quad call sites. (apeterson)
|
|
164
201
|
- 2026-08-18 — Fixed the Quad order-approval/rejection emails' production 500
|
|
165
202
|
(`str_replace(): Argument #2 must be string`, EO-1): the four `Model/Quad/ApprovalDecision.php`
|
|
166
203
|
+ `Model/Quad/SalesOrder.php` call sites passed the template-vars map as a bare positional
|
|
@@ -6,14 +6,19 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: ["dfranks", "jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
13
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
14
|
+
- worker2/Worker/Netsuite/SalesOrder.php
|
|
15
|
+
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
16
|
+
- worker2/Worker/Netsuite/Opportunity.php
|
|
13
17
|
related:
|
|
14
18
|
- ../../worker2/features/netsuite-salesorder-open-orders-sync.md
|
|
15
19
|
- ../../worker2/features/netsuite-salesorder-outbound-push.md
|
|
16
20
|
- ../../worker2/features/netsuite-opportunity-sync.md
|
|
21
|
+
- ../../library/features/netsuite-suiteql-rest-shim.md
|
|
17
22
|
- ../architecture.md
|
|
18
23
|
---
|
|
19
24
|
|
|
@@ -167,6 +172,40 @@ rather than a bad query. Confirmed live 2026-07-28.
|
|
|
167
172
|
Surfaced building the NYCHH asset-tag backfill (see
|
|
168
173
|
[nychh-asset-tag-backfill](../../worker2/features/nychh-asset-tag-backfill.md)).
|
|
169
174
|
|
|
175
|
+
## NetSuite transaction status sourcing (2.0) — and the 2026.2 REST `status.id` change
|
|
176
|
+
|
|
177
|
+
**`fetchRecord()` returns the REST record UNTOUCHED — there is no central status
|
|
178
|
+
normalization in the shared client.** `fetchRecord()` (`Netsuite.php` ~L215-223) does
|
|
179
|
+
`send('GET', route)`, json-decodes, and hands the raw record back; it never reads or maps
|
|
180
|
+
`status`. So whether a 2.0 consumer is exposed to a change in the REST record's `status`
|
|
181
|
+
field depends entirely on *that consumer*, not on the client. Audited 2026-08-25 — every 2.0
|
|
182
|
+
consumer sources transaction status from a change-safe field:
|
|
183
|
+
|
|
184
|
+
- **`worker2/Worker/Netsuite/SalesOrder.php`** reads `status->refName` (the human-readable
|
|
185
|
+
label) and compares it to `OPEN_STATUSES` text — never `status.id`.
|
|
186
|
+
- **`worker2/Component/Forecast/SaleImport/SaleImport.php`** — `statusName()` (~L845-853) reads
|
|
187
|
+
`status->refName`, matched against `EXCLUDED_STATUSES` text. This is the shared engine that
|
|
188
|
+
Invoice / CashSale / CreditMemo / CashRefund import all delegate to.
|
|
189
|
+
- **`worker2/Worker/Netsuite/Opportunity.php`** — `fetchStatus()` (~L431-444) gets transaction
|
|
190
|
+
status via **SuiteQL** (`SELECT status FROM transaction`), which already returns the letter
|
|
191
|
+
and maps `A`/`C`/`D`. (Its `entityStatus.id` at ~L197 is a *different* field — an opaque
|
|
192
|
+
lookup key, not the transaction status.)
|
|
193
|
+
- **`_underscore/Trait/Netsuite/SalesOrder.php`** — the one text `switch ($nsOrder->status)`
|
|
194
|
+
(~L589-620) is driven by a **SOAP** `$service->get()` record, not REST.
|
|
195
|
+
- **`api2`** makes no NetSuite calls of its own — it only registers the SDK at boot and writes
|
|
196
|
+
one hardcoded outbound status literal (`PENDING_FULFILLMENT`); its `->status` reads are its
|
|
197
|
+
own HTTP response envelope. Not a NetSuite status consumer.
|
|
198
|
+
|
|
199
|
+
> **NetSuite 2026.2 impact: none for 2.0.** NetSuite 2026.2 standardizes the `status.id` field
|
|
200
|
+
> returned by **REST record GETs** for transaction records to single-letter codes (A/B/D/E/V)
|
|
201
|
+
> across all transaction types (today some types return `status.id` as text like `"Open"`);
|
|
202
|
+
> `status.refName` and SuiteQL are unchanged, and SOAP/SuiteTalk is a different API. Because no
|
|
203
|
+
> 2.0 code reads `status.id` off a REST GET body as text, no patch is needed. NetSuite
|
|
204
|
+
> SuiteAnswers ID **89313**. (Not audited: other 2.0 apps that may consume this client —
|
|
205
|
+
> `toga2-supply`, `toga25-supply`, `tools` — check them the same way if touched.) The 1.0 side
|
|
206
|
+
> of the same conclusion is in
|
|
207
|
+
> [`netsuite-suiteql-rest-shim.md`](../../library/features/netsuite-suiteql-rest-shim.md).
|
|
208
|
+
|
|
170
209
|
## Logging
|
|
171
210
|
|
|
172
211
|
`send()` calls `setLogging(false)`, so **NetSuite REST request/response bodies are NOT written to
|
|
@@ -192,6 +231,14 @@ doc.)
|
|
|
192
231
|
|
|
193
232
|
## Change history
|
|
194
233
|
|
|
234
|
+
- 2026-08-25 — Documented **2.0 NetSuite transaction-status sourcing** and the conclusion that
|
|
235
|
+
the **NetSuite 2026.2 REST `status.id` text→letter standardization does not affect 2.0**
|
|
236
|
+
(SuiteAnswers 89313). `fetchRecord()` returns the REST record raw (no central status
|
|
237
|
+
normalization), and every audited consumer reads status from `status.refName` or SuiteQL, or
|
|
238
|
+
over SOAP — none reads REST `status.id` as text: worker2 SalesOrder/SaleImport (`refName`),
|
|
239
|
+
worker2 Opportunity (`fetchStatus()` via SuiteQL), `_underscore` SalesOrder trait (SOAP),
|
|
240
|
+
api2 (no NetSuite calls). Discovery/impact-analysis only — no code changed. (jcardinal)
|
|
241
|
+
|
|
195
242
|
- 2026-08-20 — Added **idempotent creates via `externalId`**: `createRecord()` has no idempotency, so
|
|
196
243
|
stamp an `externalId` (NetSuite rejects a duplicate within the account) and read the record back
|
|
197
244
|
with `GET /record/v1/<record>/eid:<externalId>` — the non-SuiteQL fallback, which matters because
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/TableView.php
|
|
13
|
+
- _underscore/Model/Core/Surface.php
|
|
14
|
+
- _underscore/Model/Client/SalesOrder.php
|
|
15
|
+
- _underscore/Model/Quad/SalesOrder.php
|
|
16
|
+
- _underscore/Model/Prudential/SalesOrder.php
|
|
17
|
+
- toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts
|
|
18
|
+
- toga-blox/src/api/types.ts
|
|
19
|
+
- dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql
|
|
20
|
+
- dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
21
|
+
- dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
22
|
+
- dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
23
|
+
- dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
24
|
+
related:
|
|
25
|
+
- surface-resolver.md
|
|
26
|
+
- item-fulfillment-stage-lifecycle-and-order-status.md
|
|
27
|
+
- ../../dbchanges2/features/surface-layer-schema.md
|
|
28
|
+
- ../../toga-blox/features/table.md
|
|
29
|
+
- ../../api2/features/tableview-field-metadata.md
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What it is
|
|
33
|
+
|
|
34
|
+
The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface
|
|
35
|
+
`FILTER_SET` rather than by the raw contents of the client's `SalesOrderStatuses` table.
|
|
36
|
+
|
|
37
|
+
Before: `SalesOrderStatuses` is a per-client (`DB_CLIENT`) table, but every tenant was seeded
|
|
38
|
+
identically from `BLANK_CLIENT_DATABASE`, and toga-blox fetched the filter options with an
|
|
39
|
+
**unscoped** `GET /{recordRoute}`. Result: all 12 statuses were offered to every client, including
|
|
40
|
+
statuses that tenant can never reach.
|
|
41
|
+
|
|
42
|
+
After: the backend emits a resolved `filterOptions` list (and the owning `surfaceSlug`) on the
|
|
43
|
+
`TYPE__STATUS` field in table meta; the front end renders exactly what it is given.
|
|
44
|
+
|
|
45
|
+
## The two layers (why this is not just "hide rows")
|
|
46
|
+
|
|
47
|
+
| Layer | Source | Answers |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| **EXISTENCE** | the client's own `SalesOrderStatuses` rows | which statuses this tenant *has* — also supplies name + ordering |
|
|
50
|
+
| **BUSINESS** | the Core `FILTER_SET` surface + `SurfaceOverrides` | which of those the tenant should be *offered* |
|
|
51
|
+
|
|
52
|
+
The emitted set is the **intersection**, computed automatically. A client that simply doesn't have a
|
|
53
|
+
status needs no override row at all (this is why **Elite** needs zero rows).
|
|
54
|
+
|
|
55
|
+
## Data model
|
|
56
|
+
|
|
57
|
+
One `Core.Surfaces` row (**id 44**, slug `sales-order-status-filter`, `type = FILTER_SET`,
|
|
58
|
+
`recordId` = the `sales-order-statuses` record, `parentSurfaceId = 2` `sales-orders-list`), plus one
|
|
59
|
+
**BADGE** `SurfaceElement` per status (**ids 147–160**).
|
|
60
|
+
|
|
61
|
+
- Elements carry **no `labelMessageId`**. Label and colour resolve from the `sales-order-status`
|
|
62
|
+
**vocabulary term** whose `value` matches the element's `config.value`. (Vocabulary terms are
|
|
63
|
+
Core-global — see [surface-resolver](surface-resolver.md).)
|
|
64
|
+
- Per-client hiding is a plain `SurfaceOverrides` row with `IS_VISIBLE = 0`. No new cascade, no
|
|
65
|
+
framework schema change: it rides the existing **base < client < persona < role** override chain.
|
|
66
|
+
- Element count is **14**, not 12 — the set is the **UNION** across tenants (see *Vocabularies are
|
|
67
|
+
not uniform* below).
|
|
68
|
+
|
|
69
|
+
## How it works (backend — `_Model_Client_TableView`)
|
|
70
|
+
|
|
71
|
+
Two new private helpers on the MAIN table-meta builder:
|
|
72
|
+
|
|
73
|
+
- `getStatusFilterSurfaceSlug()` — loads the `FILTER_SET` surface by `Surfaces.recordId`.
|
|
74
|
+
- `getStatusFilterOptions()` — calls `_Model_Core_Surface::resolve()` (so the whole override cascade
|
|
75
|
+
applies) and subtracts the hidden slugs from the client's own status rows.
|
|
76
|
+
|
|
77
|
+
Both return **null** when no surface is configured, so an unconfigured table keeps its previous
|
|
78
|
+
behaviour instead of rendering an empty dropdown. Only the **main** meta builder needed this — the
|
|
79
|
+
nested-table builder emits no `isFilterable`. (commit `01b135f4`)
|
|
80
|
+
|
|
81
|
+
## How it works (front end — toga-blox)
|
|
82
|
+
|
|
83
|
+
`useFetchTablePageMeta.ts` now **prefers server-supplied `filterOptions`** and skips the
|
|
84
|
+
`recordRoute` fetch for those fields. The query list and the index lookup share one
|
|
85
|
+
`needsFetchedOptions` predicate so the two can never skew. `src/api/types.ts`: `filterOptions[].uuid`
|
|
86
|
+
is now optional and `surfaceSlug` is declared on `TableViewField`. (commit `dded92a6` — this
|
|
87
|
+
implements a TODO blox already carried in that file.)
|
|
88
|
+
|
|
89
|
+
> blox `dist/` is gitignored — consumers only see this after `npm run build`.
|
|
90
|
+
|
|
91
|
+
## Onboarding a client: auditing which statuses it can actually reach
|
|
92
|
+
|
|
93
|
+
There are **three independent sources** of a reachable status. Check all three — none alone is
|
|
94
|
+
sufficient:
|
|
95
|
+
|
|
96
|
+
1. **Static CASE branches** in `_Model_<Client>_SalesOrder::_status()`.
|
|
97
|
+
2. **Explicit stage** — `SalesOrders.salesOrderStageId` → `SalesOrderStages.salesOrderStatusId`.
|
|
98
|
+
3. **Approval stage** — `SalesOrderStatuses.pendingApprovalTemplateStageId` → `ApprovalTemplateStages`.
|
|
99
|
+
|
|
100
|
+
**Trap — counting `SalesOrderStages` rows measures CONFIGURATION, not USAGE.** `SalesOrderStages`
|
|
101
|
+
has **no `salesOrderId`**; it is a lookup table. To count real orders you must join through
|
|
102
|
+
`SalesOrders.salesOrderStageId`.
|
|
103
|
+
|
|
104
|
+
**Trap — scraping `SLUG__*` constants over-reports.** `_Model_Client_ItemFulfillmentStatus::SLUG__SHIPPED`
|
|
105
|
+
and `APP_SLUG__COMMERCE` are *different vocabularies*, not emitted sales-order statuses.
|
|
106
|
+
|
|
107
|
+
Usage data is an input, not the verdict: the **account team overrides it**. Compass's hide list
|
|
108
|
+
deliberately hides `billed` / `closed` / `pendingBilling` even though ~40k orders sit in those
|
|
109
|
+
states from the NetSuite billing sync.
|
|
110
|
+
|
|
111
|
+
### Current hide lists (2026-08-24)
|
|
112
|
+
|
|
113
|
+
| Client | Hidden |
|
|
114
|
+
|---|---|
|
|
115
|
+
| Compass USA / Compass Canada | `picked`, `packed`, `billed`, `closed`, `pendingBilling`, `pendingBillingPartiallyFulfilled` |
|
|
116
|
+
| Quad | `pendingInitialApproval`, `pendingBilling`, `pendingBillingPartiallyFulfilled` |
|
|
117
|
+
| NYCHH | `pendingInitialApproval`, `pendingApproval` (it has **no** `ApprovalTemplateStages` rows) |
|
|
118
|
+
| Elite | none needed — existence-intersection covers it |
|
|
119
|
+
|
|
120
|
+
## Gotchas
|
|
121
|
+
|
|
122
|
+
- **Client status vocabularies are NOT uniform.** Compass, Compass Canada, Quad and NYCHH carry the
|
|
123
|
+
same 12 slugs; **Elite differs** — it has `delivered` and `shipped` where the others have
|
|
124
|
+
`picked` / `packed`. Hence the Core element set is the 14-value union.
|
|
125
|
+
- **⚠ Prudential is a different vocabulary entirely and is excluded from this work.**
|
|
126
|
+
`_Model_Prudential_SalesOrder::_status()` returns `PurchaseOrderStages.name` (POD, Ready To Ship,
|
|
127
|
+
Cancelled, Rejected, Open, Received, Accepted, Configuration, In Progress) while
|
|
128
|
+
`Client_Prudential.SalesOrderStatuses` holds only `picked`/`packed` — so its status filter would
|
|
129
|
+
offer two values matching **zero** orders. Moot today (Prudential doesn't show the status column),
|
|
130
|
+
but the wiring is structurally wrong; do not "fix" it by adding override rows. Its `_status()` also
|
|
131
|
+
uses `LIMIT 1` with **no `ORDER BY`** on a non-unique join — the exact pattern the SA135481 rule in
|
|
132
|
+
[`2.0/standards/backend-php.md`](../../../standards/backend-php.md) forbids.
|
|
133
|
+
- **⚠ OPEN — Quad and `canceled`.** Quad's `_status()` has no `canceled` branch and zero
|
|
134
|
+
canceled-stage orders, yet the account team says Quad does use canceled. Possibly a bug. Flagged,
|
|
135
|
+
**not fixed**.
|
|
136
|
+
- Table/surface meta is cached with `Infinity` staleTime in blox — a hard reload is required after a
|
|
137
|
+
metadata DB change (see [toga-blox table](../../toga-blox/features/table.md)).
|
|
138
|
+
|
|
139
|
+
## Change history
|
|
140
|
+
- 2026-08-25 — Built the feature end to end: Core `FILTER_SET` surface 44 + BADGE elements 147–160
|
|
141
|
+
(labels/colours from the `sales-order-status` vocabulary, per-client hiding via `SurfaceOverrides`
|
|
142
|
+
`IS_VISIBLE=0`); `_Model_Client_TableView` emits `surfaceSlug` + `filterOptions` on `TYPE__STATUS`
|
|
143
|
+
via `getStatusFilterSurfaceSlug()` / `getStatusFilterOptions()` (null when unconfigured, so
|
|
144
|
+
behaviour is unchanged for tables with no surface); toga-blox `useFetchTablePageMeta` prefers
|
|
145
|
+
server-supplied options and skips the per-field `recordRoute` fetch. Recorded the two-layer
|
|
146
|
+
existence/business model, the three-source status-reachability audit method (+ the
|
|
147
|
+
`SalesOrderStages`-is-a-lookup-table and `SLUG__*`-scrape traps), the non-uniform Elite vocabulary,
|
|
148
|
+
and the structurally-wrong Prudential wiring. (apeterson)
|
|
@@ -6,9 +6,10 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
|
+
- _underscore/Model/Client/Language.php
|
|
12
13
|
- dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql
|
|
13
14
|
- dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql
|
|
14
15
|
- dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql
|
|
@@ -57,6 +58,7 @@ related:
|
|
|
57
58
|
- ../../dbchanges2/features/surface-layer-schema.md
|
|
58
59
|
- ../../api2/features/surface-meta-option.md
|
|
59
60
|
- acl-permission-chain.md
|
|
61
|
+
- sales-order-status-filter-surface.md
|
|
60
62
|
---
|
|
61
63
|
|
|
62
64
|
## What it is
|
|
@@ -501,6 +503,27 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
501
503
|
including refresh, without each auth flow having to embed the id). (This is a **separate** concern from
|
|
502
504
|
`id.language` driving the data-translation sidecar — see
|
|
503
505
|
[language-translation-layer](../../api2/features/language-translation-layer.md); do not conflate.)
|
|
506
|
+
**Extended 2026-08-25 — the 07-21 fix still left EVERY API-AUTHENTICATED request untranslated, for
|
|
507
|
+
every client.** The fallback chain stopped at `id.language`, but **api2 embeds `id.language` on USER
|
|
508
|
+
auth only** (`V2.php` ~L1501; the comment at ~L1862 states *"user auth only; null for API auth"*).
|
|
509
|
+
So an API-auth request carried neither claim, `_resolveLanguageId` returned `0`, and `_loadMessages`
|
|
510
|
+
skipped the `MessageTranslations` overlay — silently serving `Core.Messages.defaultValue`.
|
|
511
|
+
**Fix:** added `_Model_Client_Language::CODE_BASE = 'en'` and gave `_resolveLanguageId` a final
|
|
512
|
+
fallback to the **client's base-language row**. Precedence is now:
|
|
513
|
+
1. `id.client.languageId` — the future user-preference slot;
|
|
514
|
+
2. `id.language` code;
|
|
515
|
+
3. **client base language** (`CODE_BASE`);
|
|
516
|
+
4. `0` → Core defaults.
|
|
517
|
+
|
|
518
|
+
Steps 1–2 still win, so a real user preference is never overridden — this matters because language
|
|
519
|
+
**will** become a user preference (in TOGa Commerce the surface layer's language is already user-set).
|
|
520
|
+
- **Symptom shape that identifies this class of bug:** language-*independent* `SurfaceOverrides`
|
|
521
|
+
(e.g. `ENABLED_RULE`) apply correctly while **only the message text** falls back to the Core
|
|
522
|
+
default. Rules working + text not working ⇒ suspect `languageId`, not the override seeding.
|
|
523
|
+
- **⚠ DIAGNOSTIC TRAP: `audience.language` in the V2 response is NOT evidence the JWT carries a
|
|
524
|
+
language.** `V2.php` ~L2100 defaults it (`$this->language ?? 'en'`), so it reads `"en"` on tokens
|
|
525
|
+
with no language claim at all. **Decode the JWT instead.** Tell-tale of an API-auth token:
|
|
526
|
+
`id.client` has **no `user` node**.
|
|
504
527
|
- **A `SurfaceOverrides` row with `attribute='CONFIG'` carrying a RULE value is silently DEAD —
|
|
505
528
|
encode rules as `VISIBILITY_RULE`/`ENABLED_RULE`, never as `CONFIG`.** `_castOverride` routes the
|
|
506
529
|
`CONFIG` attribute into `$config`, **NOT** `$visibilityRule`/`$enabledRule`; a `CONFIG` row whose
|
|
@@ -583,6 +606,19 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
583
606
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
584
607
|
|
|
585
608
|
## Change history
|
|
609
|
+
- 2026-08-25 — Fixed a **whole-platform gap**: Surface message translations were inert for **every
|
|
610
|
+
API-authenticated request, for every client**. `_resolveLanguageId` returned `0` when the JWT carried
|
|
611
|
+
neither `id.client.languageId` nor `id.language`, and `0` makes `_loadMessages` skip the
|
|
612
|
+
`MessageTranslations` overlay entirely — silently serving `Core.Messages.defaultValue`. api2 embeds
|
|
613
|
+
`id.language` on **user auth only** (`V2.php` ~L1501; comment ~L1862), so all API-auth traffic hit it.
|
|
614
|
+
Fix: added `_Model_Client_Language::CODE_BASE = 'en'` and a final fallback to the client's
|
|
615
|
+
base-language row, making precedence `id.client.languageId` → `id.language` → client base language →
|
|
616
|
+
`0`; steps 1–2 still win so a future user preference is never overridden (language will become a user
|
|
617
|
+
preference — TOGa Commerce already sets it per user). Recorded the identifying symptom shape
|
|
618
|
+
(language-independent overrides like `ENABLED_RULE` work while **only** message text falls back) and
|
|
619
|
+
the diagnostic trap that **`audience.language` is not evidence of a language claim** — `V2.php` ~L2100
|
|
620
|
+
defaults it to `'en'`; decode the JWT, and note that an API-auth token's `id.client` has no `user`
|
|
621
|
+
node. (apeterson)
|
|
586
622
|
- 2026-08-24 — **Three corrections that SUPERSEDE prior claims in this doc.** (1) The 2026-08-21
|
|
587
623
|
"client `SurfaceOverride` seeding is NOT cluster-safe / no compliant workaround exists / needs a
|
|
588
624
|
cto-level decision" section is **RESOLVED**: Core seed files now assign **explicit team-reserved id
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: ["bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/.ebextensions/git.php
|
|
@@ -50,6 +50,60 @@ Cost of not knowing this: hours of debugging a feature that was, in fact, simply
|
|
|
50
50
|
- **`api2/.ebextensions/php_include_underscore.config`** — writes
|
|
51
51
|
`include_path = ".:/var/www/html/_underscore"`. See *the 1.0 library is unreachable* below.
|
|
52
52
|
|
|
53
|
+
## ⚠ TWO independent mechanisms deliver `_underscore` — BOTH must point at the right branch
|
|
54
|
+
|
|
55
|
+
Verified 2026-08-25 at the cost of a multi-hour investigation on `sandbox-client`. `_underscore`
|
|
56
|
+
reaches an environment by **two separate paths**, configured in two different places, and **either
|
|
57
|
+
one alone can silently serve the wrong branch**:
|
|
58
|
+
|
|
59
|
+
| # | Mechanism | Where the branch is configured | Visible in the repo? |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| 1 | The **CodePipeline `_underscore` source action** | **In AWS**, on the pipeline's source action (its own `BranchName` / branch trigger) — *separate from* the api2 environment it deploys to | **No** — nothing in the repo reveals it |
|
|
62
|
+
| 2 | The `.ebextensions/git.php` **prebuild clone** | `git.<env>.json`'s `branch` key, else `'_' . strtolower(ENVIRONMENT)` | Yes |
|
|
63
|
+
|
|
64
|
+
Checking only #2 (which is the one you can `grep`) is the trap: it can be perfectly correct while
|
|
65
|
+
#1 quietly feeds the environment a different branch.
|
|
66
|
+
|
|
67
|
+
**Concrete incident (2026-08-25).** The `ApiSandboxClient` environment ran `_underscore` from
|
|
68
|
+
**`_production`** because the pipeline's `_underscore` **source action's branch trigger** was set to
|
|
69
|
+
`_production` — even though `git.sandbox-client.json` was correct (no `branch` key). This was
|
|
70
|
+
invisible because **every developer-facing signal said otherwise**:
|
|
71
|
+
|
|
72
|
+
- the pipeline UI displayed the `_sandbox-client` merge commit (`2b523788`);
|
|
73
|
+
- the pipeline reported **"All actions succeeded"**;
|
|
74
|
+
- the clone deletes `.git`, so the instance carries **no commit id** to contradict it.
|
|
75
|
+
|
|
76
|
+
Two unrelated "data bugs" were really one stale box: the Quad approval-email `str_replace()` 500
|
|
77
|
+
(needs `2b0c1470`, Aug 18) and the Quad approve-tooltip falling back to Core defaults (needs
|
|
78
|
+
`3d92f973`, Jul 21). `git merge-base --is-ancestor` proved **neither commit is on `_production`** —
|
|
79
|
+
which is what turned a code hunt into a deploy fix.
|
|
80
|
+
|
|
81
|
+
**Rule: when a merged fix isn't live, verify the pipeline's `_underscore` source action branch in
|
|
82
|
+
AWS *as well as* `git.<env>.json`.** "All actions succeeded" and a correct commit id in the pipeline
|
|
83
|
+
UI are **not** evidence the right branch was deployed.
|
|
84
|
+
|
|
85
|
+
### Diagnostic: a stack trace's LINE NUMBERS objectively pin which version of a file is running
|
|
86
|
+
|
|
87
|
+
This is the technique that broke the 2026-08-25 deadlock, and it works with no server access.
|
|
88
|
+
Take the reported line numbers and compare them against the same file at each **candidate commit**
|
|
89
|
+
(`git show <commit>:<path>`). Only one version matches.
|
|
90
|
+
|
|
91
|
+
Worked example — the reported trace (`str_replace` at line **82**, `dispatch` at **30**,
|
|
92
|
+
`setSubject` at **73**) was an exact match for the **pre-fix** `EmailTemplate.php`; the fixed file
|
|
93
|
+
has those same calls at **115 / 41 / 84**. Proof the box was stale, not the code wrong.
|
|
94
|
+
|
|
95
|
+
Companion facts from the same investigation:
|
|
96
|
+
|
|
97
|
+
- **Runtime paths in traces are `/var/app/current/...`** — *not* the `/var/www/html/_underscore`
|
|
98
|
+
`include_path` entry. Don't dismiss a trace because the path looks unfamiliar.
|
|
99
|
+
- **"My coworker sees the code on branch X" ≠ "the server runs branch X."** A passing `git log` or
|
|
100
|
+
a clean local checkout says nothing about the instance. Keep the two claims separate.
|
|
101
|
+
- **`git merge-base --is-ancestor <fix-commit> <branch>`** is the cheap, objective test for "is this
|
|
102
|
+
fix on that branch?" — run it before theorizing about data.
|
|
103
|
+
- **`git ls-remote` cheaply validates the committed deploy PAT.** One command eliminated the
|
|
104
|
+
"expired token broke the clone" hypothesis. (See the PAT security gotcha below — verify the token
|
|
105
|
+
*works*, never print or copy its value.)
|
|
106
|
+
|
|
53
107
|
## How to determine what a tier is actually running
|
|
54
108
|
|
|
55
109
|
1. **Read `ENVIRONMENT` on the instance** — `/opt/elasticbeanstalk/bin/get-config environment -k
|
|
@@ -166,6 +220,18 @@ one.
|
|
|
166
220
|
|
|
167
221
|
## Change history
|
|
168
222
|
|
|
223
|
+
- 2026-08-25 — Recorded the **second delivery mechanism**: besides `.ebextensions/git.php`, the
|
|
224
|
+
**CodePipeline `_underscore` source action has its own branch trigger configured in AWS**, and it
|
|
225
|
+
can serve the wrong branch while `git.<env>.json` is perfectly correct. Live case: `ApiSandboxClient`
|
|
226
|
+
ran `_production` because that source action was pointed at `_production`, while the pipeline UI
|
|
227
|
+
showed the `_sandbox-client` merge commit `2b523788` and reported "All actions succeeded" — and the
|
|
228
|
+
clone deletes `.git`, so the instance has no commit id to contradict it. Two apparently unrelated
|
|
229
|
+
Quad bugs (the approval-email `str_replace()` 500 needing `2b0c1470`; the approve-tooltip Core-default
|
|
230
|
+
fallback needing `3d92f973`) were one stale box, proven by `git merge-base --is-ancestor`. Added the
|
|
231
|
+
**stack-trace-line-number** technique for objectively pinning which version of a file is running
|
|
232
|
+
(82/30/73 = pre-fix vs 115/41/84 = fixed), plus: runtime paths are `/var/app/current/...` not the
|
|
233
|
+
`include_path`; "a coworker sees it on branch X" is not "the server runs branch X"; and `git ls-remote`
|
|
234
|
+
cheaply disproves an expired-PAT hypothesis. (apeterson)
|
|
169
235
|
- 2026-08-24 — Recorded the **inverse** of the branch-pin trap: an **absent** `"branch"` key in
|
|
170
236
|
`git.*.json` is **correct and intentional** — `git.php` derives `_` + `strtolower(ENVIRONMENT)`
|
|
171
237
|
when the key is missing, and `git.sandbox-client.json`/`git.json` rely on that; do not "restore"
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [mhammontree, tcox, jcardinal, ajean, bala]
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [mhammontree, tcox, jcardinal, ajean, bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- api2/Component/Api/V2/Response/Response.php
|
|
@@ -73,6 +73,14 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
73
73
|
3. **EV-6 vs EZ-1** are the two halves of exposing a scripted API: EV-6 = no `Core.RecordScripts`
|
|
74
74
|
route (the segment falls through to record lookup); EZ-1 = the route exists but there's no
|
|
75
75
|
`AclRecordScripts` dispatch grant for the caller's role.
|
|
76
|
+
4. **A MALFORMED bearer token returns EO-1 500, not EN-3 401.** In `V2.php` (~line 1840) the token
|
|
77
|
+
header/payload segments are `base64`-decoded and `json_decode`d; a malformed token decodes to
|
|
78
|
+
`null`, and `property_exists()` on `null` throws a **`TypeError`** — which (per the escalation
|
|
79
|
+
below) surfaces as an **EO-1 500**. So an obviously-invalid token looks like a server fault
|
|
80
|
+
instead of an auth failure. The same code at ~line 826 already had the correct `is_object()`
|
|
81
|
+
guard; adding it to both header and payload restores **EN-3 401**. ⚠ As of 2026-08-25 this guard
|
|
82
|
+
is **written but stashed/uncommitted** — expect the 500 in any deployed environment. Relates to
|
|
83
|
+
the 2026-08-21 cross-client stale-token finding.
|
|
76
84
|
5. **EO-1 can be a masked PHP warning, not an operation error.** Sentry's `ErrorHandler` in
|
|
77
85
|
api2 **escalates PHP warnings/notices into thrown exceptions**, so a latent "read property on
|
|
78
86
|
bool" or "undefined variable" surfaces to the client as an **HTTP 500 (EO-1)** rather than a
|
|
@@ -273,6 +281,11 @@ Full mechanics:
|
|
|
273
281
|
- The response-envelope shape and code families: see [api2 architecture](../architecture.md).
|
|
274
282
|
|
|
275
283
|
## Change history
|
|
284
|
+
- 2026-08-25 — Added the diagnostic that a **malformed bearer token returns EO-1 500 instead of
|
|
285
|
+
EN-3 401**: the header/payload `json_decode` yields `null` and `property_exists()` throws a
|
|
286
|
+
`TypeError` (`V2.php` ~1840); the sibling check at ~826 already guarded with `is_object()`.
|
|
287
|
+
Guard written but **stashed/uncommitted**, so the 500 is still live in deployed environments.
|
|
288
|
+
(apeterson)
|
|
276
289
|
- 2026-08-20 — Added a **fourth `EO-1` case: an exception message that is literally `[]`.** It is a
|
|
277
290
|
message-array truthiness throw, not an operation error — `internalApiRequest`'s throw is guarded on
|
|
278
291
|
the **shared** `response->messages`, and `V2.php:2416`'s `json_encode(array_filter(...))` is the
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["jcardinal", "mhammontree", "bala"]
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: ["jcardinal", "mhammontree", "bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
13
13
|
- api2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
@@ -65,6 +65,39 @@ are actually changing before you merge anything:
|
|
|
65
65
|
- **`api2` has no deploy workflow.** Its only GitHub Action is the `TRUE-#####:` commit-message
|
|
66
66
|
linter, so **EB deploys are manual**. Do not wait for CI to ship a framework change.
|
|
67
67
|
|
|
68
|
+
### ⚠ The `_underscore` SOURCE ACTION has its own branch trigger — and a green pipeline hides a wrong one
|
|
69
|
+
|
|
70
|
+
The Source stage's **`_underscore` action carries its own `BranchName`/branch trigger, configured in
|
|
71
|
+
AWS and independent of the api2 environment it deploys to.** Nothing in the repo reveals it. This is a
|
|
72
|
+
**second**, separate way for an environment to run the wrong framework branch — the first being
|
|
73
|
+
`.ebextensions/git.<env>.json` (above). Both must be right.
|
|
74
|
+
|
|
75
|
+
**Incident 2026-08-25 (`ApiSandboxClient`, multi-hour investigation).** The `_underscore` source
|
|
76
|
+
action was pointed at **`_production`**, so the environment ran `_production` framework code, while:
|
|
77
|
+
|
|
78
|
+
- `git.sandbox-client.json` was **correct** (no `branch` key — the intended design), so the one
|
|
79
|
+
config you can `grep` exonerated itself;
|
|
80
|
+
- the pipeline UI displayed the **`_sandbox-client` merge commit `2b523788`**;
|
|
81
|
+
- the pipeline reported **"All actions succeeded"**;
|
|
82
|
+
- the prebuild clone deletes `.git`, so the instance carries **no commit id** to contradict any of it.
|
|
83
|
+
|
|
84
|
+
Two apparently unrelated live bugs (a Quad approval-email 500 and a Quad tooltip falling back to Core
|
|
85
|
+
defaults) were the **same** stale box; `git merge-base --is-ancestor <fix> _production` proved neither
|
|
86
|
+
fix was on the branch actually deployed.
|
|
87
|
+
|
|
88
|
+
**Triage rule:** a green pipeline showing the right commit proves the *source artifact* was fetched —
|
|
89
|
+
it does **not** prove which **branch** each source action watches. Read the source action config:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
aws codepipeline get-pipeline --name "$PIPELINE" --region "$REGION" \
|
|
93
|
+
--query 'pipeline.stages[?name==`Source`].actions[].{name:name,repo:configuration.FullRepositoryId,branch:configuration.BranchName}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then confirm the fix is actually on that branch with
|
|
97
|
+
`git merge-base --is-ancestor <fix-commit> origin/<branch>`. See
|
|
98
|
+
[ENVIRONMENT drives the `_underscore` branch](../features/environment-variable-drives-underscore-branch.md)
|
|
99
|
+
for the companion mechanism and the stack-trace-line-number technique for proving a box is stale.
|
|
100
|
+
|
|
68
101
|
### Debugging a beta env without Sentry — `Logs_<Client>.Api`
|
|
69
102
|
`Logs_<Client>.Api` is a **full server-side request log**: `queryString`, request/response payloads,
|
|
70
103
|
`direction` (`IN`/`OUT`), `instanceId`, and `executionTime`. It is the reliable way to see what a
|
|
@@ -200,6 +233,13 @@ aws codeconnections get-connection --connection-arn "<CONN_ARN>" --region "$REGI
|
|
|
200
233
|
```
|
|
201
234
|
|
|
202
235
|
## Change history
|
|
236
|
+
- 2026-08-25 — Documented that the Source stage's **`_underscore` action has its own AWS-side branch
|
|
237
|
+
trigger**, independent of `git.<env>.json` and invisible in the repo — a second way to silently run
|
|
238
|
+
the wrong framework branch. `ApiSandboxClient` ran `_production` from a mis-set source action while
|
|
239
|
+
`git.sandbox-client.json` was correct, the pipeline UI showed the `_sandbox-client` merge commit
|
|
240
|
+
`2b523788`, and the run reported "All actions succeeded"; the clone deletes `.git`, so the instance
|
|
241
|
+
had no commit id to contradict it. Added the `get-pipeline` query for reading each source action's
|
|
242
|
+
`BranchName` and the `git merge-base --is-ancestor` confirmation step. (apeterson)
|
|
203
243
|
- 2026-08-10 — Added three deploy-verification facts from the Elite sales-order push: (1) **staleness
|
|
204
244
|
is per-INSTANCE** — the worker2 web instance behind `worker.beta.togahub.com`
|
|
205
245
|
(`i-0262cfdc6cb97a3d2`) ran current code while the **SQS consumer** (`i-0be8ef2e550dec05b`, env
|
|
@@ -3,7 +3,7 @@
|
|
|
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
|
-
| [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-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.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 |
|
|
6
|
+
| [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 |
|
|
7
7
|
| [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/ |
|
|
8
8
|
| [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 |
|
|
9
9
|
| [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**. | |
|
|
@@ -6,12 +6,18 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
|
+
- dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql
|
|
13
|
+
- dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
14
|
+
- dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
15
|
+
- dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
16
|
+
- dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql
|
|
12
17
|
- dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql
|
|
13
18
|
- dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql
|
|
14
19
|
- dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql
|
|
20
|
+
- dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql
|
|
15
21
|
- dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql
|
|
16
22
|
- dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql
|
|
17
23
|
- dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql
|
|
@@ -476,6 +482,47 @@ Blocks assigned by `Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql`: **`
|
|
|
476
482
|
202–222**, **`Core.Surfaces` 41–43**, **`Core.SurfaceElements` 125–146**. A new Core surface seed
|
|
477
483
|
takes the next unused block and writes ids explicitly.
|
|
478
484
|
|
|
485
|
+
Blocks assigned by `Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql`: **`Core.Surfaces` 44**
|
|
486
|
+
(`sales-order-status-filter`, `type=FILTER_SET`), **`Core.SurfaceElements` 147–160** (one BADGE per
|
|
487
|
+
sales-order status; no `Core.Messages` rows — labels come from the `sales-order-status` vocabulary).
|
|
488
|
+
See [sales-order-status-filter-surface](../../_underscore/features/sales-order-status-filter-surface.md).
|
|
489
|
+
|
|
490
|
+
### 🚨 VERIFY the block is free, and never seed literal ids with `INSERT IGNORE`
|
|
491
|
+
|
|
492
|
+
**Incident, 2026-08-24 — a reserved block was quoted as "highest used" and read as "next free", and
|
|
493
|
+
`INSERT IGNORE` hid the resulting collision.** The first attempt at the status-filter seed took
|
|
494
|
+
`Surfaces` **43** and `SurfaceElements` **145–146+**. But 43 was already `sales-order-deny-action`
|
|
495
|
+
and 145–146 were its toast elements. Because the seed used `INSERT IGNORE`:
|
|
496
|
+
|
|
497
|
+
- the new surface row was **silently never created**;
|
|
498
|
+
- its 12 elements **attached themselves to the deny-action surface**;
|
|
499
|
+
- the per-client override rows then pointed at unrelated elements — NYCHH and Quad ended up hiding
|
|
500
|
+
that surface's **success/error toasts**.
|
|
501
|
+
|
|
502
|
+
Cleaned up locally **by uuid**, never by id range (an id range would have taken the legitimate
|
|
503
|
+
deny-action rows with it). Two durable rules follow:
|
|
504
|
+
|
|
505
|
+
1. **Verify a reserved block is actually free before writing the seed** — `SELECT MAX(id)` *and*
|
|
506
|
+
`SELECT id FROM … WHERE id BETWEEN <lo> AND <hi>`. Never take quoted ids on trust, and always
|
|
507
|
+
confirm whether the number you were handed means **"next free"** or **"highest used"**.
|
|
508
|
+
2. **Use plain `INSERT`, never `INSERT IGNORE`, for any row with a LITERAL id.** `IGNORE` converts a
|
|
509
|
+
primary-key collision — the one error that must stop the migration — into a silent mis-wiring.
|
|
510
|
+
Idempotency for literal-id seeds belongs in a `NOT EXISTS` guard on a *semantic* key, or in a
|
|
511
|
+
paired `UPDATE`, not in `IGNORE`.
|
|
512
|
+
|
|
513
|
+
This is the **same failure class** as the earlier Quad wrong-surface rollout and as the NYCHH stale
|
|
514
|
+
Core-id landmine below: a surface/element id that resolves to *something* is never self-evidently the
|
|
515
|
+
*right* something.
|
|
516
|
+
|
|
517
|
+
**⚠ Pre-reserved-block `Core.Messages` rows are plain `AUTO_INCREMENT` and DO drift per environment.**
|
|
518
|
+
The reserved-id discipline above starts at the 2026-08-21 reseed (`Messages` 202–222); **older**
|
|
519
|
+
`Core.Messages` rows were auto-assigned and are **not** one of the two team-maintained id tables, so an
|
|
520
|
+
id can differ between local, sandbox and prod. Verified 2026-08-25 on sandbox-client: `Core.Messages`
|
|
521
|
+
**23 / 24 / 25** = `salesOrder.action.approve` / `.tip` / `.disabled`, and `Client_Quad.Languages` has
|
|
522
|
+
exactly one row, **id 1 = `en`**. In that instance **id 25 matched on both local and sandbox, so drift
|
|
523
|
+
was NOT the cause of the bug being chased** — worth recording so the next person neither assumes drift
|
|
524
|
+
nor rules it out without checking.
|
|
525
|
+
|
|
479
526
|
**Client override house style** (see the 2026-08-24 prod-port files for Compass / Compass Canada /
|
|
480
527
|
Quad): Core ids and `messageId`s inlined as literals; only **client-local** ids resolved at runtime
|
|
481
528
|
(e.g. `SET @adminRoleId = (SELECT id FROM Roles WHERE …)`, deliberately a lookup because role ids
|
|
@@ -483,6 +530,24 @@ differ per client *and* per environment — Compass Admin = 4, Compass Canada Ad
|
|
|
483
530
|
**every statement guarded on `@adminRoleId IS NOT NULL`** so a lookup miss no-ops instead of writing
|
|
484
531
|
garbage.
|
|
485
532
|
|
|
533
|
+
### Worked before/after: `Client_Quad` approve-tooltip — the same override, wrong then right
|
|
534
|
+
|
|
535
|
+
Two real files on the same subject, one violating the cluster-isolation HARD RULE and one compliant.
|
|
536
|
+
Read them side by side; this is the clearest example in the repo of what the rule actually costs.
|
|
537
|
+
|
|
538
|
+
| | `Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql` ❌ | `Client_Quad/2026-08-24a - ProdPortApprovePoNumberEnabledRule.sql` ✅ |
|
|
539
|
+
|---|---|---|
|
|
540
|
+
| Cross-cluster | Joins **`Core.Messages` to `Client_Quad.Languages`** — a live violation | Core ids **inlined as literals**; Core-free |
|
|
541
|
+
| Where it runs | **Local only** (all-in-one endpoint). Can **never** run on prod or sandbox — separate clusters | Runs anywhere |
|
|
542
|
+
| UUIDs | MySQL **`UUID()`** (v1) — violates the v4 rule | Pre-generated **v4 UUID literals** |
|
|
543
|
+
| Qualification | Qualifies its own tables | Unqualified (client-DB-relative) |
|
|
544
|
+
|
|
545
|
+
**`07-21b` is a known-superseded landmine, deliberately left in place.** It is superseded by
|
|
546
|
+
`2026-08-24a` and the developer has **explicitly deferred acting on it** — it is recorded here so the
|
|
547
|
+
next person recognizes it rather than "discovering" it, and does **not** open work on it. The
|
|
548
|
+
takeaway is the pattern, not a cleanup task: a green local run proves nothing about cluster safety,
|
|
549
|
+
and a `Client_*` file that names `Core.*` is broken **even though it works on your machine**.
|
|
550
|
+
|
|
486
551
|
### Ship-together dependency: the approvals GATE and the Approve step-two RULE
|
|
487
552
|
The approvals-gate marker and the Approve `ENABLED_RULE` **must ship in the same release**. The
|
|
488
553
|
`stepTwoAssigned` predicate reads `order.currentStage` / `order.stages`, which are only fetched when
|
|
@@ -666,6 +731,27 @@ rule resumes.
|
|
|
666
731
|
override is added). **Open follow-up.**
|
|
667
732
|
|
|
668
733
|
## Change history
|
|
734
|
+
- 2026-08-25 — Added a **worked before/after cluster-isolation example** on a real client migration:
|
|
735
|
+
`Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql` joins **`Core.Messages` to
|
|
736
|
+
`Client_Quad.Languages`**, so it runs locally (all-in-one endpoint) and can **never** run on prod or
|
|
737
|
+
sandbox — plus MySQL `UUID()` (v1) and self-qualified tables — versus the compliant
|
|
738
|
+
`Client_Quad/2026-08-24a - ProdPortApprovePoNumberEnabledRule.sql` (Core ids inlined, unqualified,
|
|
739
|
+
v4 UUID literals). `07-21b` is recorded as a **known-superseded landmine the developer has
|
|
740
|
+
explicitly deferred** — do not open work on it. Also recorded that **pre-reserved-block
|
|
741
|
+
`Core.Messages` rows are plain `AUTO_INCREMENT` and do drift per environment** (they are not one of
|
|
742
|
+
the two team-maintained id tables); verified on sandbox-client that Messages **23/24/25** =
|
|
743
|
+
`salesOrder.action.approve`/`.tip`/`.disabled` and `Client_Quad.Languages` holds one row, id 1 =
|
|
744
|
+
`en`, and that id 25 matched local **and** sandbox — so drift was **not** the cause in that case.
|
|
745
|
+
(apeterson)
|
|
746
|
+
- 2026-08-25 — Reserved **`Core.Surfaces` 44** + **`SurfaceElements` 147–160** for the
|
|
747
|
+
`sales-order-status-filter` `FILTER_SET` seed (`Core/2026-08-24b`; no Messages rows — labels come
|
|
748
|
+
from the `sales-order-status` vocabulary). Recorded the **reserved-block collision incident**: the
|
|
749
|
+
block was quoted as "highest used" but read as "next free", so the seed targeted the live
|
|
750
|
+
`sales-order-deny-action` surface, and `INSERT IGNORE` **silently** dropped the surface row while
|
|
751
|
+
attaching 12 elements to the wrong surface (NYCHH/Quad then hid that surface's toasts). Two rules
|
|
752
|
+
added — verify a block is free (`SELECT MAX(id)` + range check, and confirm next-free vs
|
|
753
|
+
highest-used) and **never `INSERT IGNORE` a row with a literal id**; clean up by uuid, never by id
|
|
754
|
+
range. (apeterson)
|
|
669
755
|
- 2026-08-24 — Hunted the migrations missed by the 2026-08-21 hand-picked prod push and built
|
|
670
756
|
forward-only prod-safe ports. **Settled the prod-safe authoring pattern** (new section): Core seeds
|
|
671
757
|
assign **explicit reserved id literals** (`Messages` 202–222, `Surfaces` 41–43, `SurfaceElements`
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
| [BaseInput (react-hook-form field factory)](features/base-input.md) | `BaseInput` is a **form-field factory** driven by react-hook-form. | toga-blox/src/components/BaseInput/BaseInput.tsx, toga-blox/src/components/BaseInput/BaseInput.types.ts, toga-blox/src/components/BaseInput/BaseInput.module.css, toga-blox/src/components/BaseInput/components |
|
|
10
10
|
| [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
|
|
11
11
|
| [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
|
|
12
|
-
| [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
|
|
12
|
+
| [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
|
|
13
13
|
| [Talos AI-Assistant Component (launcher + slide-out chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a slide-out chat panel (`TalosP | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/index.ts |
|
|
14
14
|
| [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
|
|
@@ -6,9 +6,11 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
|
+
- toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts
|
|
13
|
+
- toga-blox/src/api/types.ts
|
|
12
14
|
- toga-blox/src/components/Table/index.ts
|
|
13
15
|
- toga-blox/src/components/Table/types.ts
|
|
14
16
|
- toga-blox/src/components/Table/utils/buildTanstackColumns.tsx
|
|
@@ -123,7 +125,7 @@ Per-column header controls, each a popover trigger with `data-active` / `data-op
|
|
|
123
125
|
| `useInfiniteTableData` | `useInfiniteQuery` + IntersectionObserver on a `sentinelRef` |
|
|
124
126
|
| `useTableInfiniteScroll` | virtualizer-based "near end" trigger (lower-level alternative) |
|
|
125
127
|
| `useTableEdit` | dirty-row tracking, dot-notation patch, `handleSave` calls `mutationFn` and updates the query cache, clears dirty + exits edit mode on success |
|
|
126
|
-
| `useFetchTablePageMeta` | fetches table meta
|
|
128
|
+
| `useFetchTablePageMeta` | fetches table meta; fans out a filter-option query **only** for fields the server did not already resolve |
|
|
127
129
|
| `useAssignTableFieldLabels` | merges page-level labels into field defs |
|
|
128
130
|
| `useTableFieldFilterOptions` | filter options for a single field |
|
|
129
131
|
|
|
@@ -136,6 +138,19 @@ Per-column header controls, each a popover trigger with `data-active` / `data-op
|
|
|
136
138
|
(modal: fixed heights, virtualization disabled). Design tokens are `--primaryTable-*` in
|
|
137
139
|
`toga.module.css`.
|
|
138
140
|
|
|
141
|
+
## Server-resolved filter options (`filterOptions` in table meta)
|
|
142
|
+
|
|
143
|
+
`useFetchTablePageMeta` **prefers server-supplied `filterOptions`** and skips the per-field
|
|
144
|
+
`recordRoute` fetch for those fields. A single `needsFetchedOptions` predicate drives **both** the
|
|
145
|
+
query list and the index lookup, so the two can never skew (they previously could). `types.ts`:
|
|
146
|
+
`filterOptions[].uuid` is optional, and `surfaceSlug` is declared on `TableViewField`.
|
|
147
|
+
|
|
148
|
+
This exists because an unscoped `GET /{recordRoute}` returns *every* row of a lookup table, which is
|
|
149
|
+
wrong whenever the offered set is per-client. The backend now resolves the set through the Surface
|
|
150
|
+
layer and emits it — see
|
|
151
|
+
[sales-order-status-filter-surface](../../_underscore/features/sales-order-status-filter-surface.md).
|
|
152
|
+
**The blox side never derives a surface slug by naming convention; it renders what meta gives it.**
|
|
153
|
+
|
|
139
154
|
## Gotchas
|
|
140
155
|
|
|
141
156
|
- `MIN_COLUMN_WIDTH = 120` is the shrink floor only for columns with **no** explicit width.
|
|
@@ -183,6 +198,12 @@ Per-column header controls, each a popover trigger with `data-active` / `data-op
|
|
|
183
198
|
requires `npm run build` before it takes effect at runtime.
|
|
184
199
|
|
|
185
200
|
## Change history
|
|
201
|
+
- 2026-08-25 — `useFetchTablePageMeta` now prefers **server-supplied `filterOptions`** and skips the
|
|
202
|
+
`recordRoute` fetch for those fields (query list + index lookup share one `needsFetchedOptions`
|
|
203
|
+
predicate so they can't skew); `filterOptions[].uuid` made optional and `surfaceSlug` declared on
|
|
204
|
+
`TableViewField`. Implements a TODO the file already carried; first consumer is the per-client
|
|
205
|
+
sales-order status filter. Reminder: blox `dist/` is gitignored — `npm run build` before consumers
|
|
206
|
+
see it. (commit `dded92a6`) (apeterson)
|
|
186
207
|
- 2026-08-17 — Built multi-chip text column filter: each `HeaderFilterSearch` search is now its
|
|
187
208
|
own `TextFilterTag {value, mode}`, the mode dropdown sets the mode for the next chip, existing
|
|
188
209
|
chips keep their mode, and all chips AND together. Persisted as a single readable URL param
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 62 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 55 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -6,6 +6,7 @@ apps:
|
|
|
6
6
|
- api2
|
|
7
7
|
- toga2-supply
|
|
8
8
|
- toga25-supply
|
|
9
|
+
- toga-blox
|
|
9
10
|
- toga2-commerce
|
|
10
11
|
- worker2
|
|
11
12
|
- worker
|
|
@@ -14,7 +15,7 @@ project: _Underscore
|
|
|
14
15
|
client: quad
|
|
15
16
|
type: profile
|
|
16
17
|
status: active
|
|
17
|
-
updated: 2026-08-
|
|
18
|
+
updated: 2026-08-25
|
|
18
19
|
owners: ["jcardinal", "bala", "apeterson", "ajean"]
|
|
19
20
|
files: []
|
|
20
21
|
related:
|
package/package.json
CHANGED