toga-ai 1.0.767 → 1.0.769

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.
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-09-03
10
- owners: [dfranks, jcardinal, kyalamarthi]
10
+ owners: [dfranks, jcardinal, kyalamarthi, rgirish]
11
11
  files:
12
12
  - library/app/api/netsuite/rest.php
13
13
  - library/app/netsuite.php
@@ -78,6 +78,22 @@ Forecast2 tables already store.
78
78
  a per-type factor (it double-negates reversals back to positive). The shim flips
79
79
  `amount = -foreignamount`, `costEstimate = -costestimate`.
80
80
 
81
+ - **⚠ `->holdInvoice` on a sales-order shim is NOT a native NetSuite field — it is normalised from
82
+ the custom checkbox `custbody_asi_hold` (internal id 4082), and that field is SHARED by all
83
+ clients.** NetSuite REST exposes no top-level `holdInvoice`. The `"asi"` prefix is **Agilant
84
+ Solutions Inc** (the company), *not* a client prefix — do not read it as client-specific. The read
85
+ lives **inside the shim**, in all three fetch paths: `buildSalesOrderShims()` (~L533, feeding both
86
+ `listSalesOrders()` and `fetchSalesOrderByIdFull()`) and `fetchSalesOrderById()` (~L1537), so
87
+ callers such as `App_Api_Toga2::isTransferOrder()` read an already-normalised boolean. (An earlier
88
+ version read `$nsOrder->holdInvoice` in the caller with a `custbody_hold_invoice` fallback — the
89
+ wrong field name; commit `07c31e24`, 2026-08-19, found the real one and moved the read down here.)
90
+ `worker/crons/toga2/compass/edi/2_create_so_netsuite.php` **writes** the same field.
91
+ **⚠ The SOAP client `library/app/netsuite.php` never sets `holdInvoice` at all**, so any detection
92
+ that depends on it (e.g. the `zero_dollar_hold` transfer-order mode — see
93
+ [per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)) is **silently
94
+ always-false** on a SOAP path. Verified not a live risk: the togasupply engine calls
95
+ `App_Api_Netsuite_Rest::` on every sales-order/receipt/fulfillment path.
96
+
81
97
  - **`createdFrom` is not a usable SuiteQL column.** `transaction.createdfrom` is **always NULL**
82
98
  in SuiteQL (26/26). It is recoverable from `previoustransactionlinelink.previousdoc` excluding
83
99
  `previoustype = 'Opprtnty'` (an invoice links to both its SO and its opportunity; REST's
@@ -307,6 +323,16 @@ None — uniform across clients (NetSuite is a single shared account).
307
323
 
308
324
  ## Change history
309
325
 
326
+ - 2026-09-03 — Documented that a sales-order shim's **`->holdInvoice` is not a native NetSuite
327
+ field**: it is normalised inside the shim from the custom checkbox **`custbody_asi_hold` (id
328
+ 4082)**, which is **shared across all clients** ("asi" = Agilant Solutions Inc, not a client
329
+ prefix). Set in all three fetch paths (`buildSalesOrderShims()` feeding `listSalesOrders()` /
330
+ `fetchSalesOrderByIdFull()`, plus `fetchSalesOrderById()`); traced to commit `07c31e24`
331
+ (2026-08-19), which corrected the earlier caller-side read of `custbody_hold_invoice`. Recorded the
332
+ trap that the **SOAP client `app/netsuite.php` never sets it**, so hold-based detection is silently
333
+ always-false there — verified harmless today because the togasupply engine is REST-only.
334
+ Read-only session; no change to `rest.php`. (rgirish)
335
+
310
336
  - 2026-09-03 — Recorded that **`fetchItemFulfillmentById` and `fetchInventoryAdjustmentById` omitted
311
337
  `->lastModifiedDate`** (unlike `fetchItemReceiptById`), which froze ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS
312
338
  once the 2026-09-02 per-record cursor read that field off the by-reference record these shims replace. GroWrk
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-09-03
10
- owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
10
+ owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
13
13
  - worker/crons/toga2/netsuite/sync_togasupply_canon.php
@@ -463,29 +463,71 @@ the code deploy).
463
463
 
