toga-ai 1.0.1120 → 1.0.1122

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.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-10-19
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal, sking, bala]
11
11
  files:
12
12
  - worker/index.php
@@ -23,10 +23,13 @@ related:
23
23
  - ./features/oneuptime-worker-uptime-monitoring.md
24
24
  - ../../2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md
25
25
  - ../togadesk/features/email-to-ticket-intake.md
26
+ - ../worker1.5/architecture.md
26
27
  ---
27
28
 
28
29
  The 1.0 `App_` background-job tier (~550 CRON scripts); open for how crons are scheduled, how the 7-worker role election works, and its silent-death failure modes.
29
30
 
31
+ > **Moved:** the worker 1.0 jobs now run on one worker1.5 server on PHP 8.5 (TRUE-83078, commit 58dab19). For how crons run now, see [../worker1.5/architecture.md](../worker1.5/architecture.md).
32
+
30
33
  ## Summary
31
34
 
32
35
  `worker` is the legacy (**1.0** `App_` framework) **background-job tier**: ~550 standalone PHP CRON scripts under `crons/` for everything web apps must not do inline — notifications/reports, DB backups, third-party syncs (NetSuite, EDI partners, NYC DOE, ManageEngine, Syncro…), cloud-infrastructure maintenance, and TOGa / TOGaDesk / TOGa-2 client integrations.
@@ -0,0 +1,5 @@
1
+ # worker1.5 (Worker 1.5) — 1.0 knowledge
2
+
3
+ | Doc | Summary |
4
+ |-----|---------|
5
+ | [Worker 1.5 (1.0 Framework) Architecture](architecture.md) | The single-server 1.0 cron tier that replaced worker's 7-role fleet; open for how the crontab is built from schedules/*.json, the overlap check, and what the cr |
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: Worker 1.5 (1.0 Framework) Architecture
3
+ framework: "1.0"
4
+ repo: worker1.5
5
+ project: Worker 1.5
6
+ client: shared
7
+ type: architecture
8
+ status: active
9
+ updated: 2026-10-23
10
+ owners: [bala]
11
+ files:
12
+ - worker1.5/ebs/cron.worker.php
13
+ - worker1.5/.platform/hooks/postdeploy/020_cron.sh
14
+ - worker1.5/schedules/
15
+ - worker1.5/crons/
16
+ - worker1.5/mvc/get.php
17
+ related:
18
+ - ../worker/architecture.md
19
+ - ../library/architecture.md
20
+ - ../../standards/backend-php.md
21
+ ---
22
+
23
+ The single-server 1.0 cron tier that replaced worker's 7-role fleet; open for how the crontab is built from schedules/*.json, the overlap check, and what the cron list page is (display only).
24
+
25
+ ## Summary
26
+
27
+ `worker1.5` is the 1.0 `App_` background-job tier on **one Elastic Beanstalk server, PHP 8.5, AL2023** (TRUE-83078). It is a copy of the legacy `worker` repo with the same folder layout (`crons/<area>/...`, `crons/toga2/<client>/`). The 7 role schedules were merged into one file. Role election, heartbeat, stale-worker termination and per-role Elastic IPs are gone: one server runs every active job. Builds on `library`.
28
+
29
+ **Critical rules:** the schedule `schedules/cron.<ENVIRONMENT>.json` is the source of truth. Production is `cron.worker.json`. A script not in it never runs. A bad `schedule` (not 5 fields) or a `%` in a command aborts the whole crontab install on deploy. Keep cron's default `/bin/sh`: `App_Framework::isProcessRunning()` only ignores `/bin/sh` parent rows, so another shell breaks the already-running check (TRUE-83121). Any PHP warning exits the request or cron (see the backend-php standard, *Any PHP warning terminates the request*).
30
+
31
+ ## How a job becomes a cron
32
+
33
+ 1. On deploy, `.platform/hooks/postdeploy/020_cron.sh` links `/var/www/html` to `/var/app/current`, reads `ENVIRONMENT` from EB config, and runs `php ebs/cron.worker.php`.
34
+ 2. `cron.worker.php` reads `schedules/cron.<ENVIRONMENT>.json` and writes root's crontab:
35
+ - `cron` → `php /var/www/html/crons/<cron>`; `url` → `wget -q -O /dev/null http://localhost<url>`; `command` → used as is.
36
+ - `active: 0` rows are skipped.
37
+ - Every job appends to the one log file `/var/log/worker-cron.log` (logrotate `/etc/logrotate.d/worker-cron`).
38
+ - It refuses to install if the file is missing, the JSON is bad, a row has no cron/url/command, or there are 0 active jobs.
39
+ 3. Timezone for the installer is `America/Chicago`.
40
+
41
+ Schedule files: `cron.alpha/beta/demo/hotfix/sprint/stage/ui/worker.json`. Only `worker` (production) carries the real list. The others hold 1 to 2 rows.
42
+
43
+ ## Cron list page (`mvc/get.php`)
44
+
45
+ `/` renders the schedule files as a list plus a sidebar. **Display only.** Crons never go through it, so a page error does not stop crons. It reads and sorts the schedule files once into `$scheduleSections`. `cron.worker.json` (empty category) shows as section `misc`.
46
+
47
+ ## Gotchas
48
+ - **Do not read files that are not in this repo on a 1.0 page.** The page read `schedules/cron.worker.catalog.json` (old worker only). The warning became `exit` and the page died about 474 times an hour (9,387 total), probably from a health check on `/`. Fixed in TRUE-83107 (c903b20).
49
+ - **`App_Misc::getFileList` uses `readdir` and does not sort.** Any loop that keeps state per file must set that state on every file. The old loop set `$jsonContents` only for a non-empty category, so `cron.worker.json` reused the previous file's rows, or warned (and exited) if it came first.
@@ -4,12 +4,12 @@
4
4
  |-----|---------|
