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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -1
- package/knowledge/1.0/apps/library/features/netsuite-item-isfulfillable-sync.md +47 -16
- package/knowledge/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/workflows/isfulfillable-multi-client-backfill.md +90 -0
- package/knowledge/2.0/apps/_underscore/features/fulfillable-item-propagation.md +29 -2
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +2 -1
- package/knowledge/2.0/apps/toga2-commerce/architecture.md +17 -4
- package/knowledge/2.0/apps/toga2-commerce/features/client-fields.md +48 -1
- package/knowledge/2.0/apps/toga2-commerce/features/filter-search-results-page.md +150 -0
- package/knowledge/INDEX.md +2 -2
- package/package.json +1 -1
|
@@ -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-
|
|
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`
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `
|
|
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. `
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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) —
|
|
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