toga-ai 1.0.466 → 1.0.468

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,7 +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
+ | [isFulfillable from NetSuite during Item Sync (Phase 1)](features/netsuite-item-isfulfillable-sync.md) | This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite `isfulfillable` flag during item sync and stamping it onto the **Agilant | library/app/netsuite.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php |
13
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 |
14
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 |
15
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 |
@@ -6,15 +6,17 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
9
+ updated: 2026-07-28
10
10
  owners: [bala]
11
11
  files:
12
12
  - library/app/netsuite.php
13
13
  - library/app/api/toga2.php
14
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
15
  - worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php
15
16
  related:
16
17
  - toga2-api-client-and-bridge.md
17
18
  - ../../worker/features/netsuite-togasupply-per-client-sync.md
19
+ - ../../worker/workflows/isfulfillable-multi-client-backfill.md
18
20
  - ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
19
21
  ---
20
22
 
@@ -26,24 +28,43 @@ item** in the 2.0 platform. The client-facing copy is reached separately by the
26
28
  which walks the supply chain up from the source item. Only the source item is stamped here.
27
29
 
28
30
  ## 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).
31
+ - `library/app/netsuite.php`
32
+ - `App_NetSuite::getItemIsFulfillable($itemInternalId): bool|null` — a SOAP `ItemSearchBasic` by
33
+ internal id; returns the boolean (or `null` when unknown). Mirrors `getItemIsSerialized`.
34
+ - `App_NetSuite::getItemSerializedAndFulfillable($itemInternalId)` — **combined lookup** returning
35
+ **both** flags from **one** `ItemSearchBasic` (serialized via `instanceof SerializedInventoryItem`,
36
+ fulfillable via the record's `isFulfillable`). `getCreateItem` now calls this once instead of the
37
+ two separate SOAP searches (`getItemIsSerialized` + `getItemIsFulfillable`) — halves the per-line
38
+ NetSuite round-trips now that the sync reads fulfillability for every line (see refresh below).
39
+ - `library/app/api/toga2.php` — three item-creation paths now stamp `isFulfillable`:
40
+ - `getCreateItem` (~line 4364) — reads both flags via `getItemSerializedAndFulfillable` and, when
41
+ fulfillable is **non-null**, stamps `$payload['isFulfillable']` on the create/update PUT|POST. It
42
+ now also **refreshes the flag on EXISTING items** when NetSuite's value differs from the stored
43
+ value (not only on create), mirroring how the serialized flag works. Because the PUT is
44
+ diff-only, a re-sync with an unchanged value is a no-op. NetSuite internal id is resolved from the
45
+ order line or part number (`getItemInternalIdFromPartNumber` / `getItemGroupInternalIdFromPartNumber`);
46
+ stores `c_netsuiteInternalItemId`.
47
+ - `syncItemFulfillmentFromNetsuite` (~line 3562) and `syncInventoryAdjustmentFromNetsuite`
48
+ (~line 3882) — the two **other** direct `/items` creation paths now also stamp `isFulfillable` on
49
+ create. Previously only `getCreateItem` did, so items first created via these flows stayed **NULL**.
50
+ - `worker/crons/toga2/netsuite/common_sync_togasupply.php` — `isFulfillable` was added to the bulk
51
+ `/items` GET field list so the existing-item refresh has the stored value to compare NetSuite's
52
+ against (without it, the refresh has nothing to diff).
53
+ - `worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php` — one-time Compass backfill (see below).
54
+ The later **multi-client** catch-up lives in its own worker workflow doc
55
+ ([isFulfillable multi-client backfill](../../worker/workflows/isfulfillable-multi-client-backfill.md)).
39
56
 
40
57
  ## How it works
41
58
  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.
59
+ 2. `getItemSerializedAndFulfillable` SOAP-searches the item **once** and returns both the serialized
60
+ and fulfillable flags.
61
+ 3. If fulfillable is non-null, `getCreateItem` adds `isFulfillable` to the item PUT/POST payload to
62
+ api2 — on **create**, and on **update** whenever the stored value differs from NetSuite's (the
63
+ existing-item refresh). api2's Items interceptor then propagates it up-chain (Phase 2). The two
64
+ other creation paths (`syncItemFulfillmentFromNetsuite`, `syncInventoryAdjustmentFromNetsuite`)
65
+ also stamp it on create.
66
+ 4. Going forward the **daily sync** handles new items and keeps existing items current (the refresh);
67
+ items that predate the feature are caught up by the one-time backfills.
47
68
 
48
69
  ### One-time backfill (July 5+ orders)
49
70
  `backfill_isfulfillable_jul5.php` catches up Agilant items on SalesOrders since **2026-07-05**:
@@ -85,6 +106,14 @@ nearly everything resolves to `fulfillable = 1`.**
85
106
  in `Client_Compass.Apis` (name `Agilant`) — never reproduce the secret value.
86
107
 
87
108
  ## Change history
