toga-ai 1.0.376 → 1.0.378

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.
@@ -9,6 +9,7 @@
9
9
  | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
10
10
  | [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
11
11
  | [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 |
12
+ | [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/backfill_isfulfillable_jul5.php |
12
13
  | [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 |
13
14
  | [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 |
14
15
  | [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 |
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: isFulfillable from NetSuite during Item Sync (Phase 1)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-20
10
+ owners: [bala]
11
+ files:
12
+ - library/app/netsuite.php
13
+ - library/app/api/toga2.php
14
+ - worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php
15
+ related:
16
+ - toga2-api-client-and-bridge.md
17
+ - ../../worker/features/netsuite-togasupply-per-client-sync.md
18
+ - ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
19
+ ---
20
+
21
+ ## Summary
22
+ This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite
23
+ `isfulfillable` flag during item sync and stamping it onto the **Agilant-catalog (source/supplier)
24
+ item** in the 2.0 platform. The client-facing copy is reached separately by the
25
+ [2.0 propagation engine](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md),
26
+ which walks the supply chain up from the source item. Only the source item is stamped here.
27
+
28
+ ## Key files / entry points
29
+ - `library/app/netsuite.php` — `App_NetSuite::getItemIsFulfillable($itemInternalId): bool|null`.
30
+ A SOAP `ItemSearchBasic` by internal id; returns the boolean (or `null` when unknown). Mirrors
31
+ the existing `getItemIsSerialized` exactly — the only prior item flag the sync read.
32
+ - `library/app/api/toga2.php` — `getCreateItem` (~line 4364). During item create/update the sync
33
+ now reads `$isFulfillableItem = App_NetSuite::getItemIsFulfillable($nsItemInternalId)` and, when
34
+ **non-null**, stamps `$payload['isFulfillable']` on the create/update PUT|POST to api2. The
35
+ NetSuite internal id is resolved from the order line or from the part number
36
+ (`getItemInternalIdFromPartNumber` / `getItemGroupInternalIdFromPartNumber`). Stores
37
+ `c_netsuiteInternalItemId` on the item.
38
+ - `worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php` — one-time backfill (see below).
39
+
40
+ ## How it works
41
+ 1. Item sync resolves the NetSuite internal id for the item (order line, else part-number lookup).
42
+ 2. `getItemIsFulfillable` SOAP-searches the item and returns the flag.
43
+ 3. If non-null, `getCreateItem` adds `isFulfillable` to the item PUT/POST payload to api2. api2's
44
+ Items interceptor then propagates it up-chain (Phase 2).
45
+ 4. Going forward the **daily sync** handles new items automatically; existing items are caught up
46
+ by the one-time backfill.
47
+
48
+ ### One-time backfill (July 5+ orders)
49
+ `backfill_isfulfillable_jul5.php` catches up Agilant items on SalesOrders since **2026-07-05**:
50
+ - It **PUTs each source item to `/items`** via `App_Api_Toga2::send` — the *same* call the sync
51
+ makes — so **api2's interceptor performs the recursion**. It deliberately does **NOT** write the
52
+ DB directly and does **NOT** walk the chain itself. (Design choice: reuse the one propagation
53
+ path so backfill and steady-state behave identically.)
54
+ - Reads the value from NetSuite **by part number**, fulfillable-if-any (`max` across matching
55
+ internal ids).
56
+ - Env-agnostic: `App_Registry::get('config')` with `DB_HOST_PREFERENCE` = `localhost` (local) /
57
+ `writer.client` (prod); `DRY_RUN` flips true→false. Compass credentials are the same source as
58
+ `sync_togasupply_compass_usa.php`.
59
+
60
+ ## Key gotcha — what NetSuite `isfulfillable` actually means
61
+ NetSuite `isfulfillable` (SuiteQL `SELECT isfulfillable FROM item`) is **`T` for InvtPart AND
62
+ NonInvtPart (services)**; it is `F` **only** for Group/bundle headers. It does **NOT** distinguish
63
+ physical goods from services — `itemtype` would, but the sync has never used `itemtype` (only
64
+ `isSerialized`), so this feature intentionally reflects NetSuite's flag **as-is**. **Net effect:
65
+ nearly everything resolves to `fulfillable = 1`.**
66
+
67
+ - **Open product decision (caveat):** because services come back fulfillable, the storefront makes
68
+ everything clickable. Whether that is intended vs. needing `itemtype` to hide services is
69
+ **pending product confirmation**.
70
+
71
+ ## Verification performed
72
+ - **Prod read-only dry-run:** 20 reachable Compass items; chain walk correct
73
+ (Agilant `1989 B4NY9UC#ABA` → Compass `1986 B4NY9UC-2`); ~91% of July-5+ order lines reach a
74
+ Compass item, all landing on the correct Compass copy.
75
+ - **Local live backfill:** 85 Agilant PUTs → interceptor → 20 distinct Compass items set (all `=1`),
76
+ each verified to trace through an Agilant source.
77
+
78
+ ## Gotchas / known issues
79
+ - **Value stamped only on the Agilant source item here** — the client-facing copy is set by the 2.0
80
+ interceptor. If the interceptor rows aren't deployed in the target env, api2 **403s the whole item
81
+ write** on the unknown `isFulfillable` field (see the Phase-2 doc's deploy gotchas).
82
+ - **Backfill fires the api2 path, not direct SQL.** It relies on the Phase-2 interceptor being live
83
+ in the env it targets; running it against an env without the interceptor rows will 403 every PUT.
84
+ - **🔐 Credentials:** the backfill's Compass client/api uuids come from config; the API secret lives
85
+ in `Client_Compass.Apis` (name `Agilant`) — never reproduce the secret value.
86
+
87
+ ## Change history
88
+ - 2026-07-20 — Added `App_NetSuite::getItemIsFulfillable` (SOAP `ItemSearchBasic`, mirrors
89
+ `getItemIsSerialized`) and wired `getCreateItem` to stamp `isFulfillable` on the item PUT/POST to
90
+ api2 when non-null (internal id from order line or part-number lookup; stores
91
+ `c_netsuiteInternalItemId`). Added the one-time `backfill_isfulfillable_jul5.php` cron, which
92
+ re-PUTs Agilant source items so the api2 interceptor does the recursion (no direct DB writes).
93
+ Documented the NetSuite `isfulfillable` semantics gotcha (T for InvtPart + services, F only for
94
+ group headers — does not distinguish service vs physical) and the resulting open product decision.
95
+ (bala)
96
+
97
+ ## Related docs
98
+ - [App_Api_Toga2 — API Client & 1.0↔2.0 Bridge](toga2-api-client-and-bridge.md) — the transport
99
+ `getCreateItem`/the backfill use to reach api2.
100
+ - [NetSuite → TOGa Supply Per-Client Sync](../../worker/features/netsuite-togasupply-per-client-sync.md)
101
+ — the sync engine this item flag rides in.
102
+ - [Phase 2 — isFulfillable propagation up the SO↔PO chain (2.0)](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md).
@@ -198,6 +198,12 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
198
198
 
199
199
  ## Change history
200
200
 
201
+ - 2026-07-20 — Fixed a pre-existing **missing `try`-close / brace bug** in
202
+ `common_sync_togasupply.php` (the inventory-adjustments and item-fulfillments blocks lacked a
203
+ `try` close, a parse error that broke the Compass sync run). Same failure class as the 2026-07-09
204
+ tier-wide `rest.php` outage — a dropped token in this shared engine crashes the run before records
205
+ process. (Fixed incidentally while shipping the `isFulfillable` item-sync feature; see the
206
+ [library Phase-1 doc](../../library/features/netsuite-item-isfulfillable-sync.md).) (bala)
201
207
  - 2026-07-10 — Added **Quad + Growrk** to the NetSuite Sync Alert monitor's
202
208
  `CLIENT_CONFIGURATIONS` (previously unmonitored); **AIG deliberately left off** (held
203
209
  account, checkpoints frozen at the 2026-04-28 seed). Recorded a real, ongoing IF/InvAdj
@@ -16,6 +16,7 @@
16
16
  | [Error Reporting — Issue/Event Aggregation (agreed POST-to-receiver design)](features/error-reporting-issue-event.md) | Platform-wide error-reporting infrastructure for TOGA 2.0, built around a two-table **Issue / Event** aggregation model in the shared **Core Logs DB**. | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
17
17
  | [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 |
18
18
  | [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
19
+ | [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. | _underscore/Model/Client/Item.php, _underscore/Model/Compass/Item.php, dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql |
19
20
  | [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
20
21
  | [_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 |
21
22
  | [_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 |
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: isFulfillable Propagation Up the SO↔PO Chain
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-20
10
+ owners: [bala]
11
+ files:
12
+ - _underscore/Model/Client/Item.php
13
+ - _underscore/Model/Compass/Item.php
14
+ - dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql
15
+ - dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql
16
+ - dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql
17
+ related:
18
+ - recursive-item-fulfillments.md
19
+ - ../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md
20
+ - ../../../clients/compass-usa/profile.md
21
+ ---
22
+
23
+ ## Summary
24
+ `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled**
25
+ cell is actionable. The value is **born on the Agilant-catalog (source/supplier) item** during
26
+ the 1.0 NetSuite item sync (see the [Phase 1 doc](../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)),
27
+ but the toga2-supply storefront shows the **client's own catalog row** — a *different* `Items`
28
+ row linked to the source item only through the multi-tier supply chain. This feature is the
29
+ **2.0 (Phase 2)** half: an api2 interceptor that, whenever an item write carries `isFulfillable`,
30
+ walks the `SalesOrderItem`↔`PurchaseOrderItem` chain **upward** and stamps the same value onto
31
+ every client-facing item it reaches. Value flows source → client copy so the storefront gate is
32
+ correct on the row the customer actually sees.
33
+
34
+ This is a sibling of the [Recursive Item Fulfillments](recursive-item-fulfillments.md) engine —
35
+ same bridge topology, walked for a different payload (a scalar flag rather than fulfillment rows).
36
+
37
+ ## Key files / entry points
38
+ - `_underscore/Model/Client/Item.php`
39
+ - `propagateFulfillableAcrossChain(string $itemUuid)` (~lines 497–565) — the engine.
40
+ - `propagateFulfillableWhenPayloadCarriesTheFlag($api)` — the interceptor body, registered on
41
+ `postPost` and `postPut` for Items. **Gated:** `if (!isset($api->httpPayload->isFulfillable)) return;`
42
+ so an ordinary item edit that doesn't touch the flag never triggers a chain walk.
43
+ - `_underscore/Model/Compass/Item.php` — Compass's Item subclass **must call
44
+ `parent::postPost` / `parent::postPut`** so the base propagation isn't shadowed by the Compass
45
+ price-override override. (Same empty-pass-through pattern the recursive-IF engine relies on.)
46
+
47
+ ## How it works
48
+ 1. **Interceptor gate.** `postPost`/`postPut` call `propagateFulfillableWhenPayloadCarriesTheFlag`,
49
+ which returns immediately unless the payload carried `isFulfillable`. Only the sync (and the
50
+ one-time backfill) send that field, so day-to-day item edits skip the walk entirely.
51
+ 2. **Load the source item** by uuid → `id` + `isFulfillable`. If the value is **NULL**, return
52
+ (nothing authoritative to propagate).
53
+ 3. **Walk the chain UP** with a `WITH RECURSIVE ChainSalesOrderItems` CTE:
54
+ - **Anchor:** `SalesOrderItems WHERE itemId = <sourceItemId>`.
55
+ - **Recursive step:** join `PurchaseOrderItems_SalesOrderItems` (PSO) then
56
+ `SalesOrderItems_PurchaseOrderItems` (SPO) to climb one tier
57
+ (Agilant SOI → PSO → POI → SPO → next SOI). `UNION` (not `UNION ALL`) stops loops.
58
+ - **Result:** `SELECT DISTINCT LinkedSalesOrderItems.itemId WHERE itemId <> sourceItemId` —
59
+ every client-facing item id reachable above the source.
60
+ 4. **Diff-only update:**
61
+ `UPDATE Items SET isFulfillable = <value> WHERE id IN (...) AND (isFulfillable <> <value> OR isFulfillable IS NULL)`.
62
+ Re-runs are no-ops, so repeated syncs / a re-run backfill are safe.
63
+
64
+ ## Chain topology (VERIFIED in prod)
65
+ - **Agilant items sit on the `PSO.salesOrderItemId` side; Compass (client-facing) items sit on
66
+ the `SPO.salesOrderItemId` side.** The value born on the Agilant *source* item therefore flows
67
+ **up** to the Compass item the storefront renders.
68
+ - Same four bridge tables as the recursive-IF engine (`PurchaseOrders_SalesOrders`,
69
+ `SalesOrders_PurchaseOrders`, `PurchaseOrderItems_SalesOrderItems`,
70
+ `SalesOrderItems_PurchaseOrderItems`), so a **broken/missing SOI↔POI bridge row breaks this walk
71
+ too** — the same integrity class as the Compass off-by-one bridge bugs (see
72
+ [MITS PO → SO Item Linking](../../../clients/compass-usa/features/mits-po-to-so-item-linking.md)).
73
+
74
+ ## Data model / schema (deploy)
75
+ Three `dbchanges2` migrations register the field + hooks; **all must be present in the target
76
+ env or the flag write is rejected:**
77
+ 1. **Core RecordField** (`Core/2026-07-17 - Items isFulfillable RecordField.sql`) — recordId **21**
78
+ (Items), field `isFulfillable`, BOOLEAN.
79
+ 2. **Core `ApiPayloadInterceptors`** (`Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql`)
80
+ — rows for recordId 21, **POST/POST** and **POST/PUT**, global (clientId/apiId null).
81
+ 3. **Client column + ACL** (`Client/2026-07-17 - ItemsisFulfillable.sql`) — `Items.isFulfillable`
82
+ column + ACL rows.
83
+
84
+ ## Conflict rule
85
+ Multi-source kits (an item reachable from more than one source) use **LAST-WRITER-WINS**. A
86
+ deterministic `MIN`/`MAX` aggregate was **deliberately deferred** — harmless while values are
87
+ uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfillable).
88
+
89
+ ## Client variations
90
+ - **Compass** (USA/Canada, DB `Client_Compass`) is the built/verified client. Its `_Model_Compass_Item`
91
+ overrides `postPost`/`postPut` for a price override, so it **must** `parent::` up to the base or
92
+ propagation silently never fires.
93
+ - Engine itself is shared `_Model_Client_Item` logic; any client with the schema present gets it.
94
+
95
+ ## Gotchas / known issues
96
+ - **⚠ Interceptors are DB-driven.** The `postPost`/`postPut` PHP does nothing without the
97
+ `ApiPayloadInterceptors` rows in the **target env's Core DB**. Missing rows → the hook never
98
+ fires **and** api2 EV-8's the unknown `isFulfillable` field, **403-ing the whole request** (not
99
+ just dropping the field). This is the same interceptor-row requirement as the recursive-IF engine.
100
+ - **⚠ Cross-cluster Core/client on prod.** On prod, Core and client DBs are on **separate
101
+ clusters**, so the client ACL migration's cross-DB `Core.RecordFields` uuid lookup must be
102
+ **resolved/substituted at deploy** — it cannot join across clusters at runtime.
103
+ - **⚠ Compass subclass shadowing.** If a future client subclass overrides Items `postPost`/`postPut`
104
+ without calling `parent::`, propagation silently stops for that client.
105
+ - **Product decision unresolved (caveat, not a bug).** Because NetSuite's `isfulfillable` is `T`
106
+ for services as well as physical goods (see Phase-1 doc), propagation makes **everything**
107
+ clickable on the storefront. Whether "reflect NetSuite's flag as-is" is the intended behavior —
108
+ vs. using `itemtype` to hide services — is **pending product confirmation**.
109
+
110
+ ## Change history
111
+ - 2026-07-20 — Built the isFulfillable up-chain propagation engine:
112
+ `propagateFulfillableAcrossChain` + the payload-gated `postPost`/`postPut` interceptor on Items
113
+ (recursive SOI↔POI CTE, diff-only `UPDATE`), plus the Compass `parent::` fix so the price-override
114
+ subclass doesn't shadow it. Registered the field + interceptors via three `dbchanges2` migrations
115
+ (Core RecordField 21, Core POST/POST + POST/PUT interceptor rows, Client column + ACL). Verified
116
+ in prod (Agilant `1989 B4NY9UC#ABA` → Compass `1986 B4NY9UC-2`; 20 reachable Compass items) and via
117
+ a local live backfill (85 Agilant PUTs → 20 distinct Compass items set). Last-writer conflict rule
118
+ for multi-source kits (MIN/MAX aggregate deferred). (bala)
119
+
120
+ ## Related docs
121
+ - [Phase 1 — isFulfillable from NetSuite during item sync (1.0)](../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)
122
+ — where the value is sourced + the one-time backfill.
123
+ - [Recursive Item Fulfillments](recursive-item-fulfillments.md) — the sibling engine over the same
124
+ bridge topology.
125
+ - [Compass MITS PO → SO Item Linking](../../../clients/compass-usa/features/mits-po-to-so-item-linking.md)
126
+ — how the SOI↔POI bridges get built (and the integrity bugs that break this walk).
@@ -143,6 +143,9 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
143
143
  - 2026-06-08 — Documented the Recursive Item Fulfillments engine (interceptor-driven upstream fulfillment mirroring, bundle scaling, reconcile loop). (jcardinal)
144
144
 
145
145
  ## Related docs
146
+ - [isFulfillable Propagation Up the SO↔PO Chain](fulfillable-item-propagation.md) — a sibling
147
+ engine that walks the **same** bridge topology to propagate a scalar item flag (rather than
148
+ fulfillment rows) from the source item up to the client-facing copy.
146
149
  - GroWrk transfer order flow plan: `clients/growrk/features/transfer-order-flow.md`.
147
150
  - `_underscore` architecture (interceptors, `internalApiRequest`, `_Model` layer).
148
151
  - `api2` architecture (V2 metadata engine that fires these interceptors; the
@@ -3,5 +3,5 @@
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/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 |
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/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 |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-17
9
+ updated: 2026-07-20
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
@@ -38,6 +38,13 @@ files:
38
38
  - dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
39
39
  - dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql
40
40
  - dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
41
+ - dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql
42
+ - dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql
43
+ - dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql
44
+ - dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql
45
+ - dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql
46
+ - dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql
47
+ - dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql
41
48
  related:
42
49
  - ../../_underscore/features/surface-resolver.md
43
50
  ---
@@ -167,6 +174,45 @@ to `{}` so Core carries **no** `_status` default. Every client — even Quad, wh
167
174
  old default — now declares the full filter config via its own `CONFIG` override. This makes the Core
168
175
  seed a true neutral default for the filter button, matching the action-button rule above.
169
176
 
177
+ ## Display-section visibility — two patterns (marker vs field-driven)
178
+
179
+ A record-modal **display SECTION**'s own `Surfaces.isVisible` can never be client-overridden (the
180
+ cascade keys on `surfaceElementId`, never a Surface's own `isVisible` — see gotcha). Section
181
+ visibility is therefore always carried by an **element**, in one of two patterns depending on whether
182
+ the section has per-client field content:
183
+
184
+ - **Marker pattern (whole-section on/off)** — a section with fixed content (invoices / shipments /
185
+ additional-order-details / admin-notes) carries a single hidden TEXT **`sectionVisibility` marker
186
+ element** (`config.role='sectionVisibility'`); that element's `isVisible` *is* the section's
187
+ visibility. A client flips the section by overriding the marker's `IS_VISIBLE`.
188
+ - **Field-driven pattern (per-client field set)** — a section whose fields vary per client (notes)
189
+ models **each field as a real `FIELD` `SurfaceElement`** (the source of truth for that field's label
190
+ + visibility). No marker element: the FE **DERIVES** the section's visibility as "visible iff ≥1
191
+ field element is visible" (see
192
+ [surface-frontend](../../toga25-supply/features/surface-frontend.md)). This is the third section
193
+ pattern, alongside the card sections (`cardType` → grid renderer) and the marker toggles.
194
+
195
+ ### Core neutral default + client opt-in (admin-notes & notes, 2026-07-20)
196
+ Both sections follow the Core-neutral-default → client-opt-in rule (the same rule as the action/filter
197
+ buttons above): Core seeds the section **off**, each client opts in via a `SurfaceOverride`.
198
+
199
+ - **admin-notes (marker pattern).** `Core/2026-07-20a` flips the admin-notes section's
200
+ `sectionVisibility` marker `isVisible=0`, so the section is **hidden by default**. Resolved
201
+ **id-agnostically** by `Surfaces.slug='admin-notes'` + `config.role='sectionVisibility'` (never a
202
+ hardcoded element id). **Compass + Compass Canada** opt the section back on with a client-wide
203
+ `IS_VISIBLE='1'` override on that same element (`Client_Compass/2026-07-20a`,
204
+ `Client_CompassCanada/2026-07-20a`); every other client stays hidden.
205
+ - **notes (field-driven pattern).** `Core/2026-07-20b` migrates the two previously-hardcoded FE fields
206
+ (`reasonForRequest`, `shippingInstructions`) into two `FIELD` `SurfaceElements` under the `notes`
207
+ surface, both `isVisible=0` in Core (**default = neither field → the section is hidden**). Per-client
208
+ `IS_VISIBLE` overrides: **Quad → shippingInstructions only** (`Client_Quad/2026-07-20a`); **Compass +
209
+ Compass Canada → both** (`Client_Compass/2026-07-20b`, `Client_CompassCanada/2026-07-20b`).
210
+ - **A note field's value binding is a nested-array lookup, not a flat column.** Note text lives in
211
+ `order.salesOrderNotes[]` (each entry has `salesOrderNoteType.slug` + `note`), not a flat order
212
+ field. Each notes `FIELD` element names its note type in **`config.noteTypeSlug`** (`request` /
213
+ `shipping`); the FE resolves `salesOrderNotes[].note` by that slug (client-agnostic). Per-client slug
214
+ variance is handled by a **`CONFIG` override** on the element (REPLACE semantics) — no FE change.
215
+
170
216
  ## ThemeTokens — per-tenant, physically isolated in each client DB
171
217
 
172
218
  `ThemeTokens` lives **inside each tenant's own database** (`Client_Compass.ThemeTokens`,
@@ -320,6 +366,18 @@ rule resumes.
320
366
  override is added). **Open follow-up.**
321
367
 
322
368
  ## Change history
369
+ - 2026-07-20 — Applied the Core-neutral-default → client-opt-in pattern to two SalesOrder display
370
+ sections. **admin-notes** (marker pattern): `Core/2026-07-20a` hides it by default (marker
371
+ `isVisible=0`, resolved id-agnostically by `Surfaces.slug`+`config.role='sectionVisibility'`);
372
+ Compass + Compass Canada opt back in via a client-wide `IS_VISIBLE` override
373
+ (`Client_Compass`/`Client_CompassCanada/2026-07-20a`). **notes** (new field-driven pattern):
374
+ `Core/2026-07-20b` migrates the two hardcoded FE note fields into `FIELD` SurfaceElements (both
375
+ `isVisible=0` = neither by default), with per-client `IS_VISIBLE` overrides — Quad = shipping only
376
+ (`Client_Quad/2026-07-20a`), Compass + Compass Canada = both (`Client_*/2026-07-20b`). Documented the
377
+ two display-section visibility patterns (marker for whole-section on/off vs field-driven where the FE
378
+ derives section visibility from "≥1 field visible") and note-field value binding via
379
+ `config.noteTypeSlug` into `order.salesOrderNotes[]` (per-client slug variance via a `CONFIG`
380
+ override). (apeterson)
323
381
  - 2026-07-17 — Applied the Core-neutral-default → client-opt-in pattern to the sales-order action &
324
382
  filter buttons across Compass, CompassCanada, and Quad (`Client_*/2026-07-17a`+`b`). Cleared the
325
383
  Core approvals-filter `_status` default (`Core/2026-07-17h`) so every client declares its own filter
@@ -9,6 +9,6 @@
9
9
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
10
10
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
11
11
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
12
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
12
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
13
13
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
14
14
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-13
9
+ updated: 2026-07-20
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
@@ -16,6 +16,8 @@ files:
16
16
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx
17
17
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx
18
18
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx
19
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx
20
+ - toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts
19
21
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts
20
22
  - toga25-supply/src/surface/evaluateSurfaceRule.ts
21
23
  - toga25-supply/src/surface/actionRegistry.ts
@@ -173,6 +175,31 @@ three bespoke renderers `detailSection`/`locationCard`/`totalsCard` are untouche
173
175
  `sectionVisibility` marker element). **Decision (confirmed by the developer this session): KEEP the
174
176
  adapter approach — do NOT replace it with a direct `<SurfaceSection>` swap.** The bespoke grid
175
177
  renderers and the `TenantFields` seam are the deliberate integration point.
178
+
179
+ ### `displayToggle` sections — marker vs field-driven (the notes section)
180
+ `buildSection`'s `displayToggle` branch handles **two** shapes (mirroring the schema's two
181
+ display-section patterns — see
182
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)):
183
+ - **Field-carrying section (notes)** — the branch maps every `FIELD`-`renderType` element to a field
184
+ config via `elementToFieldConfig` (now carrying **`config.noteTypeSlug`**) and **DERIVES** the
185
+ section's `isVisible` as `fields.some(f => f.isVisible)` — no `sectionVisibility` marker needed. It
186
+ emits both `isVisible` and the `fields[]` array.
187
+ - **Pure-toggle section (invoices / shipments / additional-order-details / admin-notes)** — no `FIELD`
188
+ elements, so it falls back to the single `sectionVisibility` marker element's `isVisible`.
189
+
190
+ **`SalesOrderNotesSection`** consumes the field-driven shape: it renders **only the visible note
191
+ slots** and resolves each value from `order.salesOrderNotes[]` by
192
+ `salesOrderNoteType.slug === field.noteTypeSlug` (note text is a nested-array entry, not a flat order
193
+ column). When no surface `fields` are supplied it falls back to the **legacy flat props**
194
+ (`reasonForRequest` / `shippingInstructions`, flattened by `cleanOrder`) so non-migrated JSON clients
195
+ still render — see the `cleanOrder` slug gotcha below.
196
+
197
+ **Reusable pattern (field-driven display section):** for a section whose content varies per client,
198
+ model each field as a `FIELD` element (label + visibility source of truth), derive section visibility
199
+ from "≥1 field visible", and bind an array-derived value via element `config` (here `noteTypeSlug`)
200
+ resolved on the FE. This is the presentation counterpart of the schema's field-driven pattern and the
201
+ third section pattern alongside card sections (`cardType` → grid renderer) and marker toggles.
202
+
176
203
  - **Roster gate — `SALES_ORDER_SURFACE_MIGRATED_CLIENTS = [COMPASS, COMPASSCANADA, QUAD]`** (an
177
204
  in-code roster in `surfaceBundleToTenantFields.ts`; the view-model
178
205
  `useSalesOrderRecordModalLayoutModel.tsx` consults it). The view-model resolves sections from
@@ -222,6 +249,14 @@ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster
222
249
  intentionally gated in `getDetailSections`: `shipments` is **permanently hidden** (the view-model
223
250
  hardcodes `shipmentsData: []` and `useShipments` is commented out; the section gates on
224
251
  `shipmentsData.length > 0`), and `invoices` is data-gated on `invoiceData.length > 0`.
252
+ - **`cleanOrder.ts` flattens notes by the WRONG note-type slugs (latent blank-notes bug).**
253
+ `cleanOrder` builds the legacy flat `order.reasonForRequest` / `order.shippingInstructions` via
254
+ `getNoteBySlug(notes, "reasonForRequest")` / `("shippingInstructions")`, but the API's actual
255
+ `salesOrderNoteType.slug` values are **`request`** / **`shipping`**. So for any client whose notes
256
+ use the real slugs (e.g. Compass) those flat fields resolve to `""` and the legacy notes render
257
+ blank. The new field-driven notes path resolves by the element's `config.noteTypeSlug` (the correct
258
+ slug) so it is unaffected; the flat path only survives as the non-migrated-client fallback in
259
+ `SalesOrderNotesSection`. Fixing `cleanOrder`'s slugs is the durable follow-up for JSON clients.
225
260
  - **The surface `record` shape must match the rule field namespace — wrap as `{ order }`.** Seeded
226
261
  Tier-1 rules namespace their field as `order._status`, so `getByPath(record, "order._status")` only
227
262
  resolves if the host passes `record={{ order }}`. Passing the bare order object resolves to
@@ -258,6 +293,18 @@ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster
258
293
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
259
294
 
260
295
  ## Change history
296
+ - 2026-07-20 — Made the SalesOrders `notes` display section **field-driven** per client.
297
+ `buildSection`'s `displayToggle` branch now handles field-carrying sections: it maps each `FIELD`
298
+ element to a field config (`elementToFieldConfig` now carries `config.noteTypeSlug`) and **derives**
299
+ section visibility as "≥1 field visible" — no `sectionVisibility` marker needed (pure-toggle sections
300
+ invoices/shipments/additional-order-details/admin-notes still fall back to their marker element).
301
+ `SalesOrderNotesSection` renders only the visible note slots and resolves each value from
302
+ `order.salesOrderNotes[].note` by `salesOrderNoteType.slug === config.noteTypeSlug`, with a legacy
303
+ flat-prop fallback for non-migrated JSON clients. Recorded the reusable field-driven-display-section
304
+ pattern (third section pattern, alongside card sections and marker toggles). Discovered a latent
305
+ blank-notes bug in `cleanOrder.ts` (flattens by the wrong slugs
306
+ `reasonForRequest`/`shippingInstructions` vs the API's actual `request`/`shipping`) — the surface
307
+ resolver reads by the correct slug and bypasses it. (apeterson)
261
308
  - 2026-07-13 — Fixed two SalesOrders row-actions bugs: (1) the portal dismiss overlay in
262
309
  `SurfaceRowActions.tsx` propagated an outside-click up the React component tree to the row handler
263
310
  and opened the record modal — added `e.stopPropagation()` (+ close) on the overlay `onClick`;
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 12 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 13 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 15 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
10
10
  - **togadesk** (TOGa Desk) — 9 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 34 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 35 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -11,6 +11,7 @@ apps:
11
11
  - worker1.5
12
12
  - worker2
13
13
  - dbchanges2
14
+ - library
14
15
  project: _Underscore
15
16
  client: compass-usa
16
17
  type: profile
@@ -88,4 +89,10 @@ separate, related client (see its own profile).
88
89
  order. Contrast Quad, which gets the full picked/packed machine. See
89
90
  [IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
90
91
  - Built on the shared 2.0 Recursive Item Fulfillments engine (upstream mirroring).
92
+ - **`Items.isFulfillable` gates the storefront Qty Fulfilled cell (2026-07).** Sourced from
93
+ NetSuite onto the Agilant source item during the 1.0 item sync
94
+ ([Phase 1](../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)) and propagated
95
+ up the SO↔PO chain to the client-facing Compass item by an api2 interceptor
96
+ ([Phase 2](../../2.0/apps/_underscore/features/fulfillable-item-propagation.md)). Verified on prod
97
+ Compass chains; a one-time July-5+ backfill re-PUTs source items through the same path.
91
98
  - This profile is a starting point; expand as more Compass-specific behavior is captured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.376",
3
+ "version": "1.0.378",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",