464
464
  ## Client variations
465
465
 
466
- - **Transfer-order routing is opt-in per wrapper (default OFF).** A client whose NetSuite
467
- *transfer orders* arrive as NetSuite *sales orders* can have the importer split them into
468
- `TransferOrders` vs `SalesOrders` by setting
469
- `const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` before the `require_once`
470
- (NYCHH's `sync_togasupply_hh.php` is the only one that does). The signal is the NetSuite custom
471
- checkbox `custbody_stocking_order`, read by `App_Api_Toga2::isTransferOrder()`/`isStockingOrder()`
472
- — **not** a dollar/hold heuristic. Full mechanics, PO bridges, and origin-location resolution:
473
- [NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
474
- - **Transfer-order integration is governed by TWO DISTINCT wrapper flags — a master enable switch and a
475
- NYCHH-only classification-mode switch (do not conflate them).**
466
+ - **Transfer-order integration is governed by TWO DISTINCT wrapper constants — a master enable switch
467
+ and an explicit per-client detection MODE (do not conflate them).** These orders **masquerade as
468
+ NetSuite sales orders** and are split into `TransferOrders` vs `SalesOrders` by
469
+ `App_Api_Toga2::isTransferOrder()`.
476
470
  - **`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS`** — the master "do transfer-order integration at all"
477
- switch, defined in every `sync_togasupply_*.php` launcher. `true` for **GroWrk, Adyen, Elite, and
478
- NYCHH**; `false` for the rest. These are the clients exercised by the shared
479
- `syncTransferOrderFromNetsuite` / `getTransferOrderItemsByUuid` fixes (2026-08-30 above).
480
- - **`IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`** — a **separate classification-mode** switch,
481
- **NYCHH-only** (`sync_togasupply_hh.php`), that makes `App_Api_Toga2::isTransferOrder()` classify by
482
- the NetSuite `custbody_stocking_order` checkbox instead of the legacy rule.
483
-
484
- These orders **masquerade as NetSuite sales orders** and are split into `TransferOrders` vs
485
- `SalesOrders` by `isTransferOrder()`. The classification **SIGNAL is per-client**: **NYCHH** routes by
486
- the `custbody_stocking_order` stocking flag (above); **GroWrk / Elite / Adyen** use the **legacy rule**
487
- (`holdInvoice AND total === 0`). Do **not** assume one universal signal — the master enable is shared,
488
- the classification mode is not.
471
+ switch, defined in every `sync_togasupply_*.php` launcher. `true` for **GroWrk, Adyen, and
472
+ NYCHH**; `false` for Elite (pre-wired, not on) and the rest. **Every one of the 7
473
+ `isTransferOrder()` call sites is guarded by this**, so the mode throw below can never fire for a
474
+ client with transfer orders off.
475
+ - **`TRANSFER_ORDER_DETECTION_MODE`** — the classification mode, one of two `App_Api_Toga2` class
476
+ constants (see next bullet). Required whenever the master switch is on.
477
+
478
+ ### ⚠ `TRANSFER_ORDER_DETECTION_MODE` — explicit per client, NO default, throws if unset (2026-09-03)
479
+
480
+ `isTransferOrder()` used to branch on a boolean `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`
481
+ (defined only in NYCHH's launcher) and **otherwise fall through to GroWrk's legacy `$0 total +
482
+ holdInvoice` rule**. That made the legacy rule an **implicit, silent default**: a new client turning
483
+ transfer orders on without the constant silently got another client's classification applied to its
484
+ orders, on a green-looking run. That boolean is **removed everywhere**.
485
+
486
+ Each launcher now declares an explicit mode before its `require_once`:
487
+
488
+ ```php
489
+ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG;
490
+ // or
491
+ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD;
492
+ ```
493
+
494
+ | Mode constant | Value | Rule | Clients |
495
+ |---|---|---|---|
496
+ | `TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG` | `'stocking_flag'` | NetSuite checkbox `custbody_stocking_order` (id 7097) — **unchecked = transfer order** | NYCHH |
497
+ | `TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` | `'zero_dollar_hold'` | `holdInvoice === true` **AND** `total === 0` | GroWrk, Adyen, Elite (pre-set, unused — its master switch is off) |
498
+
499
+ `App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODES` is the allowlist array. **`isTransferOrder()` THROWS
500
+ when the mode is undefined or not in the allowlist** — a deliberate fail-loud, chosen over defaulting
501
+ to the legacy rule, consistent with the 2026-08-27 "every section fails loud" policy. Consequence,
502
+ and it is not optional: **`library` and `worker` MUST deploy together.** A worker deploy without the
503
+ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
504
+
505
+ - **A separate NYCHH-only flag, `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK`,** now guards
506
+ `attachInventoryAdjustmentToItemFulfillment()` (the `custbody_stock_adjustment` link). The removed
507
+ routing boolean had been doing double duty for it; it is an unrelated feature that merely has NYCHH
508
+ as its only client today. See
509
+ [NYCHH inventory-adjustment → item-fulfillment linking](../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md).
510
+
511
+ - **`->holdInvoice` is NOT a native NetSuite field** — it is normalised by the REST shim from the
512
+ custom checkbox `custbody_asi_hold` (internal id **4082**), which is **shared across all clients**
513
+ ("asi" = Agilant Solutions Inc, the company, not a client prefix). `App_Api_Netsuite_Rest` sets it
514
+ in `buildSalesOrderShims()` (feeding `listSalesOrders()` + `fetchSalesOrderByIdFull()`) and in
515
+ `fetchSalesOrderById()`. **`app/netsuite.php` (SOAP) never sets it at all** — ZERO_DOLLAR_HOLD would
516
+ be silently always-false on a SOAP path. Not a live risk: `common_sync_togasupply.php` uses
517
+ `App_Api_Netsuite_Rest::` on **every** sales-order / receipt / fulfillment path, and all
518
+ transfer-order clients are on REST. Field detail:
519
+ [SuiteQL/REST shim](../../library/features/netsuite-suiteql-rest-shim.md).
520
+
521
+ - **⚠ OPEN (pre-existing, not fixed): an absent NetSuite `total` collapses to `0.0` and can
522
+ misclassify.** The ZERO_DOLLAR_HOLD branch reads
523
+ `$total = isset($nsOrder->total) ? (float) $nsOrder->total : 0.0;`. A REST response with a
524
+ missing/blank `total` (transient API failure — not a real $0 order) becomes `0.0`, and combined with
525
+ the hold flag set, a **non-zero order classifies as a transfer order**. This line predates the mode
526
+ refactor and was carried across unchanged; it goes against the fail-loud rule for cron/sync code.
527
+ Raised with the developer, left as-is pending a decision.
528
+ - **Full mechanics of the stocking-flag mode** (PO bridges, origin-location resolution):
529
+ [NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
530
+ Legacy-rule background: [GroWrk transfer order flow](../../../clients/growrk/features/transfer-order-flow.md).
489
531
  - `isParentCustomer` — `true` walks NetSuite child customers (`listChildCustomers`); `false`
490
532
  syncs the single customer. Prudential-EDI and the live Quad wrapper use `false`.
491
533
  - The original combined-array entries used `App_Api_Toga2::CLIENT_UUID_*` consts for Quad and
@@ -1212,6 +1254,23 @@ the code deploy).
1212
1254
 
1213
1255
  ## Change history
1214
1256
 
1257
+ - 2026-09-03 — **Transfer-order detection is now an explicit per-client MODE with no default; the old
1258
+ `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` boolean is removed from both repos.**
1259
+ `isTransferOrder()` previously branched on that boolean (NYCHH-only) and otherwise fell through to
1260
+ GroWrk's `$0 total + holdInvoice` rule — an implicit silent default that would apply one client's
1261
+ classification to another's orders on a green-looking run. Launchers now set
1262
+ `TRANSFER_ORDER_DETECTION_MODE` to one of two new `App_Api_Toga2` constants
1263
+ (`__STOCKING_FLAG` = NYCHH, `__ZERO_DOLLAR_HOLD` = GroWrk/Adyen/Elite), validated against a
1264
+ `TRANSFER_ORDER_DETECTION_MODES` allowlist; an undefined/unknown mode **throws**. All 7 call sites
1265
+ are guarded by `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS`, so the throw can't fire for a client with
1266
+ TOs off. The `custbody_stock_adjustment` adjustment→fulfillment link, which the removed boolean was
1267
+ also guarding, moved to its own `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK` (NYCHH only).
1268
+ Also recorded: `->holdInvoice` is the REST shim's normalisation of the **shared** custom checkbox
1269
+ `custbody_asi_hold` (id 4082, "asi" = Agilant Solutions Inc, not a client), never set on the SOAP
1270
+ path — safe today because this engine is REST-only; and the pre-existing `total ?: 0.0` default in
1271
+ the ZERO_DOLLAR_HOLD branch can misclassify on a transient NetSuite failure (open, unchanged).
1272
+ **`library` + `worker` must deploy together** or GroWrk and Adyen throw on their next run. (rgirish)
1273
+
1215
1274
  - 2026-09-03 — **By-reference shim wrote the cursor on the WRONG CLOCK → ITEM_RECEIPTS + ITEM_FULFILLMENTS
1216
1275
  re-matched the same record forever (fixed, `library` only).** Follow-on to the same-day by-reference
1217
1276
  fix below: the shims set `lastModifiedDate` from the REST GET (UTC), `date()`-formatted in
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-03
10
10
  owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -380,6 +380,14 @@ across clients, establish **which app** the missing control is in. Detail:
380
380
  [Surface Resolver](surface-resolver.md) and
381
381
  [toga25-supply side navigation](../../toga25-supply/features/side-navigation-and-default-route.md).
382
382
 
383
+ > **Narrow scope precisely: the FLAGS are inert, the ENDPOINT is not gone.** "Dead config" above is
384
+ > about the `navigation-*` `AclActions` flags having no effect on the 2.5 UI — that stands, verified
385
+ > prod-wide. It is **not** a claim that `toga25-supply` has stopped calling `Page::meta`: verified
386
+ > 2026-09-03, five of its view models still call `GET /v2/pages/meta` (nothing in `src/` reads a
387
+ > field off the result). So a change to `Page::meta`'s request path can still reach the 2.5 app even
388
+ > though its ACL flags cannot reach the 2.5 nav. Detail:
389
+ > [Surface Frontend](../../toga25-supply/features/surface-frontend.md).
390
+
383
391
  Worked example (Quad, 2026-08-26): the vendor-items **Add and Edit buttons both** read
384
392
  `acl['vendor-items']['navigation-edit-on-vendor-items']` (AclActions id 7). `Client_Quad` had a row
385
393
  for id 5 only, so the page was reachable but button-less, even though record 19 already granted role
@@ -598,6 +606,10 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
598
606
  and every repo is on the **same branch** so the generated model matches the DB.
599
607
 
600
608
  ## Change history
609
+ - **2026-09-03** — Scoped the "dead config in the surface-driven 2.5 app" claim: the `navigation-*`
610
+ **flags** are inert (unchanged), but `toga25-supply` still **calls `GET /v2/pages/meta`** from five
611
+ view models, so `Page::meta` is not out of that app's request path. Additive clarification only.
612
+ (apeterson)
601
613
  - **2026-09-01** — Four additions from a `PurchaseOrderItems._qty*` grant investigation.
602
614
  (1) **Read `Core.Records.aclDatabase` FIRST** — both databases carry an `AclFieldPermissions`
603
615
  table, so checking Core for a `CLIENT`-acl record (here `purchase-order-items`, record 18) returns
@@ -5,16 +5,16 @@
5
5
  | [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/providers/TalosStreamProvider.tsx |
6
6
  | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.test.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalFlowDetailInputs.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASS/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASSCANADA/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
7
7
  | [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page — fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
8
- | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
8
+ | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/resolveRole.ts, toga25-supply/src/globalFieldsConfig/clientLoginFields.json, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
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/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql |
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [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/TransferOrders/viewModel/useTransferOrdersPageViewModel.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 |
12
12
  | [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/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, 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/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
13
13
  | [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
14
14
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
15
- | [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/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, 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/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.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/hooks/useSalesOrderVip.ts, 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, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
15
+ | [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/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, 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/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.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/hooks/useSalesOrderVip.ts, 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, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
16
16
  | [Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/hooks/useTalosThreads.ts, toga25-supply/src/hooks/useTalosThreads.test.ts, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/hooks/useTalosSurface.test.ts, toga25-supply/src/App.tsx, toga25-supply/db-migrations/PLAYBOOK.md, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
17
17
  | [Transfer Orders page (TableView → Core surfaces → React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen — list + read-only record modal — built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/pages/TransferOrders/TransferOrder.module.css, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
18
18
  | [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, toga25-supply/.env.development |
19
- | [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 |
19
+ | [Cypress Testing Harness (multi-tenant, fully stubbed)](workflows/cypress-testing.md) | The Cypress harness for `toga25-supply`: an **e2e** project and a **component** project, plus a stub layer that answers **every** `/v2` call so no test touches | toga25-supply/cypress.config.ts, toga25-supply/cypress/README.md, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/tenants.ts, toga25-supply/cypress/support/api/stubApi.ts, toga25-supply/cypress/support/api/envelope.ts, toga25-supply/cypress/support/api/users.ts, toga25-supply/cypress/support/api/surfaces.ts, toga25-supply/cypress/support/api/session.ts, toga25-supply/cypress/e2e/login.cy.ts, toga25-supply/cypress/e2e/emptyNavigation.cy.ts, toga25-supply/cypress/component/harness.cy.tsx, toga25-supply/cypress/fixtures/tenants/COMPASS/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/COMPASSCANADA/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/NYCHH/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/QUAD/surfaces/navigation.json, toga25-supply/scripts/cypress.mjs, toga25-supply/tsconfig.node.json, toga25-supply/package.json |
20
20
  | [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |
@@ -19,6 +19,7 @@ files:
19
19
  - toga25-supply/src/hooks/useTableCellInteractions.ts
20
20
  - toga25-supply/src/providers/TalosStreamProvider.tsx
21
21
  related:
22
+ - workflows/cypress-testing.md
22
23
  - features/talos-integration.md
23
24
  - features/client-configurable-fields.md
24
25
  - features/meta-driven-table-data.md
@@ -70,6 +71,18 @@ toga-blox table/modal templates.
70
71
  - **UI library:** `@agilant/toga-blox` provides `PrimaryTable*Layout`, `TableRecordModal`,
71
72
  `BaseInput`, `BaseButton`, `DetailSection`, the `useTableData` hook, FontAwesome helpers, etc.
72
73
 
74
+ ### Testing
75
+
76
+ Two harnesses, both fully stubbed — **no front-end test ever talks to a backend**:
77
+
78
+ - **Vitest + React Testing Library** (`npm test`) for logic: hooks, view models, auth context,
79
+ table URL-state, error classification.
80
+ - **Cypress** (`npm run cy:e2e:<tenant>`, `npm run cy:ct`) for the **tenant x role x feature**
81
+ matrix, driven by per-tenant surface **fixtures** rather than real logins or a seeded database.
82
+
83
+ The full design, run commands, guardrails and traps live in
84
+ [cypress-testing](workflows/cypress-testing.md) — read that before writing or changing a test.
85
+
73
86
  ## Directory layout (`src/`)
74
87
 
75
88
  - `pages/<Entity>/` — page component (thin) + `viewModel/` (the `use*PageViewModel` hook) +
@@ -6,10 +6,12 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-01
9
+ updated: 2026-09-03
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/fieldsConfig/index.ts
13
+ - toga25-supply/src/fieldsConfig/resolveRole.ts
14
+ - toga25-supply/src/globalFieldsConfig/clientLoginFields.json
13
15
  - toga25-supply/src/fieldsConfig/useClientFields.ts
14
16
  - toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/
15
17
  - toga25-supply/src/pages/Inventory/viewModel/FIELDS/
@@ -61,6 +63,37 @@ and the `useClientFields()` hook.
61
63
  const tenantFields = clientFields.salesOrdersPageFields; // your key
62
64
  ```
63
65
 
66
+ ### How `resolveRole` decides the role (verified 2026-09-03)
67
+
68
+ `src/fieldsConfig/resolveRole.ts` takes `(clientSlug, user)` and reads **boolean-ish string flags
69
+ off the `/v2/users` record** — `"1"` means set. The flag set and the returned role names **differ
70
+ per client**, and the checks are ordered, so the first match wins:
71
+
72
+ | clientSlug | flag (in order) | role returned |
73
+ |---|---|---|
74
+ | `COMPASS`, `COMPASSCANADA` | `_isAdmin` | `ADMIN` |
75
+ | | `_isSuperAdmin` | `SUPERUSER` |
76
+ | | `_isSupervisor` | `MANAGER` |
77
+ | | none set | `USER` |
78
+ | `QUAD` | `_isGlobalAdmin` | `GLOBALADMIN` |
79
+ | | `_isBuyer` | `BUYER` |
80
+ | | `_isItShopper` | `ITSHOPPER` |
81
+ | | none set | `USER` |
82
+ | any other client | — | `DEFAULT` |
83
+
84
+ Two things follow that catch people out:
85
+
86
+ - **Which flags are even requested is per-client**, declared in
87
+ `src/globalFieldsConfig/clientLoginFields.json` (`userApiFields`). A flag the client does not
88
+ request is never fetched, so `resolveRole` cannot see it. `NYCHH`, for example, has no entry, so
89
+ no role flags are requested and the role is always `DEFAULT`.
90
+ - **An ungranted or unrequested field comes back ABSENT, not null**, from the API (see the
91
+ [ACL permission chain](../../_underscore/features/acl-permission-chain.md)) — so `user._isAdmin`
92
+ being `undefined` is the normal shape, not a bug.
93
+
94
+ The Cypress harness mirrors this table as `roleFlags` per tenant in `cypress/support/tenants.ts`;
95
+ keep the two in step. See [Cypress testing harness](../workflows/cypress-testing.md).
96
+
64
97
  ### By client × role vs. by client only
65
98
 
66
99
  - **Client × role** (e.g. `salesOrdersPageFields` — ADMIN vs MANAGER see different filter buttons):
@@ -168,6 +201,11 @@ modal's chrome from the same per-client JSON, not from the component:
168
201
  `vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
169
202
 
170
203
  ## Change history
204
+ - 2026-09-03 — Documented the actual `resolveRole` flag → role table (COMPASS/COMPASSCANADA:
205
+ `_isAdmin`/`_isSuperAdmin`/`_isSupervisor`, in that order, else `USER`; QUAD:
206
+ `_isGlobalAdmin`/`_isBuyer`/`_isItShopper`, else `USER`; every other client `DEFAULT`) and the
207
+ fact that which flags are requested is itself per-client, via
208
+ `globalFieldsConfig/clientLoginFields.json` — so NYCHH is always `DEFAULT`. (apeterson)
171
209
  - 2026-07-20 — Extracted the **API-fetch projection** out of these presentation JSON blobs into a
172
210
  new per-page `apiFields.json` + `useClientApiFields` mechanism (see
173
211
  [client-api-fetch-fields](client-api-fetch-fields.md)). The `salesOrderDetailsConfig` block was
@@ -28,6 +28,11 @@ files:
28
28
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
29
29
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx
30
30
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
31
+ - toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
32
+ - toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx
33
+ - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
34
+ - toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx
35
+ - toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx
31
36
  - toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
32
37
  - toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx
33
38
  - toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx
@@ -78,6 +83,7 @@ files:
78
83
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts
79
84
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
80
85
  related:
86
+ - ../workflows/cypress-testing.md
81
87
  - ../../_underscore/features/surface-resolver.md
82
88
  - ../../_underscore/features/sales-order-denial-reason.md
83
89
  - meta-driven-table-data.md
@@ -965,7 +971,43 @@ Final: the parameterized FE map above.
965
971
  button except **View Log** renders and does nothing on click. Whether the SR modal gets its own
966
972
  actions surface is **still an open decision**, not an oversight.
967
973
 
974
+ ## ⚠ The app still calls the legacy `GET /v2/pages/meta`
975
+
976
+ Verified 2026-09-03. The Surface layer replaced `Page::meta` **as the source of nav/ACL/label
977
+ config** — that part of the story holds — but the **endpoint is still being called**. Five view
978
+ models call blox's `useFetchPageMeta` → `getPageMeta` → `apiGet("/pages/meta", {page, app})`:
979
+
980
+ - `src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx` (fires **twice** per landing-page load)
981
+ - `src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx`
982
+ - `src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx`
983
+ - `src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx`
984
+ - `src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx`
985
+
986
+ **Nothing in `src/` reads any field off the result.** The only references to `pageMeta.acl` are
987
+ *comments* in `src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts` noting it was replaced. So
988
+ these are almost certainly dead weight left over from the Surface migration — but until they are
989
+ removed, the app still makes the call, and a request-level change to `/pages/meta` can still hit
990
+ this app.
991
+
992
+ Two consequences:
993
+
994
+ - Any test/stub layer must answer `/pages/meta` or the calls fall through (this is how it was
995
+ found — the Cypress fall-through guardrail flagged it on its **first** run, not a code read).
996
+ See [Cypress testing harness](../workflows/cypress-testing.md).
997
+ - **Removing the five calls is separate, unstarted work.** Stubbing them is not the fix.
998
+
999
+ Related: [legacy page meta](../../_underscore/features/page-meta-context-field-settings.md) and the
1000
+ [ACL permission chain](../../_underscore/features/acl-permission-chain.md) (whose "dead config"
1001
+ claim is about the `navigation-*` **flags** being inert — still true — not about the endpoint being
1002
+ unused).
1003
+
968
1004
  ## Change history
1005
+ - 2026-09-03 — Recorded that the 2.5 app **still calls `GET /v2/pages/meta`** from five view
1006
+ models (SalesOrders — twice per load — VendorItems, Inventory, Bundles, ItemFulfillmentModal)
1007
+ even though nothing in `src/` reads a field off the result. Found by the new Cypress
1008
+ fall-through guardrail. Qualifies, does not overturn, the "Page::meta is replaced" claim: the
1009
+ nav/ACL config really is surface-driven; the endpoint call is leftover. (apeterson)
1010
+
969
1011
  - 2026-09-03 — **Built the stacked-Total `+` joiner in the Approval Workflow modal**
970
1012
  (`withStackedCurrencyJoiner`, new `RecordApprovalModal/helpers/stackedCurrencyJoiner.ts` + 13
971
1013
  vitest cases; one-line wiring in `RecordApprovalModalLayout` `renderField`, workflow modal only —