5
5
  | [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) | Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM pages, state, API, multi-tenant resolution); open when working anywhere in this |
6
6
  | [Bundle item visibility & selectability (the three flags, and why they are enforced nowhere but the client)](features/bundle-item-visibility-and-selectability.md) | Which kit components a shopper can see/select is decided entirely in the browser, enforced nowhere on the server; open when touching bundle rendering, kit check |
7
- | [Cart addItem line merge — re-adding an item refreshes its price, except in edit-order mode](features/cart-add-item-line-merge.md) | What `useCartStoreZu.addItem` does when the item is already a cart line: a draft cart takes the new price, edit-order mode keeps the saved order price; open for |
8
7
  | [Cart Bundle Submission & the bundleUuid Identity Contract](features/cart-bundle-submission-and-identity.md) | How cart bundles (kits) become SalesOrderItems on submit/edit, and the bundleUuid identity contract every consumer must obey; open before touching cart-bundle s |
9
8
  | [Cart Notification Emails — auto-add/remove lifecycle + duplicate prevention](features/cart-notification-emails.md) | Cart Notifications section: CC-email duplicate prevention + auto-add/remove of order-for-user & delegate-manager emails; open when touching notification emails |
10
9
  | [Cart Order For user — Add New User duplicate guard + approving-manager rule](features/cart-order-for-user-and-approving-manager.md) | Cart "Order For" user section: the Add New User duplicate check (username OR email), who the Order For list can find, and the config-driven rule that blocks che |