109
+ - 2026-07-28 — Code-review hardening: (1) `getCreateItem` now **refreshes `isFulfillable` on
110
+ existing items** when NetSuite's value differs (not create-only), mirroring the serialized flag;
111
+ diff-only PUT keeps re-syncs no-op. (2) The two other `/items` creation paths
112
+ (`syncItemFulfillmentFromNetsuite`, `syncInventoryAdjustmentFromNetsuite`) now also stamp the flag
113
+ on create — they previously left it NULL. (3) Added `isFulfillable` to the worker sync's bulk
114
+ `/items` GET field list so the refresh has a stored value to diff. (4) New combined
115
+ `App_NetSuite::getItemSerializedAndFulfillable` returns both flags from one `ItemSearchBasic`, so
116
+ `getCreateItem` no longer makes two SOAP searches per line. (bala)
88
117
  - 2026-07-20 — Added `App_NetSuite::getItemIsFulfillable` (SOAP `ItemSearchBasic`, mirrors
89
118
  `getItemIsSerialized`) and wired `getCreateItem` to stamp `isFulfillable` on the item PUT/POST to
90
119
  api2 when non-null (internal id from order line or part-number lookup; stores
@@ -100,3 +129,5 @@ nearly everything resolves to `fulfillable = 1`.**
100
129
  - [NetSuite → TOGa Supply Per-Client Sync](../../worker/features/netsuite-togasupply-per-client-sync.md)
101
130
  — the sync engine this item flag rides in.
102
131
  - [Phase 2 — isFulfillable propagation up the SO↔PO chain (2.0)](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md).
132
+ - [isFulfillable multi-client backfill (worker)](../../worker/workflows/isfulfillable-multi-client-backfill.md)
133
+ — the cross-client catch-up cron and its client-DB / catalog-matching gotchas.
@@ -10,4 +10,5 @@
10
10
  | [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
11
11
  | [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
12
12
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
13
+ | [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill that catches up `Items.isFulfillable` on **existing** items across **all 17 togasupply clients** (AIG, Broward Sheriff, Canon, Endeavor Health | worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, library/app/api/toga2.php |
13
14
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: isFulfillable Multi-Client Backfill (all togasupply clients)
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-07-28
10
+ owners: [bala]
11
+ files:
12
+ - worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php
13
+ - library/app/api/toga2.php
14
+ related:
15
+ - ../features/netsuite-togasupply-per-client-sync.md
16
+ - ../../library/features/netsuite-item-isfulfillable-sync.md
17
+ - ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
18
+ - ../../../clients/compass-usa/profile.md
19
+ ---
20
+
21
+ ## Summary
22
+ One-time backfill that catches up `Items.isFulfillable` on **existing** items across **all 17
23
+ togasupply clients** (AIG, Broward Sheriff, Canon, Endeavor Health, ERAU, GroWrk, NYC Health +
24
+ Hospitals, Masonite, Miami-Dade, NCCI, Prudential, Quad, SBA, SHRSS, S&P Global, Trividia Health,
25
+ and Compass). It is the multi-client successor to the Compass-only `backfill_isfulfillable_jul5.php`
26
+ (see the [Phase 1 library doc](../../library/features/netsuite-item-isfulfillable-sync.md)). Like that
27
+ one, it re-PUTs each Agilant **source** item to api2 `/items` so the **api2 interceptor does the
28
+ recursive up-chain propagation** — it never writes the DB directly and never walks the chain itself.
29
+
30
+ ## Steps (how the cron runs)
31
+ 1. **Scope each client's Agilant-catalog items directly from that client's DB** — NOT via api2 —
32
+ keyed by `c_netsuiteInternalItemId`. Direct DB is required because that field is not an api2 GET
33
+ field (see gotchas). The Agilant catalog is matched by **`Catalogs.name = 'Agilant'`**, not by id.
34
+ 2. **Read `isFulfillable` from NetSuite by internal id** via SuiteQL `WHERE id IN (...)`, **cached
35
+ across clients** so a shared internal id is fetched once for the whole run.
36
+ 3. **PUT each source item to api2 `/items`** (`App_Api_Toga2::send`). The Items interceptor then
37
+ propagates the value up the SO↔PO chain onto every client-facing item (Phase 2).
38
+ 4. **Per-client scoping rule:**
39
+ - **Compass** — limited to orders since a cutoff (~**2026-06-20**); it was already partly
40
+ backfilled and only orders after that had correct chain linkage to propagate up to the client
41
+ items.
42
+ - **Every other client** — takes **ALL** its Agilant items (never previously backfilled).
43
+ 5. **Resilience:** clients whose DB read fails are **skipped and reported**, not fatal. Supports
44
+ `DRY_RUN`, `DB_HOST_PREFERENCE` (`localhost` / `writer.client` / `sandbox-dev`), and
45
+ `API_ENDPOINT_OVERRIDE` (the **9th arg** of `App_Api_Toga2::send`) so the run can target prod or a
46
+ beta/sandbox endpoint.
47
+
48
+ ## Gotchas / known issues
49
+ - **Agilant catalog is matched by NAME, not id.** `Catalogs.name = 'Agilant'` is stable across every
50
+ client, but the `catalogId` **differs per client** — **Quad = 2, all other clients = 1** (verified
51
+ across all 17 client DBs). Hardcoding `catalogId = 2` would silently hit only Quad and miss the
52
+ other 16. Always resolve the catalog by name.
53
+ - **`c_netsuiteInternalItemId` is NOT an api2 GET field** — there is no `Core.RecordField` for it, so
54
+ it can only be read by direct DB query. That is why the backfill scopes items via direct DB reads
55
+ rather than an api2 `/items` GET.
56
+ - **Client DB names don't always follow the client identifier.** Compass's identifier is
57
+ `Compass_Usa` but its database is **`Client_Compass`**. All client DBs live on **one cluster**
58
+ (prod `prod-client`), so a single host/user/pass with a swapped **database name** reaches them all.
59
+ - **⚠ Environment must be fully deployed, or the backfill can't propagate.** PUTting `isFulfillable`
60
+ to `api.beta.togahub.com` returns **EV-9 "no write permission"** — the field exists but the write
61
+ ACL was never deployed there (only `dev-sandbox` had field + interceptor + ACL). The Core
62
+ RecordField, the `ApiPayloadInterceptors` rows, **and** the client column + write ACL must all be
63
+ present on the **same** environment, and the run must scope from that same cluster. Deploy order:
64
+ dbchanges (field/ACL/interceptors) before running the backfill. See the
65
+ [Phase 2 deploy gotchas](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md).
66
+ - **🔐 Credentials.** The cron reuses the per-client api uuids/secrets already embedded in the
67
+ `sync_togasupply_*` crons; the api secrets live in each `Client_<client>.Apis` (name `Agilant`) —
68
+ never reproduce a secret value in code or docs.
69
+
70
+ ## Run status
71
+ - 2026-07-28 — Ran across all NetSuite/togasupply clients. Compass scoped to orders after
72
+ ~2026-06-20 (only those had correct chain linkage to propagate up to the client items); every other
73
+ client backfilled from all its Agilant items.
74
+
75
+ ## Change history
76
+ - 2026-07-28 — Built and ran the multi-client `backfill_isfulfillable_all_clients.php`: direct
77
+ client-DB scoping of Agilant-catalog items (matched by `Catalogs.name`, not id) keyed by
78
+ `c_netsuiteInternalItemId`, NetSuite reads by internal id (cached across clients), re-PUT to api2 so
79
+ the interceptor recurses. Per-client scope (Compass ~post-2026-06-20, all others full), DRY_RUN /
80
+ DB_HOST_PREFERENCE / API_ENDPOINT_OVERRIDE knobs, failed-DB clients skipped + reported. Recorded the
81
+ catalog-by-name (Quad catalogId=2), `c_netsuiteInternalItemId`-not-a-GET-field, Compass DB-name, and
82
+ beta EV-9 deploy-gap gotchas. (bala)
83
+
84
+ ## Related docs
85
+ - [isFulfillable from NetSuite during item sync (Phase 1, library)](../../library/features/netsuite-item-isfulfillable-sync.md)
86
+ — where the value is sourced and the earlier Compass-only backfill.
87
+ - [isFulfillable propagation up the SO↔PO chain (Phase 2, _underscore)](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md)
88
+ — the api2 interceptor this backfill relies on to do the recursion.
89
+ - [NetSuite → TOGa Supply Per-Client Sync](../features/netsuite-togasupply-per-client-sync.md)
90
+ — the steady-state sync whose credentials/config this backfill reuses.
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
9
+ updated: 2026-07-28
10
10
  owners: [bala]
11
11
  files:
12
12
  - _underscore/Model/Client/Item.php
@@ -50,7 +50,14 @@ same bridge topology, walked for a different payload (a scalar flag rather than
50
50
  one-time backfill) send that field, so day-to-day item edits skip the walk entirely.
51
51
  2. **Load the source item** by uuid → `id` + `isFulfillable`. If the value is **NULL**, return
52
52
  (nothing authoritative to propagate).
53
- 3. **Walk the chain UP** with a `WITH RECURSIVE ChainSalesOrderItems` CTE:
53
+ - **Writer-routed (read-your-writes).** Both this source read **and** the chain-walk CTE below
54
+ call `->setisReadHostEnabled(false)` so they execute on the **writer**, not a read replica.
55
+ `_Query` routes SELECTs to a read replica by default (`Query.php` ~line 273) — a separate
56
+ connection that cannot see this request's still-open write transaction and can lag replication,
57
+ so a replica read could return the *pre-write* value (or NULL for a brand-new item) and
58
+ propagate the wrong value or skip. The UPDATE was already writer-routed; now read + update share
59
+ the one request transaction.
60
+ 3. **Walk the chain UP** with a `WITH RECURSIVE ChainSalesOrderItems` CTE (also writer-routed, see above):
54
61
  - **Anchor:** `SalesOrderItems WHERE itemId = <sourceItemId>`.
55
62
  - **Recursive step:** join `PurchaseOrderItems_SalesOrderItems` (PSO) then
56
63
  `SalesOrderItems_PurchaseOrderItems` (SPO) to climb one tier
@@ -93,6 +100,20 @@ uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfilla
93
100
  - Engine itself is shared `_Model_Client_Item` logic; any client with the schema present gets it.
94
101
 
95
102
  ## Gotchas / known issues
103
+ - **⚠ Propagation reads MUST be writer-routed.** `_Query` SELECTs default to a read replica
104
+ (`Query.php` ~line 273), a separate connection that can't see the current request's open write
105
+ transaction and may lag replication. The interceptor runs inside the write request, so a
106
+ replica-routed read here can see the **pre-write** value (or NULL on a just-created item) and
107
+ propagate wrongly or skip. Both SELECTs in `propagateFulfillableAcrossChain` therefore set
108
+ `setisReadHostEnabled(false)`. Any future read added to an interceptor that must see the same
109
+ request's writes needs the same treatment.
110
+ - **⚠ Beta deploy gap — EV-9 "no write permission" (write ACL not deployed).** PUTting
111
+ `isFulfillable` to `api.beta.togahub.com` returns **EV-9**: the field *exists* there but its write
112
+ ACL was never deployed (only `dev-sandbox` had the full field + interceptor + ACL from earlier
113
+ work). The three pieces — Core RecordField, `ApiPayloadInterceptors` rows, **and** the client
114
+ column + write ACL — must all be present on the **same** environment (and a backfill run must scope
115
+ from that same cluster) or the write is rejected and nothing propagates. Reinforces the deploy-order
116
+ dependency: dbchanges (field + ACL + interceptors) before the code/backfill that writes the field.
96
117
  - **⚠ Interceptors are DB-driven.** The `postPost`/`postPut` PHP does nothing without the
97
118
  `ApiPayloadInterceptors` rows in the **target env's Core DB**. Missing rows → the hook never
98
119
  fires **and** api2 EV-8's the unknown `isFulfillable` field, **403-ing the whole request** (not
@@ -108,6 +129,12 @@ uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfilla
108
129
  vs. using `itemtype` to hide services — is **pending product confirmation**.
109
130
 
110
131
  ## Change history
132
+ - 2026-07-28 — Fixed a **read-replica routing** bug: `propagateFulfillableAcrossChain`'s source-item
133
+ read and the recursive chain-walk CTE now `setisReadHostEnabled(false)` so they run on the writer
134
+ and see the request's own open write transaction (read-your-writes) — a replica read could return
135
+ the pre-write value or NULL and propagate wrongly/skip. Recorded the **beta EV-9 write-ACL deploy
136
+ gap** (field present but write ACL undeployed on `api.beta.togahub.com`; all three deploy pieces
137
+ must land on the same env). (bala)
111
138
  - 2026-07-20 — Built the isFulfillable up-chain propagation engine:
112
139
  `propagateFulfillableAcrossChain` + the payload-gated `postPost`/`postPut` interceptor on Items
113
140
  (recursive SOI↔POI CTE, diff-only `UPDATE`), plus the Compass `parent::` fix so the price-override
@@ -6,8 +6,9 @@
6
6
  | [Cart Bundle Submission & the bundleUuid Identity Contract](features/cart-bundle-submission-and-identity.md) | How cart **bundles** (kits) are turned into `SalesOrderItems` when a cart is submitted or an existing order is edited, and the **identity-field contract** every | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts, src/utils/formatSalesOrderBundlesFromApi.ts, src/stores/useCartStoreZu.ts, src/pages/OrderDetails/helpers/formatSalesOrderDataFromLocalStorage.ts, src/pages/OrderDetails/view/components/OrderItems.tsx |
7
7
  | [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
8
8
  | [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/pages/Cart/view/cartForm/CartFormSection.tsx, src/pages/Cart/view/cartForm/CartFormRenderer.tsx, src/pages/Cart/view/EditCart.tsx, src/pages/Cart/view/EditOrder.tsx, src/pages/Cart/viewModel/useEditOrderOrEditCartViewModel.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts, src/hooks/useAssignClientFields.ts |
9
- | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/pages/Account/view/MySettingsView.tsx, src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx, src/components/NavIcons/NavIconItem.tsx, src/components/Submenus/AlertSubmenu.tsx, src/components/Submenus/types.ts, src/pages/Account/AccountPage.tsx, src/pages/Account/view/MyOrdersView.tsx, src/pages/GetSupport/GetSupportPage.tsx, src/pages/GetSupport/viewModel/useGetSupportViewModel.ts, src/queries/queries.ts |
9
+ | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/pages/Account/view/MySettingsView.tsx, src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx, src/components/NavIcons/NavIconItem.tsx, src/components/Submenus/AlertSubmenu.tsx, src/components/Submenus/types.ts, src/pages/Account/AccountPage.tsx, src/pages/Account/view/MyOrdersView.tsx, src/pages/GetSupport/GetSupportPage.tsx, src/pages/GetSupport/viewModel/useGetSupportViewModel.ts, src/queries/queries.ts, src/App.tsx, src/pages/Filter/FilterPage.tsx, src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json |
10
10
  | [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh | toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/viewModel/FIELDS/shared/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/CartPage.tsx |
11
+ | [Filter / Search-Results Page & the Two Search Entry Points](features/filter-search-results-page.md) | The storefront has **two distinct search entry points that render the same card component through completely different code paths and different FIELDS files**. | src/pages/Filter/FilterPage.tsx, src/pages/Filter/viewModel/useFilterViewModel.ts, src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json, src/pages/Filter/viewModel/FIELDS/COMPASSCANADA/FRENCH/USER/FILTERPAGEFIELDS.json, src/pages/Filter/viewModel/FIELDS/QUAD/ENGLISH/BUYER/FILTERPAGEFIELDS.json, src/components/Header/Header.tsx, src/pages/Home/view/components/BundlesSection.tsx, src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/USER/HOMEPAGEFIELDS.json, src/components/Cards/BundleViewCard.tsx, src/utils/renderBadge.tsx, src/hooks/useAssignClientFields.ts |
11
12
  | [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. | src/themeConfig/themes.json, src/themeConfig/ThemeContext.tsx, src/themeConfig/types.ts, src/components/ThemeSwitcher/ThemeSwitcher.tsx, src/components/AuthLayout/AuthLayout.tsx, src/api/axiosInstance.ts, src/contexts/AuthContext.tsx, tailwind.config.js |
12
13
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. | toga2-commerce/amplify.yml, toga2-commerce/.gitattributes, toga2-commerce/package.json, toga2-commerce/.github/workflows/sync-stage-environments.yml |
13
14
  | [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) | The Cypress **e2e** convention set for `toga2-commerce`, and the first **active** e2e coverage for the **Cart** page (`cartV2.cy.ts`, slice 1 — 12 tests, verifi | toga2-commerce/cypress/e2e/cartPage/cartV2.cy.ts, toga2-commerce/cypress/fixtures/cart/fetchSingleUserAdmin.json, toga2-commerce/cypress/fixtures/cart/fetchLocations.json, toga2-commerce/cypress/fixtures/cart/fetchUserShippingMethods.json, toga2-commerce/cypress/support/commands.ts, toga2-commerce/cypress/support/e2e.ts, toga2-commerce/src/pages/Cart/CartPage.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartFormSection.tsx, toga2-commerce/src/pages/Cart/view/cartTable/CartContentsTable.tsx, toga2-commerce/src/pages/Cart/view/cartTable/CartTableItem.tsx, toga2-commerce/src/components/Inputs/AdvancedInput.tsx, toga2-commerce/src/components/BaseButton/BaseButton.tsx |
@@ -6,8 +6,8 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-23
10
- owners: ["apeterson"]
9
+ updated: 2026-07-28
10
+ owners: ["apeterson", "tcox"]
11
11
  files:
12
12
  - src/main.tsx
13
13
  - src/App.tsx
@@ -26,6 +26,7 @@ related:
26
26
  - 2.0/apps/toga2-commerce/features/client-fields.md
27
27
  - 2.0/apps/toga2-commerce/workflows/amplify-build-and-deploy.md
28
28
  - 2.0/apps/api2/architecture.md
29
+ - 2.0/apps/toga2-commerce/features/filter-search-results-page.md
29
30
  ---
30
31
 
31
32
  ## Summary
@@ -236,11 +237,23 @@ generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `save
236
237
  - **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit
237
238
  invalidation for data that must refresh on navigation.
238
239
  - **24h `staleTime` + persisted cache** → users can see stale catalog/pricing; invalidate on the
239
- events that should bust it.
240
+ events that should bust it. **This also masks config/copy deploys:** `FIELDS` is served *through*
241
+ the `["clientFields", …]` query, so a returning user rehydrates the old labels from
242
+ `localStorage["commerce"]` and sees stale copy for up to 24h after a build that contains the new
243
+ JSON. Verify any FIELDS change after `localStorage.removeItem('commerce')` or a logout, never on a
244
+ warm browser. A build-version `buster` in `persistOptions` would fix this at deploy time but
245
+ invalidates every persisted query app-wide — **open team decision, not implemented.**
240
246
  - **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the
241
247
  first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS,
242
248
  theme → DEFAULT). Use the per-tenant dev scripts.
243
249
  - **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase
244
250
  (`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as
245
- lowercase `en`/`fr`. See the client-fields doc.
251
+ `en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"`
252
+ comparison silently never matches. See the client-fields doc.
253
+
254
+ ## Change history
255
+ - 2026-07-28 — Gotchas: recorded that the 24h persisted React Query cache also masks `FIELDS`
256
+ config/copy deploys (verify after clearing `localStorage["commerce"]`; `persistOptions.buster` is
257
+ an open team decision), and corrected the runtime language key to `fr-CA` requiring
258
+ `startsWith("fr")` (tcox)
246
259
  </content>
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-23
9
+ updated: 2026-07-28
10
10
  owners: ["apeterson", "tcox", "bala"]
11
11
  files:
12
12
  - src/pages/Account/view/MySettingsView.tsx
@@ -31,11 +31,15 @@ files:
31
31
  - src/pages/GetSupport/GetSupportPage.tsx
32
32
  - src/pages/GetSupport/viewModel/useGetSupportViewModel.ts
33
33
  - src/queries/queries.ts
34
+ - src/App.tsx
35
+ - src/pages/Filter/FilterPage.tsx
36
+ - src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json
34
37
  related:
35
38
  - 2.0/apps/toga2-commerce/architecture.md
36
39
  - 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
37
40
  - 2.0/apps/toga2-commerce/features/cart-notification-emails.md
38
41
  - 2.0/apps/toga2-commerce/features/cart-page-config-architecture.md
42
+ - 2.0/apps/toga2-commerce/features/filter-search-results-page.md
39
43
  - 2.0/apps/_underscore/features/persona-name-translation.md
40
44
  ---
41
45
 
@@ -226,6 +230,26 @@ Any code that branches on the current language must compare against **`"fr-CA"`*
226
230
  `language.startsWith("fr")`, **never `language === "fr"`** (that comparison silently fails and was the
227
231
  root cause of the Get Support image bug — see gotchas).
228
232
 
233
+ ### FIELDS ride the persisted React Query cache — config fixes can appear not to deploy
234
+
235
+ `FIELDS` is statically bundled, but it is **served through a React Query query**
236
+ (`["clientFields", clientName, language, role]`). `src/App.tsx` sets the QueryClient default
237
+ `staleTime` to **24h** and persists the entire cache to `localStorage` under the key **`"commerce"`**
238
+ via `createSyncStoragePersister` + `PersistQueryClientProvider`. Consequence: a **returning** user
239
+ rehydrates the *old* fields object from localStorage and keeps seeing **stale labels for up to 24
240
+ hours after a deploy**, even though the new JSON is in the shipped bundle.
241
+
242
+ This is the single most common false negative when verifying a FIELDS/copy change — QA reports "your
243
+ fix didn't ship" on a browser that simply replayed the persisted cache.
244
+
245
+ - **Clear it manually:** `localStorage.removeItem('commerce')` then refresh.
246
+ - **Logging out also clears it** — both `src/contexts/AuthContext.tsx` (logout path) and
247
+ `src/api/axiosInstance.ts` call `localStorage.removeItem("commerce")`.
248
+ - **Proper deploy-time fix — not implemented, team decision pending:** pass a build-version `buster`
249
+ string in `persistOptions` in `App.tsx`. It is one line, but it invalidates **every** persisted query
250
+ app-wide on each deploy (losing all warm cache, not just fields), so it needs a deliberate call
251
+ rather than a drive-by change.
252
+
229
253
  ## What's inside a FIELDS JSON
230
254
 
231
255
  Shape varies by page, but common forms:
@@ -322,6 +346,21 @@ on switch.
322
346
  untranslated on purpose (the dropdown *contents* are translated). It is not a localization gap.
323
347
  - **No fallback** in `FIELDS[client][language][role]` — a missing tenant/role/language combo returns
324
348
  `null` and the page renders empty. Keep all role variants in sync.
349
+ - **No fallback for an individual *key* either — and that failure is silent.** The "no fallback" rule
350
+ applies one level deeper than the section lookup: a key missing from an otherwise-present page JSON
351
+ just yields `undefined`, which the consuming component happily interpolates. Real symptoms shipped
352
+ 2026-07-28: `renderBadge(undefined)` rendered a structurally-present but **empty, uncolored,
353
+ icon-less** badge, and a card template `` `${count} ${label}s` `` rendered **"3 undefineds"**. Nothing
354
+ throws, nothing logs, TypeScript is clean (the lookups are optional-chained `any`). When adding a
355
+ card/label key, add it to **every** tenant × language × role file for that page — 12 for Compass
356
+ (COMPASS/ENGLISH × 4 roles + COMPASSCANADA/{ENGLISH,FRENCH} × 4), 3 for QUAD. See
357
+ [filter-search-results-page](filter-search-results-page.md).
358
+ - **Key names are per page, not global — the same UI element can have different key names in two
359
+ pages' JSON.** The kit badge is `bundleBadge` in `HOMEPAGEFIELDS` but `kitBadge` in
360
+ `FILTERPAGEFIELDS`. Never assume a label added for one page covers another; grep the consuming
361
+ component for the exact `fields?.<key>` it reads.
362
+ - **A stale persisted cache masks FIELDS deploys** — see the persisted-cache section above; verify
363
+ copy changes after `localStorage.removeItem('commerce')` or a logout, never on a warm browser.
325
364
  - Role is derived from string `"1"` flags on the user (`user?._isAdmin === "1"`), not booleans.
326
365
  - `getClientLoginFields` defaults to **COMPASS** for an unknown host, which can mask a
327
366
  misconfigured tenant in local dev.
@@ -339,6 +378,14 @@ on switch.
339
378
  slip. Do not "reconcile" the two lists.
340
379
 
341
380
  ## Change history
381
+ - 2026-07-28 — Documented two failure modes found while fixing the header-search kit card: (1) there is
382
+ **no fallback for an individual key** inside a present page JSON — a missing key silently yields
383
+ `undefined` (empty uncolored badge, `"3 undefineds"`), and key names differ per page
384
+ (`bundleBadge` in HOMEPAGEFIELDS vs `kitBadge` in FILTERPAGEFIELDS); (2) FIELDS are served *through*
385
+ the `["clientFields", …]` React Query, whose cache is persisted to `localStorage["commerce"]` with a
386
+ 24h `staleTime`, so returning users see **stale labels for up to 24h after a deploy** — clear with
387
+ `localStorage.removeItem('commerce')` or a logout. Recorded the pending team decision on adding a
388
+ build-version `buster` to `persistOptions` in `App.tsx`. (tcox)
342
389
  - 2026-07-23 — **Correction:** clarified that `LANGUAGE_SENSITIVE_QUERY_KEYS` alone does **not**
343
390
  refresh the persona name on language switch — `user._personaOptions` lives in the persisted Zustand
344
391
  `user` store (populated once at login), not a live React Query, so nothing in the allow-list owns it.
@@ -0,0 +1,150 @@
1
+ ---
2
+ title: Filter / Search-Results Page & the Two Search Entry Points
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-28
10
+ owners: ["tcox"]
11
+ files:
12
+ - src/pages/Filter/FilterPage.tsx
13
+ - src/pages/Filter/viewModel/useFilterViewModel.ts
14
+ - src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json
15
+ - src/pages/Filter/viewModel/FIELDS/COMPASSCANADA/FRENCH/USER/FILTERPAGEFIELDS.json
16
+ - src/pages/Filter/viewModel/FIELDS/QUAD/ENGLISH/BUYER/FILTERPAGEFIELDS.json
17
+ - src/components/Header/Header.tsx
18
+ - src/pages/Home/view/components/BundlesSection.tsx
19
+ - src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/USER/HOMEPAGEFIELDS.json
20
+ - src/components/Cards/BundleViewCard.tsx
21
+ - src/utils/renderBadge.tsx
22
+ - src/hooks/useAssignClientFields.ts
23
+ related:
24
+ - 2.0/apps/toga2-commerce/architecture.md
25
+ - 2.0/apps/toga2-commerce/features/client-fields.md
26
+ - 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
27
+ ---
28
+
29
+ ## Summary
30
+
31
+ The storefront has **two distinct search entry points that render the same card component through
32
+ completely different code paths and different FIELDS files**. That split is the single most important
33
+ fact about this page: a search-results defect almost always reproduces on **one** path only, and the
34
+ first diagnostic question is *"which search box did you use?"*
35
+
36
+ | Entry point | Component | Route | FIELDS section read |
37
+ |---|---|---|---|
38
+ | **Home page lower search bar** | `src/pages/Home/view/components/BundlesSection.tsx` | stays on `/home`, filters **in place** | `HOMEPAGEFIELDS` |
39
+ | **Header search bar** (global, every authenticated page) | `src/components/Header/Header.tsx` → `src/pages/Filter/FilterPage.tsx` | **navigates** to `/filter?search=<text>&category=all` | `FILTERPAGEFIELDS` |
40
+
41
+ Both then render the shared **`src/components/Cards/BundleViewCard.tsx`**. Because the two callers
42
+ populate that shared card's props from two different JSON files, the card can render correctly from
43
+ one entry point and be broken from the other — which is exactly the 2026-07-28 "3 undefineds" kit-card
44
+ bug (see Change history).
45
+
46
+ ## How it works
47
+
48
+ ### Header search → `/filter`
49
+
50
+ `Header.tsx` navigates to `` `/filter?search=${encodeURIComponent(inputValue)}` + "&category=all" ``.
51
+ `FilterPage.tsx` reads the query string, and `useFilterViewModel.ts` runs the catalog query and calls
52
+ `useAssignClientFields(fieldKey, language, user)` to resolve `FILTERPAGEFIELDS` for the current
53
+ tenant × language × role (see [client-fields](client-fields.md) for the resolver). `FilterPage`
54
+ `renderBundles` maps each result to a `BundleViewCard`.
55
+
56
+ ### Home search → in-place filter
57
+
58
+ The Home page's own lower search bar never leaves `/home`. `BundlesSection.tsx` reads
59
+ `HOMEPAGEFIELDS` (already loaded by the Home ViewModel) and renders the same `BundleViewCard`.
60
+
61
+ ### The shared card contract (`BundleViewCard`)
62
+
63
+ The card is a dumb presentational component — every label is a prop. Two props matter for kit cards:
64
+
65
+ - `includedItems` — the count.
66
+ - `includedItemsText` — the **singular** noun, e.g. `"included item"`.
67
+
68
+ The card composes them itself, in **two places** (mobile and desktop blocks, ~lines 112 and 191):
69
+
70
+ ```tsx
71
+ text={`${includedItems} ${includedItemsText}${includedItems == 1 ? "" : "s"}`}
72
+ ```
73
+
74
+ So a caller that forgets `includedItemsText` renders **"3 undefineds"** — the literal string
75
+ `undefined` plus the appended `s`. There is no default and no runtime guard.
76
+
77
+ ### Badges are keyed off the badge *text*
78
+
79
+ `src/utils/renderBadge.tsx` takes the resolved label string and derives **everything** from it:
80
+
81
+ - `badgeTextMapping` / `badgeTextMappingFrench` — display text (French is looked up by the **English**
82
+ key, e.g. `Kit → "Trousse"`), falling back to the raw string.
83
+ - `getBadgeClasses(badgeText)` — a `switch` on the text for background/foreground colors (it accepts
84
+ both English and French spellings: `"Kit"`, `"Trousse"`, `"Ensemble"`).
85
+ - `getBadgeIcon(badgeText)` — a `switch` on the text for the FontAwesome icon; only `"Kit"` and
86
+ `"VIP"` match.
87
+
88
+ `renderBadge(undefined, "")` therefore renders a **structurally present but empty and uncolored
89
+ badge**: no text, no icon, and the `default:` class branch (no color). Nothing throws and nothing
90
+ logs — the bug looks like a CSS problem and is actually a missing config key.
91
+
92
+ ### The badge key is named differently on each page
93
+
94
+ Same visual badge, two different FIELDS key names — do not assume one:
95
+
96
+ - Home / `HOMEPAGEFIELDS` → **`bundleBadge`** (`fields?.bundleBadge?.label`)
97
+ - Filter / `FILTERPAGEFIELDS` → **`kitBadge`** (`fields?.kitBadge?.label`)
98
+
99
+ ## Gotchas
100
+
101
+ - **A missing *key* inside a present FIELDS file fails silently.** The known "no fallback" rule for
102
+ `FIELDS[tenant][language][role]` also applies one level deeper: an individual key absent from a
103
+ page's JSON yields `undefined`, which renders as an empty badge or `"N undefineds"`. Any card-label
104
+ key must exist in **every** tenant × language × role file for that page — 12 files for Compass
105
+ (COMPASS/ENGLISH × 4 roles, COMPASSCANADA/ENGLISH × 4, COMPASSCANADA/FRENCH × 4) and 3 for QUAD
106
+ (QUAD/ENGLISH/{GLOBALADMIN,BUYER,ITSHOPPER}).
107
+ - **Adding a label to one page's FIELDS does not cover the other page.** `HOMEPAGEFIELDS` and
108
+ `FILTERPAGEFIELDS` are independent files with independent key sets; a label added for the Home card
109
+ must be added again (possibly under a different key name) for the Filter card.
110
+ - **French pluralization is broken by design in the shared card.** `BundleViewCard` blindly appends
111
+ `"s"`, so `"articles inclus"` renders as **"3 articles incluss"** in fr-CA. This pre-dates the
112
+ Filter-page fix (the Compass Canada *home* page has always shown it) and the Filter page now
113
+ mirrors it for parity. The real fix is pre-pluralized per-language labels (or a singular/plural pair)
114
+ in the card contract — **follow-up ticket, not yet filed.**
115
+ - **French kit badge loses its icon.** The FRENCH JSONs store the label `"Trousse"`, so
116
+ `getBadgeClasses` still colors it but `getBadgeIcon` (which only matches `"Kit"`) returns `null` — no
117
+ BoxesStacked icon in French. Storing the English `"Kit"` in the French file would get both the icon
118
+ *and* auto-translated text via `badgeTextMappingFrench`. Current behavior matches the Home page, so
119
+ it was left alone; treat it as a known cosmetic gap, not a regression.
120
+ - **Persisted React Query cache hides FIELDS fixes for up to 24h after deploy.** FIELDS are served
121
+ *through* a query (`["clientFields", tenant, language, role]`) and the whole cache is persisted to
122
+ `localStorage["commerce"]` with a 24h `staleTime`, so a returning user rehydrates the **old** labels
123
+ even though the new JSON is in the bundle. When QA says "your config fix didn't ship", have them run
124
+ `localStorage.removeItem('commerce')` and refresh (logging out also clears it). See
125
+ [client-fields](client-fields.md).
126
+
127
+ ## Known open items
128
+
129
+ - **QUAD has the same missing-key bug, unfixed.** All three
130
+ `src/pages/Filter/viewModel/FIELDS/QUAD/ENGLISH/{GLOBALADMIN,BUYER,ITSHOPPER}/FILTERPAGEFIELDS.json`
131
+ still lack `kitBadge` and `includedItemsText`, so a header-search kit card on QUAD renders the empty
132
+ badge + `"3 undefineds"`. The fix is the identical two-entry JSON addition; it was left out because
133
+ the 2026-07-28 session was scoped to Compass.
134
+ - French pluralization (`"3 articles incluss"`) — needs a card-contract change, see gotchas.
135
+
136
+ ## Change history
137
+ - 2026-07-28 — Fixed header-search kit cards for Compass USA + Compass Canada. Two defects in the
138
+ Filter path: (1) `FilterPage.tsx` rendered `BundleViewCard` without passing `includedItemsText`
139
+ → `"3 undefineds"` (added `const includedItemsText = fields?.includedItemsText?.label;` beside the
140
+ existing `kitBadgeLabel` read and passed it through in `renderBundles`); (2) the `kitBadge` key was
141
+ missing from every `FILTERPAGEFIELDS.json` except COMPASSCANADA/ENGLISH/ADMIN, so
142
+ `renderBadge(undefined)` produced an empty uncolored badge. Added `kitBadge` ("Kit"/"Trousse") and
143
+ `includedItemsText` ("included item"/"articles inclus") to all **12** Compass FILTERPAGEFIELDS
144
+ files, copied from the working HOMEPAGEFIELDS labels so both search paths render identically. The
145
+ code change is a no-op for tenants whose config lacks the keys, so QUAD is unchanged (still buggy —
146
+ see Known open items). `npx tsc --noEmit` clean. (tcox)
147
+ - 2026-07-28 — Initial doc: recorded the two search entry points (Home in-place vs. header →
148
+ `/filter`), the shared `BundleViewCard` label contract, text-keyed badge rendering in
149
+ `renderBadge.tsx`, the `bundleBadge` vs `kitBadge` key-name split, and the silent-`undefined`
150
+ failure mode for a missing per-key FIELDS entry. (tcox)
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 14 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 16 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 17 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) — 10 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
11
11
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
@@ -28,7 +28,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
28
28
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
29
29
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
30
30
  - **ai-bdr** (AI-BDR) — 8 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
31
- - **toga2-commerce** (TOGa Commerce) — 10 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
31
+ - **toga2-commerce** (TOGa Commerce) — 11 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
32
32
  - **toga25-supply** (TOGa 2.5 Supply) — 11 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
33
33
  - **toga-blox** (TOGa Blox) — 8 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
34
34
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.466",
3
+ "version": "1.0.468",
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",