toga-ai 1.0.242 → 1.0.244
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +1 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/netsuite-supporting-record-webhook-importer.md +135 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/sessions/2026-06-30-surface-layer-jcardinal.md +74 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
|
|
16
16
|
| [NetSuite → TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
17
17
|
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
18
|
+
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
18
19
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
19
20
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
20
21
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NetSuite Supporting-Record Webhook Importer (the reusable recipe)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-30
|
|
10
|
+
owners: ["dfranks"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Netsuite/Employee.php
|
|
13
|
+
- worker2/Worker/Netsuite/Account.php
|
|
14
|
+
- worker2/Worker/Netsuite/Classification.php
|
|
15
|
+
- worker2/Worker/Netsuite/Customer.php
|
|
16
|
+
- worker2/Worker/Netsuite/Item.php
|
|
17
|
+
- worker2/Worker/Netsuite.php
|
|
18
|
+
- _underscore/Model/Forecast/Employee.php
|
|
19
|
+
- _underscore/Model/Forecast/Account.php
|
|
20
|
+
- _underscore/Model/Forecast/Classification.php
|
|
21
|
+
- _underscore/Component/Forecast/Db/Db.php
|
|
22
|
+
- test/@dave/test_employee_lifecycle.php
|
|
23
|
+
- test/@dave/test_account_lifecycle.php
|
|
24
|
+
- test/@dave/test_classification_lifecycle.php
|
|
25
|
+
- test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
|
|
26
|
+
- worker/crons/toga2/forecast2/import_supporting_records.php
|
|
27
|
+
related:
|
|
28
|
+
- ./netsuite-opportunity-sync.md
|
|
29
|
+
- ./netsuite-salesorder-open-orders-sync.md
|
|
30
|
+
- ../../_underscore/features/forecast-sale-import.md
|
|
31
|
+
- ../../_underscore/features/netsuite-rest-client.md
|
|
32
|
+
- ../architecture.md
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Summary
|
|
36
|
+
|
|
37
|
+
A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record*
|
|
38
|
+
importer (the lookup/dimension tables behind Forecast2 — Employees, Accounts, Classifications,
|
|
39
|
+
Customers, Items) into a **real-time, webhook-driven, single-record upsert**. A NetSuite
|
|
40
|
+
create/edit/delete arrives at `webhook.togahub.com/netsuite`, the `_Worker_NetSuite::Webhook`
|
|
41
|
+
router dispatches `Netsuite/<Record>/<verb>`, and a thin per-record handler upserts (or
|
|
42
|
+
guarded-deletes) one row in the `Forecast` schema.
|
|
43
|
+
|
|
44
|
+
This is the supporting-record sibling of the transaction-family importers — the
|
|
45
|
+
[Opportunity sync](./netsuite-opportunity-sync.md) (header+children), the
|
|
46
|
+
[Open-Orders sync](./netsuite-salesorder-open-orders-sync.md), and the
|
|
47
|
+
[Forecast.Sales import engine](../../_underscore/features/forecast-sale-import.md). After
|
|
48
|
+
Items + Customers established it and Employees/Accounts/Classifications repeated it verbatim, the
|
|
49
|
+
**recipe is the durable knowledge**: the remaining siblings are mechanical. This doc captures the
|
|
50
|
+
recipe and its two recurring variants so a new one is a fill-in-the-blanks job, not a fresh design.
|
|
51
|
+
|
|
52
|
+
## The recipe (per supporting record)
|
|
53
|
+
|
|
54
|
+
1. **Handler** `worker2/Worker/Netsuite/<Record>.php`:
|
|
55
|
+
`abstract class _Worker_Netsuite_<Record> implements _Interface_Static_Webhook_Netsuite`.
|
|
56
|
+
The shared `_Worker_NetSuite::Webhook` router PascalCases the inbound `recordType` to find it —
|
|
57
|
+
**no router edit is needed.** Verbs:
|
|
58
|
+
- `post` / `put` → `syncRecord()` then `_Database::transactionCommit(self::DB_FORECAST)`
|
|
59
|
+
(the lazy-transaction discipline — `syncRecord` itself never commits; the caller owns the txn).
|
|
60
|
+
- `delete` → run the **referenced-guard** first, and commit **only if it actually deletes**.
|
|
61
|
+
2. **`syncRecord()`** (no commit): fetch the record via SuiteQL single-id form —
|
|
62
|
+
`_Component_Api_Netsuite::send('POST', QUERY_SUITEQL.'?limit=1', ['q'=>$sql], ['Prefer'=>'transient'])`
|
|
63
|
+
— then **upsert keyed on `netsuiteInternalId`**: `load()` the existing row, then **change-detect
|
|
64
|
+
with string-cast compares** (`_Number::nullableIntChanged` for nullable ints) before `save()`.
|
|
65
|
+
The guard is mandatory because **`save()` does NOT diff** — without the change-detect it would
|
|
66
|
+
re-write (and could re-touch) every field on every webhook.
|
|
67
|
+
3. **Model** `_underscore/Model/Forecast/<Record>.php extends _Model_True`, declaring **ONLY the
|
|
68
|
+
NetSuite-sourced columns.** Deliberately **omit externally-managed columns** so a webhook upsert's
|
|
69
|
+
`save()` can never null them — e.g. Employees' `teamId` / `departmentId` / `role` / `urlHeadshot`
|
|
70
|
+
are Power-BI/HR-managed and are left off the model.
|
|
71
|
+
4. **Delete guard** = a `_Component_Forecast_Db::<record>IsReferenced(int): bool` helper using
|
|
72
|
+
`SELECT EXISTS( … UNION ALL … )` across **every inbound RESTRICT FK**. Referenced → return a
|
|
73
|
+
*retained-message string* (do **NOT** throw, do **NOT** blast dependent rows); unreferenced →
|
|
74
|
+
hard-delete. All these helpers are added to the **same** `_underscore/Component/Forecast/Db/Db.php`
|
|
75
|
+
right after `lookupId()` (so independent sibling-ticket PRs branched from `_production` conflict
|
|
76
|
+
*trivially* on merge — keep every helper method).
|
|
77
|
+
5. **Enqueuer:** add `'<record>':'<record>'` to `RECORD_TYPE_MAP` in
|
|
78
|
+
`test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js` (account/customer/employee/
|
|
79
|
+
classification entries already exist).
|
|
80
|
+
6. **e2e harness** `test/@dave/test_<record>_lifecycle.php`: clone an existing one. It drives NetSuite
|
|
81
|
+
REST (check/create/get/update/delete) + the in-process worker verbs (post/put/verb-delete) + a local
|
|
82
|
+
dump of the resulting `Forecast` rows. Boots via chdir `worker2` + `require index.php`.
|
|
83
|
+
7. **Cron:** leave the legacy `SHOULD_SYNC_<X>` daily pull **ON** as a backstop (and, for self-ref
|
|
84
|
+
records, the parent/supervisor backfill). It shares `import_supporting_records.php` with the other
|
|
85
|
+
sections, so **never disable the wrapper.**
|
|
86
|
+
|
|
87
|
+
## Two recurring variants
|
|
88
|
+
|
|
89
|
+
- **Self-referential parent/supervisor FK** (`Employees.supervisorEmployeeId`,
|
|
90
|
+
`Classifications.parentClassificationId`) = **RESOLVE-OR-NULL, never recursive upsert.** Resolve the
|
|
91
|
+
parent's local id by its NetSuite internalId via `lookupId`; if absent store **NULL** (the daily cron
|
|
92
|
+
backfills the link later). Inserting a non-existent local parent id would violate or dangle the FK.
|
|
93
|
+
The delete guard for a self-ref record must **also** check for child rows
|
|
94
|
+
(`WHERE parent…Id = id`) to avoid orphaning the hierarchy.
|
|
95
|
+
- **Type/enum mapping** (Accounts): replicate the authoritative shim's `accttype`→canonical map
|
|
96
|
+
(`App_Api_Netsuite_Rest::listAccounts`, `library/app/api/netsuite/rest.php:218-237`) as a handler
|
|
97
|
+
const `ACCT_TYPE_MAP` so stored values stay format-consistent (`_income`, `_accountsReceivable`, …);
|
|
98
|
+
an unmapped value passes through raw. **Keep the const in sync with the shim.**
|
|
99
|
+
- **Name from a hierarchical NetSuite `name`/`fullname`** = the **leaf segment**
|
|
100
|
+
(`explode(' : ')` + `array_pop`), matching the SOAP/cron/Item resolver.
|
|
101
|
+
|
|
102
|
+
## Gotchas / known issues
|
|
103
|
+
|
|
104
|
+
- **SuiteQL over the REST client has NO server-side placeholder binding.** `_Component_Api_Netsuite::send`
|
|
105
|
+
passes the query string as-is, so the **`(int)` cast on an already-int id is the injection control** —
|
|
106
|
+
the same codebase-wide idiom used by `lookupId()` and the Item/Opportunity/Employee handlers. A
|
|
107
|
+
php-reviewer flags it **CRITICAL** on a fresh read; it is **acceptable here** because the id is a
|
|
108
|
+
local PK from `load()`, never user input. Document the cast-only mitigation with a one-line comment.
|
|
109
|
+
- **SuiteQL returns parent/supervisor reference ids as numeric STRINGS**, so the private resolve-helper
|
|
110
|
+
parameter must stay **untyped** (not `int|null`) or it `TypeError`s.
|
|
111
|
+
- **The supporting-record tables already carry a UNIQUE index on `netsuiteInternalId`** (Employees,
|
|
112
|
+
Accounts, Classifications, Customers, Items), so the upsert's `load()` resolves exactly one row — no
|
|
113
|
+
duplicate-amplification (unlike the [opportunity `load()` footgun](./netsuite-opportunity-sync.md)),
|
|
114
|
+
no migration needed.
|
|
115
|
+
- **`save()` does not diff** — see recipe step 2: the change-detect guard before `save()` is what makes
|
|
116
|
+
a no-change webhook a true no-op.
|
|
117
|
+
|
|
118
|
+
## Change history
|
|
119
|
+
|
|
120
|
+
- 2026-06-30 — Captured the supporting-record webhook-importer recipe and its two variants
|
|
121
|
+
(self-ref RESOLVE-OR-NULL parent FK + child-orphan delete-guard; Accounts `accttype` enum map; leaf
|
|
122
|
+
segment naming) after the third+ application (TRUE-79870 Employees, TRUE-79871 Accounts, TRUE-79872
|
|
123
|
+
Classifications) repeated the Items/Customers pattern verbatim. Recorded the model-omits-externally-
|
|
124
|
+
managed-columns rule, the `_Component_Forecast_Db::<record>IsReferenced` delete-guard contract
|
|
125
|
+
(retained-message string, never throw/blast), the trusted-int SuiteQL idiom (no REST placeholder
|
|
126
|
+
binding; cast-only mitigation), the SuiteQL-returns-string reference-id `TypeError` trap, and the
|
|
127
|
+
leave-the-legacy-cron-on-as-backstop boundary. (dfranks)
|
|
128
|
+
|
|
129
|
+
## Related docs
|
|
130
|
+
|
|
131
|
+
- [NetSuite → TOGA Opportunity Sync](./netsuite-opportunity-sync.md) — header+children transaction sibling.
|
|
132
|
+
- [NetSuite → Forecast Open-Orders Sync](./netsuite-salesorder-open-orders-sync.md).
|
|
133
|
+
- [Forecast.Sales NetSuite import engine](../../_underscore/features/forecast-sale-import.md) — the
|
|
134
|
+
revenue-transaction family + the shared `_Component_Forecast_Db` low-level helpers.
|
|
135
|
+
- [Worker (worker2) Architecture](../architecture.md).
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
## 2.0 framework
|
|
18
18
|
|
|
19
19
|
- **_underscore** (_Underscore) _(framework core)_ — 19 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
20
|
-
- **worker2** (Worker) —
|
|
20
|
+
- **worker2** (Worker) — 23 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
21
21
|
- **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
22
22
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
23
23
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: surface-layer
|
|
4
|
+
title: Surface grouped-meta + SalesOrders section migration (Compass/CompassCanada/Quad)
|
|
5
|
+
author: jcardinal
|
|
6
|
+
repos: [_underscore, dbchanges2, toga25-supply]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: shared
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-06-30
|
|
11
|
+
updated: 2026-06-30
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: surface-layer
|
|
15
|
+
**Date:** 2026-06-30
|
|
16
|
+
**Project/Repo:** _underscore / dbchanges2 / toga25-supply (2.0)
|
|
17
|
+
**Task:** Build a grouped surface-meta resolver and migrate the toga25-supply SalesOrders record-modal sections onto the Surface UI presentation layer for the seeded clients (Compass, CompassCanada, Quad), keeping non-migrated clients on JSON.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
- **`metaGroup` grouped resolver** — `_Model_Core_Surface::metaGroup(&$api, string $slugs): object` at `GET /v2/surfaces/meta-group?slugs=a,b,c`. Splits/dedupes the slug list (cap `META_GROUP_MAX_SLUGS=25`, logs truncation), calls the existing `resolve()` per slug (shares its per-slug file cache), returns `{group, surfaces:{slug=>bundle|null}}`. `php -l` clean. Verified live on dev-sandbox (200 after the ACL grant).
|
|
23
|
+
- **Resolver message-resolution fixes** (`Surface.php` `_buildBundle`/`_resolveElement`): remap element message slots id→messageKey before return (labels previously fell back to the element key); collect `disabledTooltipMessageId` + emit `disabledTooltip`; set `surface.titleMessageKey`; merge the 2nd `_loadMessages` return into `$messageKeyById`; log dangling message ids; `resolve()` now `: object`. These were latent because SECTION/BUTTON_BAR rendering was never shipped.
|
|
24
|
+
- **FE grouped fetch + adapter**: `useFetchSurfaceMetaGroup(slugs[])` (order-insensitive key, staleTime Infinity, `extractGroup` by `{surfaces}` fingerprint) and `surfaceBundleToTenantFields()` adapter → existing `TenantFields` shape, so `SalesOrderSummaryGrid` renderers + `getDetailSections` are untouched.
|
|
25
|
+
- **Migrated-client roster gate**: `SALES_ORDER_SURFACE_MIGRATED_CLIENTS=[COMPASS,COMPASSCANADA,QUAD]` in the view-model; only roster clients fetch+resolve Surface sections (merge = JSON-base + Surface-wins), everyone else stays JSON.
|
|
26
|
+
- **DB seeds (applied on dev-sandbox, working)**: Core `2026-06-30a` (meta-group RecordScript + `order-recurring` surface + 5 display-toggle marker surfaces); Client `2026-06-30a` meta-group AclRecordScripts grant; Compass/CompassCanada/Quad `2026-06-30a` SurfaceOverrides for section deltas.
|
|
27
|
+
- **403 EZ-1 on meta-group fixed** by applying the Client AclRecordScripts dispatch grant per client DB. Verified via toga-db: `meta` script id 25 → roles 1,3,4; `meta-group` id 27 had no grant until applied.
|
|
28
|
+
- **JSON cleanup**: removed the migrated section blocks from COMPASS/COMPASSCANADA/QUAD `orderViewFields.json` (kept salesOrderDetailsConfig/itemFulfillmentClickRule/recordActionFields/recordApprovals). DEFAULT left intact.
|
|
29
|
+
- **/capture PUSHED** — 4 surface feature docs + 3 client profiles updated on team `_main` (commits f732160 + fc8de41).
|
|
30
|
+
|
|
31
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
32
|
+
- **Client-DB seeds resolving Core ids via cross-DB subselect** (`Core.RecordScripts`/`Core.Surfaces`/`Core.SurfaceElements` from a `Client_*` file). Works on the dev-sandbox all-in-one instance but **WILL FAIL in production** — Core is a separate MySQL cluster there, and cross-DB queries require one instance. This is the current id-agnostic approach in all 4 Client seeds; it is the prod blocker (see Blockers).
|
|
33
|
+
- **Parent `parentSurfaceId` GROUP-row design for grouped meta** — rejected by CTO review: adds Core schema state + a new TYPE, must be seeded per-client per-env for a grouping the FE already knows statically, and reintroduces id drift. The slug-list `metaGroup` is schema-stateless.
|
|
34
|
+
- **Surface-only cutover for all clients** (no roster gate) — would regress non-seeded clients (NYCHH/Prudential/SPGlobal/DEFAULT) because the Core section surfaces are SHARED and seeded from Compass's shape; they'd inherit it.
|
|
35
|
+
- **Overriding a section's visibility via the Surface row's own `isVisible`** — the resolver cascade (`_loadOverrides`) only overrides ELEMENTS, never a surface's `isVisible`. Hence each display-toggle surface carries one `sectionVisibility` marker element that the overrides target.
|
|
36
|
+
- **Removing MANAGER/Quad summary shipping/tax tooltips via override** — `_castOverride` treats `TOOLTIP_MESSAGE` as numeric-only, so it can't null a tooltip. Cosmetic; deferred.
|
|
37
|
+
- **Seeding `elementType` for Quad** — confirmed dead config (no `.ts/.tsx` reads it); needs no migration.
|
|
38
|
+
|
|
39
|
+
## Not tried yet (candidates for next session)
|
|
40
|
+
- **Prod cross-cluster seed fix** (the on-hold blocker): rewrite the 4 Client seeds to hardcode Core ids as literals + resolve `roleId` by name (per `Client/2026-06-23b - ItemTranslationsAcl.sql`), zero `Core.` refs. User chose "I'll give you the prod ids" — apply the Core seed to prod Core, run the lookup query (in session memory / below) to get literal recordScriptId/surfaceId/surfaceElementId, then bake them in. Also audit the EXISTING `2026-06-25c`/`2026-06-29c`/`2026-06-25d` surface seeds for the same anti-pattern.
|
|
41
|
+
- **Action-bar / approval-modal migration** — still JSON; pulls in the Compass approval state machine + Tier-2 `stepTwoAssigned`. The `sales-order-actions` BUTTON_BAR surface is already seeded but not yet consumed by the FE.
|
|
42
|
+
- **Seed NYCHH / Prudential / SPGlobal** (and add to the roster) once confirmed live on toga25-supply; they need Quad-like client-level overrides to match the DEFAULT shape.
|
|
43
|
+
|
|
44
|
+
## Current file state
|
|
45
|
+
| File | Status | Notes |
|
|
46
|
+
|------|--------|-------|
|
|
47
|
+
| _underscore/Model/Core/Surface.php | modified | metaGroup() + message-resolution fixes; php -l clean |
|
|
48
|
+
| dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql | created | meta-group script + order-recurring + 5 display surfaces + markers. Prod-safe (Core→Core). |
|
|
49
|
+
| dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql | created | meta-group dispatch grant. ⚠ cross-DB Core ref — prod fix pending. |
|
|
50
|
+
| dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql | created | MANAGER hide invoices/admin-notes. ⚠ cross-DB. |
|
|
51
|
+
| dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql | created | MANAGER deltas (shippingMethod/invoices/admin-notes), role by name. ⚠ cross-DB. |
|
|
52
|
+
| dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql | created | client-level deltas (costCenter/shippingMethod/recurringTotal + 3 display hides). ⚠ cross-DB. |
|
|
53
|
+
| toga25-supply/src/surface/useFetchSurfaceMeta.ts | modified | added useFetchSurfaceMetaGroup + extractGroup |
|
|
54
|
+
| toga25-supply/src/surface/index.ts | modified | re-export group hook |
|
|
55
|
+
| toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts | created | adapter + slug list + migrated-client roster |
|
|
56
|
+
| toga25-supply/.../SalesOrderRecordModalLayout/helpers/index.ts | modified | re-export adapter + roster |
|
|
57
|
+
| toga25-supply/.../viewModel/useSalesOrderRecordModalLayoutModel.tsx | modified | roster-gated Surface merge |
|
|
58
|
+
| toga25-supply/.../FIELDS/{COMPASS,COMPASSCANADA,QUAD}/orderViewFields.json | modified | section blocks deleted; data-wiring kept |
|
|
59
|
+
|
|
60
|
+
## Decisions made
|
|
61
|
+
- **Grouped meta = slug-list `metaGroup`, not a parent GROUP row** (CTO AGREE-with-alternative): schema-stateless, reuses resolve()/cache, avoids id drift.
|
|
62
|
+
- **Migrated-client roster gate in code, not an env flag**: protects unseeded/unknown clients from the shared Compass-shaped Core base. Remove a client from the roster only after its seeds are verified; then its JSON sections are safe to delete.
|
|
63
|
+
- **Display sections = field-less SECTION surfaces with one `sectionVisibility` marker element**: only way to express per-role section visibility given the resolver overrides elements, not surface.isVisible.
|
|
64
|
+
- **Data-wiring stays JSON** (salesOrderDetailsConfig, itemFulfillmentClickRule): presentation-only migration, config/logic boundary (Inventory precedent).
|
|
65
|
+
- **Prod-safe Client-seed pattern = literal Core ids + Client-local roleId-by-name** (per ItemTranslationsAcl). Deferred to the prod pass.
|
|
66
|
+
|
|
67
|
+
## Blockers
|
|
68
|
+
- **PROD CROSS-CLUSTER (on hold, user-paused):** the 4 Client-DB seeds use `Core.*` cross-DB subselects that break against prod's separate Core cluster. Fix recorded in memory [[surface-prod-crossdb-fix-pending]]. Lookup query to get prod literal ids is in the session thread. Dev-sandbox is unaffected (all-in-one) and fully working.
|
|
69
|
+
|
|
70
|
+
## Exact next step
|
|
71
|
+
> When resuming the prod fix: apply `Core/2026-06-30a` to the prod Core cluster, then run the labeled lookup query against prod Core (returns meta-group recordScriptId + surfaceId/surfaceElementId for order-details/c_erpEntityId, ship-to/shippingMethod.name, order-recurring/isTotal, and the invoices/admin-notes/additional-order-details/notes `sectionVisibility` markers). Paste results, then rewrite the 4 Client `2026-06-30a` seeds to hardcode those Core ids as literals and resolve only `roleId` by name — removing every `Core.` reference — following `dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
_Saved by /session-save on 2026-06-30_
|
package/package.json
CHANGED