11
10
  | [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | How the cart total (subtotal/shipping/tax/total) is computed and where shipping cost flows from; open when a cart total or shipping charge looks wrong. |
12
11
  | [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | Current state of the Cart form (field-config-driven, section-hardcoded, two dual-mode forms) plus the DECIDED-but-unbuilt config-driven refactor to the toga2.5 |
12
+ | [Cart price refresh — cart lines follow the current item price, and submit stops on a price change](features/cart-price-refresh.md) | How the cart keeps every stand-alone line at the item's current price (one central refresh, re-add merge, edit-order mode) and why submit sends the shopper back |
13
13
  | [Catalog cache freshness — the persisted query cache, per-query staleTime overrides, and focus refetch](features/catalog-cache-freshness.md) | The persisted React Query cache (`localStorage["commerce"]`), the per-query `staleTime` overrides that served prices up to 24h old, focus refetch, how to opt a |
14
14
  | [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | Storefront category tile order is one DB column (`AssortmentItems.sortOrder` ASC) with a NULL-sorts-first trap; open when asked to reorder items on a category p |
15
15
  | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Per-tenant/language/role JSON config that drives almost all commerce UI text, fields, and login fetches (Layer A global + Layer B FIELDS registry); open before |
@@ -0,0 +1,127 @@
1
+ ---
2
+ title: Cart price refresh — cart lines follow the current item price, and submit stops on a price change
3
+ framework: "2.0"
4
+ repo: toga2-commerce
5
+ project: TOGa Commerce
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-23
10
+ owners: ["tcox"]
11
+ files:
12
+ - toga2-commerce/src/hooks/useCartPriceRefresh.ts
13
+ - toga2-commerce/src/api/fetchLatestItemPrices.ts
14
+ - toga2-commerce/src/components/CartPriceRefresh/CartPriceRefresh.tsx
15
+ - toga2-commerce/src/stores/useCartStoreZu.ts
16
+ - toga2-commerce/src/pages/OrderDetails/hooks/useSubmitOrder.ts
17
+ - toga2-commerce/src/pages/Cart/CartPage.tsx
18
+ - toga2-commerce/src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts
19
+ - toga2-commerce/src/api/syncSalesOrderFromApiToLocalStorage.ts
20
+ - toga2-commerce/src/pages/ItemsView/ItemViewPage.tsx
21
+ - toga2-commerce/src/hooks/useCartPriceRefresh.test.ts
22
+ - toga2-commerce/src/stores/useCartStoreZu.test.ts
23
+ related:
24
+ - ./catalog-cache-freshness.md
25
+ - ./order-submit-sync-sequencing.md
26
+ - ./cart-order-total-computation.md
27
+ - ./inactive-item-purchase-gating.md
28
+ - ./cart-bundle-submission-and-identity.md
29
+ - ../workflows/unit-testing-vitest.md
30
+ - ../workflows/local-development-hosts.md
31
+ - ../../api2/features/v2-rest-query-contract.md
32
+ ---
33
+
34
+ How the cart keeps every stand-alone line at the item's current price (one central refresh, re-add merge, edit-order mode) and why submit sends the shopper back to the cart when a price changed; open for any "cart shows the old price" or "order went in at the wrong price/tax" bug.
35
+
36
+ ## Summary
37
+
38
+ > **Status 2026-10-23 — branch-dependent.** All of this is on `TRUE-82729` (PR #378 → `_production`, pending merge). On `_production` a cart line keeps the price it was added at.
39
+
40
+ 1. **The cart checks prices itself.** `useCartPriceRefresh` (mounted once in `App.tsx`) fetches the current price of every cart item in one call and writes it into the cart store. Per-page fixes kept failing because each page's own cache handed the cart old prices — never fix a cart price on one page again.
41
+ 2. **Re-adding an item** (`addItem` on an existing line) takes the new `price` / `lease` / `Item`, keeps the line `uuid` (sales order item link), sums quantity.
42
+ 3. **Edit-order mode reprices too** — a placed order's lines move to the current price when it is opened for edit.
43
+ 4. **Submit re-checks.** If a line price changed after the shopper left the cart page, submit stops, shows a toast and goes back to the cart (tax must be worked out again).
44
+
45
+ Only stand-alone lines (`items`). Kit lines (`bundles`) and lease price are **not** refreshed.
46
+
47
+ ## How it works
48
+
49
+ ### The refresh (`src/hooks/useCartPriceRefresh.ts`)
50
+
51
+ - Mounted via `src/components/CartPriceRefresh/CartPriceRefresh.tsx` in `App.tsx`, inside `AuthProvider` / `ToasterProvider`, next to `RouterProvider` — runs on every page.
52
+ - One React Query query. Key: `["cartItemPrices", <sorted item uuids>, <tenant price field>, user.uuid, user._currencyId]`.
53
+ - `fetchLatestItemPrices.ts`: `GET /items`, fields `[uuid, <price field>]`, where `Items.uuid:in:<uuid>:<uuid>` (api2 splits `in` values on `:`), `recordsPerPage` = uuid count (an unpaged V2 list stops at 25 rows, no warning). See [V2 REST query contract](../../api2/features/v2-rest-query-contract.md).
54
+ - **Price field per tenant** comes from the item page FIELDS (`ITEMSPAGE.fetchItemDetailsFields`), filled by `replaceFieldVariables` — so QUAD gets `_unitPrice(personaId:<id>)`. No price field → query disabled.
55
+
56
+ When it runs:
57
+
58
+ | Trigger | How |
59
+ |---|---|
60
+ | items / quantities change | effect on `cartContentsKey` (`uuid:quantity`, price left out so applying prices never re-triggers) |
61
+ | tab becomes visible | `refetchOnWindowFocus: true` |
62
+ | window gets focus | own `window` `focus` listener — Supply open in a separate window does not hide the tab, and React Query v5 listens only to `visibilitychange` |
63
+ | idle on a page | `refetchInterval` 60s, paused while hidden |
64
+ | order submit | `useSubmitOrder` (below) |
65
+
66
+ Manual refetches pass `cancelRefetch: false`, so two triggers join one request.
67
+
68
+ **Prices are applied inside `queryFn`, never from cached `data`.** The query cache is persisted to `localStorage["commerce"]`; applying from `data` would write the restored, old prices. `queryFn` reads `signal` and writes nothing when aborted (logout, user / persona / currency switch, the submit check).
69
+
70
+ ### The store (`useCartStoreZu.ts`)
71
+
72
+ - `applyLatestItemPrices` sets **both** line `price` (totals in `useCartViewModel`, and the sales order sync) and `Item._unitPrice` (what `CartContentsTable` shows). Skips a null / empty price — never sets 0. Returns the same state when nothing changed (no re-render).
73
+ - `priceChangeCount` goes up only when a **line** `price` changes; `reviewedPriceChangeCount` is set by `markPricesReviewed()`; `havePricesChangedSinceReview()` compares them.
74
+ - `addItem` on an existing line: new `price` / `lease` / `Item`, kept `uuid`, summed quantity — same in edit-order mode.
75
+ - `syncSalesOrderItemsFromLocalStorageCartToApi` treats a price-only difference as a change, so the `PUT` sends it.
76
+
77
+ ### Submit guard (`useSubmitOrder.ts`)
78
+
79
+ 1. `CartPage` `onSubmit` (cart → `/order-details`) calls `markPricesReviewed()`.
80
+ 2. On submit, `await queryClient.refetchQueries({ queryKey: ["cartItemPrices"], type: "active" })` — before **both** paths (new order `createNewSalesOrderSubmit`, edit order `syncSalesOrderLocalStorage`); both read the cart store at call time.
81
+ 3. If `havePricesChangedSinceReview()`: no submit, error toast "Prices have changed since you reviewed your cart. Please check your cart and submit again." (hardcoded English, like the hook's other toast), navigate to `/cart` (edit mode `/cart?uuid=<order>`).
82
+
83
+ **Why:** tax is worked out on the cart page from the subtotal and saved into `salesOrder.tax`; both submit paths send that saved tax. Without the guard, a price change after leaving the cart submits new prices with old tax. Requested by tcox after the review page showed the old price until submit.
84
+
85
+ ### Decision — edit-order mode reprices (reversed 2026-10-21)
86
+
87
+ The 2026-10-19 rule "edit-order mode keeps the saved order price" is **gone** (tcox, `f73c9444`). Edit-order restore (`syncSalesOrderFromApiToLocalStorage`) sets the line price from the **saved** order line; the refresh then moves it to the current price.
88
+
89
+ ### Open
90
+
91
+ - **QUAD:** an admin editing another user's order reprices with the **admin's** persona price. Compass not affected. Left as is, pending developer decision.
92
+ - Lease price is never refreshed (no page fetches `_leasePrice`).
93
+
94
+ ## How to test (QC — UI only)
95
+
96
+ Commerce and Supply must point at the same environment (client-sandbox: both use `https://api.client.sandbox.togahub.com/v2`).
97
+
98
+ 1. In Commerce, add an item to the cart. Note its price.
99
+ 2. In Supply, change that item's price and save.
100
+ 3. Click back into the Commerce window. The cart shows the new price (or within 60s if left open on the cart page).
101
+ 4. Go to the review page (Submit on the cart). In Supply, change the price again.
102
+ 5. Submit in Commerce. Expect the "Prices have changed…" toast and the cart page with the new price and new tax.
103
+ 6. Submit again from the cart. The order goes in at the new price.
104
+ 7. Open a placed order for edit. Its lines show the current price.
105
+
106
+ Unit tests: `src/hooks/useCartPriceRefresh.test.ts`, `src/stores/useCartStoreZu.test.ts` — see [unit testing](../workflows/unit-testing-vitest.md).
107
+
108
+ ## Gotchas
109
+
110
+ - **⚠ "Supply price change doesn't show in Commerce" — the backend was never at fault.** Check client-sandbox `Logs_<Tenant>.Api` for Supply's `PUT /v2/items/<uuid>` (payload `_unitPrice`) and `Client_<Tenant>.ItemPrices.price`. If both are right, the cause is a Commerce front-end cache — see [catalog cache freshness](./catalog-cache-freshness.md).
111
+ - **⚠ The cart table shows `Item._unitPrice`, not line `price`.** Update both or the screen and the totals disagree.
112
+ - **Logout always empties the cart** — `AuthContext` `resetClientState` clears `zu-cart` on sign-in and sign-out (since 2024-12, `15b6be31`) so the next user never sees the last cart. Not a bug. Only edit-order restores a cart from the API.
113
+ - **Item page:** the Add to Cart price effect depends on `[selectedItem, currencySymbol, user?._currencyId]` and `fetchItemDetails` has no `staleTime` — same lines as `TRUE-82427`, so both merge clean. Details in [catalog cache freshness](./catalog-cache-freshness.md#item-detail-add-to-cart-must-use-the-refetched-price-true-82427).
114
+ - **Item page** resets only quantity after add. It used to reset price to 0, so a second click sent price 0.
115
+ - The inactive-item guard also lives in `addItem` — see [inactive-item purchase gating](./inactive-item-purchase-gating.md#3-the-store-is-the-choke-point-not-the-buttons).
116
+ - A currency change in MySettings clears the cart, so a merge never mixes currencies.
117
+
118
+ ## Change history
119
+ - 2026-10-23 — Created from `cart-add-item-line-merge` (merged in). Central `useCartPriceRefresh` (one call, focus / window focus / 60s / cart change triggers), `applyLatestItemPrices`, submit price guard with review counter, edit-order mode now reprices (2026-10-19 decision reversed). TRUE-82729, PR #378. (tcox)
120
+ - 2026-10-19 — Re-add takes the new price/lease/Item (keeps line uuid, sums quantity); item page no longer resets price to 0 after add; first cart store unit tests. TRUE-82729. (tcox)
121
+
122
+ ## Related
123
+
124
+ - [catalog-cache-freshness](./catalog-cache-freshness.md)
125
+ - [order-submit-sync-sequencing](./order-submit-sync-sequencing.md)
126
+ - [cart-order-total-computation](./cart-order-total-computation.md)
127
+ - [inactive-item-purchase-gating](./inactive-item-purchase-gating.md)
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-21
9
+ updated: 2026-10-23
10
10
  owners: ["bala", "apeterson", "tcox"]
11
11
  files:
12
12
  - toga2-commerce/src/lib/queryClient.ts
@@ -32,7 +32,7 @@ files:
32
32
  - toga2-commerce/src/pages/BundleView/hooks/applyBundleFormatting.ts
33
33
  - toga2-commerce/src/pages/GetSupport/GetSupportPage.tsx
34
34
  related:
35
- - ./cart-add-item-line-merge.md
35
+ - ./cart-price-refresh.md
36
36
  - ./inactive-item-purchase-gating.md
37
37
  - ./force-logout-on-deployment.md
38
38
  - ./sso-redirect-and-session-gating.md
@@ -94,7 +94,7 @@ A change reaches the shopper on: navigating to the page (mount), refresh / fresh
94
94
 
95
95
  Limits to state to the business:
96
96
 
97
- 1. **Prices already in the cart do not update** — the cart is Zustand + `localStorage`, not React Query. Re-adding the item refreshes its line price on `TRUE-82729` (not in edit-order mode) — see [cart addItem line merge](./cart-add-item-line-merge.md).
97
+ 1. **Cart line prices are the exception** — the cart is Zustand + `localStorage`, not page queries. On `TRUE-82729` (pending merge) its own refresh also polls every 60s — see [cart price refresh](./cart-price-refresh.md).
98
98
  2. **Alt-tabbing to another application is not a focus event** — see [frontend standard §4](../../../standards/frontend.md).
99
99
  3. **On a cold load the old value paints first** from `localStorage["commerce"]`, then swaps.
100
100
 
@@ -115,7 +115,7 @@ Limits to state to the business:
115
115
 
116
116
  - No loop: `selectedItem` is view-model state, and React Query structural sharing keeps its reference when data is unchanged.
117
117
  - Never add `itemToAddToCart` as a dependency — it is rebuilt every render.
118
- - The re-add-keeps-old-price and second-click-price-0 bugs are fixed on `TRUE-82729` (pending merge) — see [cart addItem line merge](./cart-add-item-line-merge.md).
118
+ - `TRUE-82729` carries the identical lines (these deps + no `staleTime` on `fetchItemDetails`), so both branches merge clean. The cart no longer depends on this path for its price — see [cart price refresh](./cart-price-refresh.md).
119
119
 
120
120
  ### Opting a single query out — `staleTime: 0` + `gcTime: 0` is not enough
121
121
 
@@ -180,7 +180,7 @@ Pinned 2026-09-03 on `TRUE-81610`.
180
180
  - **⚠ "The price didn't update" — check the environment first.** A price changed in one environment's Supply never reaches a Commerce pointed at another. `npm run compass` uses `.env.development` → `https://api.beta.togahub.com/v2` (beta DB). To test a client-sandbox Supply change locally: run `npx vite --mode sandbox-client --host compass.togacommerce` (`.env.sandbox-client` → `https://api.client.sandbox.togahub.com/v2`), then `localStorage.clear()` and log in again (beta tokens do not work on client-sandbox).
181
181
  - **⚠ A per-query `refetchOnWindowFocus: false` or `staleTime` beats the global default.** Grep for them before blaming `queryClient.ts`.
182
182
  - **⚠ Never verify a catalogue or FIELDS change on a warm browser.** Run `localStorage.removeItem('commerce')` or log out first.
183
- - **⚠ A cart line's price never refetches** (Zustand, outside React Query).
183
+ - **⚠ A cart line's price never refetches on `_production`** (Zustand, outside React Query). `TRUE-82729` adds [cart price refresh](./cart-price-refresh.md); its gotchas hold the backend check for "Supply price change doesn't show".
184
184
  - **⚠ `gcTime: 0` does not stop persistence.** Also exclude the key via `dehydrateOptions.shouldDehydrateQuery`.
185
185
  - **⚠ Nothing is excluded from persistence on `_production`** (2026-10-08).
186
186
  - **⚠ The core-parameters watcher is not a targeted invalidation** — any Core parameter change logs out every user. Use `queryClient.invalidateQueries` for anything narrower.
@@ -190,8 +190,9 @@ Pinned 2026-09-03 on `TRUE-81610`.
190
190
  - **`AuthLayout` never remounts**, so `refetchOnMount` fires once per session for queries it owns.
191
191
 
192
192
  ## Change history
193
+ - 2026-10-23 — Cart prices now refreshed by their own query on `TRUE-82729` — see [cart price refresh](./cart-price-refresh.md) (replaces `cart-add-item-line-merge`). (tcox)
193
194
  - 2026-10-21 — `TRUE-82427` restarted from `_production` as one commit `dccedde1` (pending PR); old branch, PR #376 and its `_sandbox-client` merge gone (reverted `b435def9`). Same scope, plus: Home error page only when the failing query has no data; Home display lists default to `[]`; `AuthLayout` reads client fields directly instead of `useHomeViewModel` (stops the Home queries running on every page); local env-mode testing gotcha. `TRUE-81382` noted off `_sandbox-client` (`df472007`). (tcox)
194
- - 2026-10-19 — The two open item-page Add to Cart bullets closed by `TRUE-82729`; moved to [cart addItem line merge](./cart-add-item-line-merge.md). (tcox)
195
+ - 2026-10-19 — The two open item-page Add to Cart bullets closed by `TRUE-82729`; moved to [cart price refresh](./cart-price-refresh.md). (tcox)
195
196
  - 2026-10-13 — `TRUE-82427` first version (old branch, since replaced): per-query `staleTime` / `refetchOnWindowFocus: false` overrides removed, focus refetch on app-wide, guard's explicit opt-in removed, Home refetch fix, item-detail Add to Cart uses refetched price, Home refetch reapplies the submitted search, 81382 merge resolution corrected; four other refetch-ignoring pages logged as open. Countries + 5-min login queries left out of scope. 30s heavy-query shape superseded. (tcox)
196
197
  - 2026-10-08 — Bundle queries uncached + excluded from persistence (TRUE-81382); branch map of the two persistence filters; buster live on `_production`. (tcox)
197
198
  - 2026-10-01 — Global default already `staleTime: 0`; per-query 24h overrides inventoried; ticket scope agreed. (apeterson)
@@ -6,13 +6,14 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: ["bala", "apeterson"]
9
+ updated: 2026-10-23
10
+ owners: ["bala", "apeterson", "tcox"]
11
11
  files:
12
12
  - toga2-commerce/src/pages/OrderDetails/hooks/useSubmitOrder.ts
13
13
  - toga2-commerce/src/api/syncSalesOrdersDataFromLocalStorageCartToApi.ts
14
14
  - toga2-commerce/src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts
15
15
  related:
16
+ - ./cart-price-refresh.md
16
17
  - ./cart-bundle-submission-and-identity.md
17
18
  - ./inactive-item-purchase-gating.md
18
19
  - ../architecture.md
@@ -31,7 +32,7 @@ Submitting an order from the cart fires **two independent sync routines** — on
31
32
 
32
33
  ## How it works
33
34
 
34
- `useSubmitOrder` drives the submit. The two routines are:
35
+ `useSubmitOrder` drives the submit. On `TRUE-82729` (pending merge) it first runs a last cart price check and stops if a price changed since the cart page — see [cart price refresh → submit guard](./cart-price-refresh.md#submit-guard-usesubmitorderts). The two routines are:
35
36
 
36
37
  | Routine | File | What it does |
37
38
  |---|---|---|
@@ -98,8 +99,13 @@ Three changes, in `toga2-commerce`:
98
99
  - **⚠ Omitting a line from the rebuilt cart DELETES it from a placed order.** `processItems` with `getDeleteItems = true` derives the delete set by difference, so "don't re-add this one" and "destroy this one" are the same instruction on this path.
99
100
  - **There is NO success toaster on order placement — by design, not a bug.** `useSubmitOrder` calls `addToaster` **only in its `catch` block** (`status: 'error'`). Both success paths navigate away before anything could render: cart → `createNewSalesOrderSubmit()` → `navigate("/order-details?uuid=…&type=orderPlaced")` (no `addToaster` anywhere in it); edit-order → `window.location.href = getSupplyRedirectLink(orderUuid)`. **Consequence for debugging: placing an order is not a valid way to test toaster styling.** Use a path that stays mounted — add/edit shipping address (Cart or Account), Account → My Settings, or Cart → add new user; for the error variant, force a submit failure to hit the catch block. See [blox Toaster](../../toga-blox/features/toaster.md).
100
101
 
102
+ ## Change history
103
+ - 2026-10-23 — Price check before both submit paths (TRUE-82729, pending merge); see [cart price refresh](./cart-price-refresh.md). (tcox)
104
+ - 2026-08-11 — Item sync before header sync, `depth: 1`, self-overwrite `PUT` dropped (TRUE-80551). (bala, apeterson)
105
+
101
106
  ## Related
102
107
 
108
+ - [cart-price-refresh](./cart-price-refresh.md)
103
109
  - [cart-bundle-submission-and-identity](./cart-bundle-submission-and-identity.md)
104
110
  - [inactive-item-purchase-gating](./inactive-item-purchase-gating.md)
105
111
  - [architecture](../architecture.md)
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-19
9
+ updated: 2026-10-23
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga2-commerce/vitest.config.ts
@@ -14,10 +14,11 @@ files:
14
14
  - toga2-commerce/src/lib/session.test.ts
15
15
  - toga2-commerce/src/api/refreshAuthTokens.test.ts
16
16
  - toga2-commerce/src/stores/useCartStoreZu.test.ts
17
+ - toga2-commerce/src/hooks/useCartPriceRefresh.test.ts
17
18
  related:
18
19
  - ./cypress-testing.md
19
20
  - ../features/sso-redirect-and-session-gating.md
20
- - ../features/cart-add-item-line-merge.md
21
+ - ../features/cart-price-refresh.md
21
22
  ---
22
23
 
23
24
  How to run and write unit tests in commerce (vitest + jsdom), and how to verify a change actually compiles without tripping the build-mode trap.
@@ -28,15 +29,18 @@ How to run and write unit tests in commerce (vitest + jsdom), and how to verify
28
29
  2. Name the file next to its subject: `src/lib/session.test.ts`, `src/api/refreshAuthTokens.test.ts`, `src/stores/useCartStoreZu.test.ts` (Zustand store). Test names are snake_case `test_…`.
29
30
  3. **Prove the test fails on the pre-fix code first.** Reverting `hasSession` turned 5 red; removing the single-flight and identity guards turned 3 red. A regression test that was never seen red proves nothing.
30
31
  4. Test a race with a **held-open promise you resolve by hand**, never real timing — that is what makes the refresh tests deterministic.
31
- 5. Verify a build compiles with `npx vite build --mode development` (cheap), then read the trap below before trusting any `DEV`-gated path.
32
+ 5. **Test a React Query hook by mounting the real hook** (`react-dom` `createRoot` + `act`, mocked `fetch`) — example `src/hooks/useCartPriceRefresh.test.ts`. To test `refetchInterval`, fake only `setInterval` / `clearInterval`.
33
+ 6. Verify a build compiles with `npx vite build --mode development` (cheap), then read the trap below before trusting any `DEV`-gated path.
32
34
 
33
35
  ## Gotchas
34
36
 
37
+ - **⚠ React Query v5 notifies on `setTimeout(0)`.** A "settle" helper must await macrotasks inside `act`, not microtasks — microtask-only settles were flaky.
35
38
  - **⚠ `npm run test` fails with `vitest: command not found` on a stale `node_modules`.** vitest/jsdom are in `devDependencies` but not installed until you run `npm install`.
36
39
  - **⚠ `import.meta.env.DEV` comes from `NODE_ENV`, not `--mode`.** `vite build` pins `NODE_ENV=production`, so a `DEV`-gated code path is **unreachable in every deployed mode** — including `--mode development`. Never gate behaviour a deployed build needs on `DEV`.
37
40
  - **⚠ Commerce's production build script is plain `vite build` with no `--mode`,** so `MODE` is `"production"`. Anything keyed on `MODE` (e.g. the SSO `Environments.slug` lookup) resolves to `PRODUCTION` there.
38
41
  - Commerce had **no** unit harness before 2026-09-22 — Cypress only ([cypress-testing](./cypress-testing.md)). Put browser/route behaviour in Cypress; put pure logic and races in vitest.
39
42
 
40
43
  ## Change history
44
+ - 2026-10-23 — First hook test (`useCartPriceRefresh.test.ts`); React Query macrotask settle + fake-interval gotchas. TRUE-82729. (tcox)
41
45
  - 2026-10-19 — First cart store test (`useCartStoreZu.test.ts`, 4 tests); stale `node_modules` gotcha. TRUE-82729. (tcox)
42
46
  - 2026-09-22 — Created. vitest + jsdom added to commerce with 19 tests over `hasSession` and the token refresh. TRUE-81991 (`e546db3d`). (apeterson)
@@ -7,7 +7,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
7
7
  - **library** (Library) _(framework core)_ — 41 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 43 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
- - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
10
+ - **worker1.5** (Worker 1.5) — 1 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 18 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
12
12
  - **togaview** (TOGa View) — 9 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
13
13
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1120",
3
+ "version": "1.0.1122",
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",
@@ -1,86 +0,0 @@
1
- ---
2
- title: Cart addItem line merge — re-adding an item refreshes its price, except in edit-order mode
3
- framework: "2.0"
4
- repo: toga2-commerce
5
- project: TOGa Commerce
6
- client: shared
7
- type: feature
8
- status: active
9
- updated: 2026-10-21
10
- owners: ["tcox"]
11
- files:
12
- - toga2-commerce/src/stores/useCartStoreZu.ts
13
- - toga2-commerce/src/stores/useCartStoreZu.test.ts
14
- - toga2-commerce/src/stores/useEditOrderZu.ts
15
- - toga2-commerce/src/pages/ItemsView/ItemViewPage.tsx
16
- - toga2-commerce/src/api/syncSalesOrderFromApiToLocalStorage.ts
17
- - toga2-commerce/src/pages/Cart/view/cartTable/CartTableBundleItem.tsx
18
- related:
19
- - ./catalog-cache-freshness.md
20
- - ./inactive-item-purchase-gating.md
21
- - ./cart-bundle-submission-and-identity.md
22
- - ./cart-order-total-computation.md
23
- - ../workflows/unit-testing-vitest.md
24
- - ../workflows/local-development-hosts.md
25
- ---
26
-
27
- What `useCartStoreZu.addItem` does when the item is already a cart line: a draft cart takes the new price, edit-order mode keeps the saved order price; open for any "cart shows the old price" or "editing an order changed its price" bug.
28
-
29
- ## Summary
30
-
31
- > **Status 2026-10-19 — branch-dependent.** The rules below are on `TRUE-82729` (`c2b756a8`, pending merge, not on `_production`). Ship it **with or after** `TRUE-82427` — see [Merge order](#merge-order).
32
-
33
- A line is matched by `Item.uuid`. On a match:
34
-
35
- | Mode | `price` / `lease` / `Item` | `uuid` (sales order item link) | quantity |
36
- |---|---|---|---|
37
- | Draft cart | taken from the new item | kept | summed |
38
- | Edit-order (`useEditOrderZu.getState().inEditMode`) | **kept** (saved order values) | kept | summed |
39
-
40
- New items added while editing use the current price. The line `price` (`ItemForZuCart.price`) is what totals (`useCartViewModel.ts` reduce) and the sales order sync (`syncSalesOrderItemsFromLocalStorageCartToApi.ts` L42) use.
41
-
42
- ## How it works
43
-
44
- ### Decision — edit-order mode never reprices a placed order
45
-
46
- Decided 2026-10-19 (tcox). Repricing an order the customer already placed is bad business practice. Edit mode starts at `/cart?uuid=<salesOrderUuid>` (`useCartViewModel.ts` ~L87-110 calls `setInEditMode(true)`).
47
-
48
- **Check `inEditMode`, never the line `uuid`.** A normal draft cart is a quote sales order loaded at login by `syncSalesOrderWithLocalStorageCart` (`AuthLayout`), so its lines carry sales order item uuids too.
49
-
50
- ### Who calls `addItem` with an existing line
51
-
52
- - **Item page** (`ItemViewPage.tsx` `handleAddItemToCart`) — the main re-add path. After add it resets quantity to 1 only; price/lease/uuid stay.
53
- - **Split from kit** (`CartTableBundleItem.tsx` ~L74) — sends `bundleItem.item._unitPrice`, the item's own catalog price (the kit price is `bundleItem._unitPrice`). Split is enabled only for COMPASS / COMPASSCANADA admins.
54
- - **Home `ItemsSection` and Filter** — call `addItem` only when the item is not in the cart (they show +/- otherwise), so they never reach the merge branch.
55
- - **Cart "+" (`incrementItem`)** — never changes price.
56
-
57
- A currency change in MySettings clears the cart, so a merge never mixes currencies.
58
-
59
- ### Merge order
60
-
61
- On `_production` the item page still serves a price cached up to 24h (see [catalog cache freshness](./catalog-cache-freshness.md#where-the-staleness-lives)), so a re-add would copy that old cached price onto the line. `TRUE-82427` removes that cache. The branches touch different lines; no conflict expected.
62
-
63
- ### How to test (no DB change)
64
-
65
- 1. Add an item to the cart.
66
- 2. In the browser console, edit `localStorage["zu-cart"]`: parse it, set `c.state.items[0].price = 1`, write it back. Reload.
67
- 3. Re-add the same item from the item page. The line must jump back to the real price.
68
- 4. Edit mode: open `/cart?uuid=<test order uuid>`, fake the price the same way, re-add. The price must stay.
69
-
70
- Passed 2026-10-19. Unit tests: `src/stores/useCartStoreZu.test.ts` (4 tests incl. edit mode) — see [unit testing](../workflows/unit-testing-vitest.md).
71
-
72
- ## Gotchas
73
-
74
- - **⚠ Fixing the re-add price exposed a price-0 bug on the item page.** `handleAddItemToCart` used to reset `itemForCart` to price 0 / lease 0 / uuid `""`; the sync effect does not re-run, so a second click sent price 0. The old merge hid it by keeping the line's price. Fixed on `TRUE-82729`: reset quantity only.
75
- - **⚠ The old merge bug was old, not a regression** — present since at least 2025-10 (`8c3ad44c`). It surfaced in QA after `TRUE-82427` made the item page send the refetched price (CodeRabbit on the old PR #376, since closed — 82427 was restarted, see [catalog cache freshness](./catalog-cache-freshness.md#summary)). `TRUE-81382` never touches the cart store.
76
- - The inactive-item guard also lives in `addItem` — see [inactive-item purchase gating](./inactive-item-purchase-gating.md#3-the-store-is-the-choke-point-not-the-buttons).
77
-
78
- ## Change history
79
- - 2026-10-19 — Created. Re-add takes the new price/lease/Item (keeps line uuid, sums quantity); edit-order mode keeps the saved price (decision); item page no longer resets price to 0 after add; first cart store unit tests. TRUE-82729 (`c2b756a8`, pending merge). (tcox)
80
-
81
- ## Related
82
-
83
- - [catalog-cache-freshness](./catalog-cache-freshness.md)
84
- - [inactive-item-purchase-gating](./inactive-item-purchase-gating.md)
85
- - [cart-bundle-submission-and-identity](./cart-bundle-submission-and-identity.md)
86
- - [cart-order-total-computation](./cart-order-total-computation.md)