toga-ai 1.0.239 → 1.0.241

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.
@@ -5,7 +5,7 @@
5
5
  | [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
6
6
  | [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
7
7
  | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php |
8
- | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
8
+ | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, _underscore/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
9
9
  | [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 |
10
10
  | [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 |
11
11
  | [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 |
@@ -6,10 +6,11 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-29
9
+ updated: 2026-06-30
10
10
  owners: [dfranks]
11
11
  files:
12
12
  - test/@dave/checker.php
13
+ - _underscore/Component/Forecast/SaleImport/SaleImport.php
13
14
  - test/@dave/looper.php
14
15
  - test/@dave/reconcile_netsuite_totals.php
15
16
  - test/@dave/fixer.php
@@ -32,6 +33,7 @@ related:
32
33
  - ../architecture.md
33
34
  - ../../library/features/netsuite-suiteql-rest-shim.md
34
35
  - ../../library/features/netsuite-suiteql-api-reference.md
36
+ - ../../_underscore/features/forecast-sale-import.md
35
37
  ---
36
38
 
37
39
  ## Summary
@@ -202,6 +204,47 @@ by reconciling a chosen tranDate range directly against NetSuite.
202
204
  on `tranDate`, not NS `lastmodifieddate`, so a **symmetric** lastmodified fingerprint isn't possible — it
203
205
  would require NS-pull-then-id-lookup or the recency-pruned approach.
204
206
 
207
+ - **JOURNAL ENTRIES are reconciled WITHIN the SALES category, not as a separate category (TRUE-79862
208
+ architecture decision, dfranks).** A JE is **not its own reconciliation category** — JE rows live in
209
+ the **same `Forecast.Sales` table** as the four sale types (`netsuiteTransactionType='journalEntry'`),
210
+ written by the same `_Component_Forecast_SaleImport` importer (see the sale-import doc's *JournalEntry
211
+ import* section). So the **sales pass covers the WHOLE `Sales` table**, sale-types + JEs together:
212
+ - **`fixer.php`** — `findSalesDiscrepancies` merges, on the **NS side**, the sale-type per-line aggregate
213
+ with `journalEntryNsTotals()` (a GL-account-grouped JE helper); the **FC side reads ALL `Sales` rows**
214
+ (no type filter); each discrepancy is tagged **`kind` (`'sale'` | `'je'`)**. The FIX dispatch routes
215
+ sale ids to `fixSales()` and JE ids to `fixJournalEntries()`. The per-corrector DELETE scoping stays
216
+ **per-type** (`fixSales` delete-candidates scoped to the 4 sale types; `fixJournalEntries` scoped to
217
+ `'journalEntry'`) so neither corrector deletes the other's rows.
218
+ - **`checker.php`** — the sales `nsSub` is a **`UNION ALL`** of the sale-type arm and a JE arm; the sales
219
+ `fcSub` reads **all `Sales` rows**. checker needs **no per-item/JE-specific logic** because its
220
+ fingerprint compares per-transaction (per-id) totals + the id-set, which are **item-agnostic**.
221
+ - **JE→`Forecast.Sales` reconciliation math + scope (mirrors `_Component_Forecast_SaleImport::buildJournalEntryRows`).**
222
+ Only JE lines posting to the revenue/cost **account-NUMBER allowlist** are sales-relevant: revenue
223
+ `41100/41300/41500`, cost `51100/51200` (exact-number, never prefix — see the sale-import doc for the
224
+ sub-account traps). `revenue = Σ(−foreignamount)` over revenue-acct lines; `profit = Σ(−foreignamount)`
225
+ over **all** allowlisted lines (cost lines net profit down via debit-positive `foreignamount`). Group by
226
+ **(local salesRep, item)** → one Sales row per group; synthetic `lineNumber` from
227
+ `ksort(json_encode([salesRepId,itemId]))` then `1..N` — **fixer MUST key identically** or its upsert
228
+ dupes against the webhook importer's rows. JE `foreignamount`s are **cent-precision**, so a per-JE
229
+ sum-then-round equals the importer's per-salesRep round-then-sum **exactly** — this is what keeps
230
+ checker's "in sync" == fixer's "nothing to fix" for JEs (the compute-identically invariant applied to JEs).
231
+ Scope facts: of **~1235 JEs** in 2025-01-01..present, only **~295** touch a revenue/cost account; **~34**
232
+ of those net to **0 rev AND 0 profit** and import no rows. JEs hitting only bank/equity/tax/AP accounts
233
+ are correctly excluded **by design** (e.g. JE 7213407 = a Bank↔Equity reclass, 0 allowlist lines → never
234
+ imported — not a miss). An accrual JE and its NetSuite auto-reversal are **separate transactions**, each
235
+ reconciled independently by its own `internalId`.
236
+ - **JE item dimension is NULL today; here is how to enable it later.** `itemId` is null on JE Sales rows
237
+ because the JE import path reads item **only** via the configurable `JE_LINE_ITEM_FIELD` (currently null)
238
+ — it does **not** read native `transactionline.item`, so even a populated native `tl.item` would NOT flow
239
+ through. To enable: (1) set `JE_LINE_ITEM_FIELD` to the source field (`'item'` for native, or a
240
+ `custcol_*` if a custom column is added) — `journalLineLookup` then resolves it via
241
+ `lookupId('Items','netsuiteItemInternalId',…)` and the group key already carries the item slot; AND (2)
242
+ update **`fixer.php`** to SELECT the item column in its JE SuiteQL, resolve NS item→local id (reuse
243
+ `createMissingForecastItemFromNetSuite` self-heal), and extend the group key to `(salesRep, item)`.
244
+ **`checker.php` needs no change.** Consider whether item should self-heal (salesRep does **not** — see
245
+ the sale-import doc). The salesRep dimension itself is the JE line custom column `custcol_sales_rep_line`
246
+ (wired under TRUE-79862).
247
+
205
248
  ## Data model
206
249
 
207
250
  `Forecast.Sales`, `Forecast.OpenOrderItems` on the **core2** cluster
@@ -214,6 +257,16 @@ None — Forecast2 is a single shared dataset.
214
257
 
215
258
  ## Gotchas / known issues
216
259
 
260
+ - **CROSS-TYPE CONTAMINATION: once JEs share `Forecast.Sales`, any FC Sales read missing a type scope
261
+ treats JE rows as rogue sales.** Discovered live (TRUE-79862): the SALES fix **deleted 74 JE rows
262
+ (8 JEs) as "stale sales"** because `findSalesDiscrepancies`'s FC query had **no type filter**, so JE
263
+ rows looked like FC-only sales that the NS-sales aggregate never returns. **Correct resolution is NOT
264
+ to filter JEs OUT of the sales pass** — it is to reconcile the **whole table together** (NS side =
265
+ sale-types `UNION` JE; FC side = all rows) and **route the FIX by `kind`** (`'sale'`→`fixSales`,
266
+ `'je'`→`fixJournalEntries`), keeping each corrector's DELETE scoping per-type so neither deletes the
267
+ other's rows. (See the JE-within-Sales architecture under *How it works*.) General lesson: when two
268
+ transaction families share one table, an audit read with no type scope manufactures phantom FC-only
269
+ deltas for the other family.
217
270
  - **An NS-DELETED invoice leaves STALE cost-only `Forecast.Sales` rows the add/update-only import can
218
271
  never remove.** The 5-min `import_sales.php` cron is **add/update-only — it has no delete path** (only
219
272
  the nightly discrepancy-fix deletes). So when an invoice is **deleted in NetSuite**, its already-imported
@@ -420,6 +473,24 @@ None — Forecast2 is a single shared dataset.
420
473
 
421
474
  ## Change history
422
475
 
476
+ - 2026-06-30 — **Folded JOURNAL ENTRIES into the SALES reconciliation in `fixer.php` + `checker.php`
477
+ (TRUE-79862).** Architecture decision (dfranks): a JE is **not its own category** — JE rows live in the
478
+ same `Forecast.Sales` table (`netsuiteTransactionType='journalEntry'`) as the four sale types, so the
479
+ sales pass reconciles the **whole table**. `fixer.findSalesDiscrepancies` merges the NS sale-type
480
+ aggregate with `journalEntryNsTotals()`, reads ALL FC Sales rows, tags each diff `kind` (`sale`|`je`),
481
+ and routes the FIX (`fixSales`/`fixJournalEntries`) with per-type DELETE scoping. `checker`'s sales
482
+ `nsSub` becomes a `UNION ALL` (sale-types + JE) over all FC Sales rows — no JE-specific logic since its
483
+ fingerprint is per-id/item-agnostic. **Gotcha recorded: cross-type contamination** — the initial
484
+ un-typed FC Sales read deleted 74 JE rows (8 JEs) as "stale sales"; fixed by reconciling the families
485
+ together + routing by kind, not by filtering JEs out. **JE math/scope** (mirrors
486
+ `buildJournalEntryRows`): allowlist accts 41100/41300/41500 rev + 51100/51200 cost; group by
487
+ (salesRep,item) with `ksort(json_encode([salesRep,item]))` synthetic lineNumber — fixer keys identically
488
+ to the importer or it dupes; cent-precision foreignamounts make sum-then-round == importer round-then-sum
489
+ (compute-identically holds for JEs). Of ~1235 JEs (2025→present) only ~295 hit a rev/cost acct, ~34 net
490
+ to $0 and import nothing; bank/equity/tax/AP-only JEs excluded by design (not misses). Accrual + NS
491
+ auto-reversal reconcile independently per internalId. **Item dimension is null today** (`JE_LINE_ITEM_FIELD`
492
+ null; native `tl.item` not read); enabling it needs the importer field set AND fixer SELECT/resolve/group-
493
+ by-(salesRep,item) — checker needs no change. (dfranks)
423
494
  - 2026-06-29 — **Recorded that `fixer.php`'s OOI path lacks `locationId`/`quantityBackordered`/
424
495
  `amountDue`** (planning for TRUE-79162). The Sales path already handles anchor-line `amountDue`
425
496
  behind a `forecastColumnExists('Sales','amountDue')` guard; the OpenOrderItems path (lookup SELECT,
@@ -9,3 +9,4 @@
9
9
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
10
10
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
11
11
  | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
12
+ | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -141,7 +141,22 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
141
141
  configs must use a `scope` discriminator in the query key or they overwrite each other's cache
142
142
  (record modal vs. approval modal: the approval modal omitting item-image fields blanked the
143
143
  record modal's images until scoping isolated the entries).
144
+ - **`poNumber` does NOT go through the approval modal.** `SalesOrderView` routes
145
+ `activeModal === "poNumber"` to a **separate** `EnterPoNumberModal` and explicitly excludes it
146
+ from `SalesOrderApprovalModalsLayout` (`activeModal !== "poNumber"`). The approval-modal
147
+ `modalSelected` union still lists `"poNumber"`, but that path is handled by the PO modal, not
148
+ the approval flow — don't add PO-number handling to the approval layout.
149
+ - **Approval submit payload: workflow vs single decision differ.** A single approve/deny posts a
150
+ **boolean** `isApproved` (`true`/`false`). The workflow path
151
+ (`handleFormatApprovalWorkflowPayload`) posts a **string** `"1"`/`"0"`, emits a request **only**
152
+ for stages whose `note` / `assignedTo` / `decision` actually changed, and chooses verb by
153
+ `stage.ApprovalDecisions.uuid` — `null` → POST `/approval-decisions`, otherwise PUT
154
+ `/approval-decisions/{uuid}`.
144
155
 
145
156
  ## Change history
157
+ - 2026-06-30 — Recorded two behaviors confirmed while building approval-modal tests: `poNumber`
158
+ is handled by a separate `EnterPoNumberModal` (excluded from the approval layout), and the
159
+ workflow vs single-decision submit payloads differ (string `"1"`/`"0"` + changed-stages-only +
160
+ POST/PUT by decision uuid, vs boolean `isApproved`). (apeterson)
146
161
  - 2026-06-23 — Documented from CLAUDE.md Modal Patterns 1–5 + the `create-record-modal` skill during
147
162
  initial knowledge seed. (apeterson)
@@ -0,0 +1,143 @@
1
+ ---
2
+ title: Cypress Testing Harness (component + e2e)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-06-30
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/cypress.config.ts
13
+ - toga25-supply/cypress/tsconfig.json
14
+ - toga25-supply/cypress/support/component.tsx
15
+ - toga25-supply/cypress/support/component-index.html
16
+ - toga25-supply/cypress/support/e2e.ts
17
+ - toga25-supply/cypress/support/commands.ts
18
+ - toga25-supply/cypress/support/fixtures.ts
19
+ - toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts
20
+ - toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx
21
+ - toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx
22
+ - toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx
23
+ - toga25-supply/cypress/e2e/salesOrderApproval.cy.ts
24
+ related:
25
+ - ../features/record-modals-and-nested-tables.md
26
+ - ../features/action-button-rule-engine.md
27
+ - ../architecture.md
28
+ ---
29
+
30
+ ## What it is
31
+
32
+ The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch
33
+ (`cypress` was already a dependency but there was no config, no `cypress/` dir, and zero
34
+ tests anywhere in the repo). It defines **both** a `component` project (Vite devServer,
35
+ mounts a single component) and an `e2e` project, plus reusable support code for mounting
36
+ components with the app's providers, mocking a component's data hook without a network or
37
+ tenant config, and sharing fixtures. The first specs cover the Sales Order **approval
38
+ modal flow**. Use this doc when adding any new test to the repo.
39
+
40
+ ## How the harness is wired
41
+
42
+ ### `cypress.config.ts` — two projects
43
+
44
+ - **`component`** — `devServer: { framework: "react", bundler: "vite" }` with a `viteConfig`
45
+ that **mirrors the app's `vite.config.ts`**: the `@` → `src` alias, the regex aliases that
46
+ force a single React copy, `optimizeDeps.exclude: ["@agilant/toga-blox"]` +
47
+ `optimizeDeps.include: ["dayjs"]`. This is the load-bearing part — toga-blox must resolve
48
+ the same way it does under `npm run dev`, or mounted components fail to bundle or get a
49
+ second React copy.
50
+ - **`e2e`** — standard `baseUrl` + `supportFile` setup for full-journey specs.
51
+
52
+ ### `cypress/support/component.tsx` + `commands.ts` — mounting with providers
53
+
54
+ `cy.mountWithProviders` is a custom command that wraps the mounted component in
55
+ `QueryClientProvider` + `ToasterProvider`. It accepts an **injectable `queryClient`** so a
56
+ spec can `cy.spy()` on `removeQueries` / `invalidateQueries` and assert the cache routine a
57
+ component runs on success. The default QueryClient is configured with `retry: false` for
58
+ determinism. Support also imports `@/index.css` (styling) and `@/api/api` — the latter so
59
+ toga-blox's `apiPost` / `apiPut` have a configured axios instance available during component
60
+ tests.
61
+
62
+ ### Window-backed hook-mock pattern (key technique)
63
+
64
+ `cypress/support/mocks/useApprovalModalViewModel.ts` lets you unit-test a data-binding
65
+ component **without** a network or tenant config. The component devServer aliases the
66
+ component's **relative** import of its view model to the mock:
67
+
68
+ ```ts
69
+ // in the component viteConfig resolve.alias
70
+ { find: /^\.\/viewModel\/useApprovalModalViewModel$/, replacement: "<abs>/cypress/support/mocks/useApprovalModalViewModel.ts" }
71
+ ```
72
+
73
+ The mock returns whatever the spec parked on `window.__approvalModalViewModel`. In Cypress
74
+ component testing the spec and the component share the **same `window`**, so a plain
75
+ assignment before `cy.mount` is visible to the hook. Helpers `setApprovalModalViewModel` /
76
+ `resetApprovalModalViewModel` set and clear it. To mock a different component's hook, add a
77
+ parallel regex alias + a parallel `window.__<name>` mock.
78
+
79
+ ### `cypress/support/fixtures.ts` — shared fixtures
80
+
81
+ Imports the **real shipped** tenant config
82
+ `src/pages/SalesOrders/.../viewModel/FIELDS/QUAD/approvalActionFields.json` as
83
+ `recordActionFields`, so tests track the actual field / label / guardrail config the app
84
+ ships (not a hand-rolled copy that drifts). Adds synthetic order / approval-stages / PO
85
+ fixtures alongside it. (The QUAD file is reused only as a representative real config — this
86
+ harness is generic test infra, not Quad-specific behavior.)
87
+
88
+ ## What the specs cover
89
+
90
+ - **`SalesOrderApprovalModalsLayout.cy.tsx`** (component) — the sales-order binding:
91
+ null-guards, `isApproveButtonEnabled` derivation, and the documented `onSuccess` cache
92
+ routine asserted via the spied QueryClient.
93
+ - **`RecordApprovalModalLayout.cy.tsx`** (component) — the generic approval modal flow: deny
94
+ requires a reason (approve optional), submit fires the decision API + `onSuccess`/`onClose`,
95
+ discard-reason guardrail on Cancel/backdrop, workflow approve-enablement, deep workflow
96
+ stage editing (deny one stage with a required note → submits only that stage; an empty
97
+ required note blocks submit), and the **decision-failure path** (500 → modal stays open,
98
+ `onSuccess`/`onClose` not called).
99
+ - **`EnterPoNumberModal.cy.tsx`** (component) — the PO-number action: create requires a PO#,
100
+ POST `/purchase-orders/`, credit-card flag `0`/`1`, edit → PUT.
101
+ - **`salesOrderApproval.cy.ts`** (e2e) — full journey: seed auth via
102
+ `localStorage.accessToken`, row-action menu → Deny → submit, stubbing only the write.
103
+
104
+ ## Conventions for writing tests here
105
+
106
+ - **Component test of a data-binding component** → use the window-backed hook mock so you
107
+ never need a real network/tenant config; park state on `window.__<hook>` before `cy.mount`.
108
+ - **Asserting a cache side-effect** (invalidate/remove queries on success) → inject a spied
109
+ `queryClient` into `cy.mountWithProviders`.
110
+ - **Testing an async error path** → use `cy.on("uncaught:exception", ...)` to swallow the
111
+ rejected `mutateAsync` so the rejection doesn't fail the spec, then assert the modal stayed
112
+ open and `onSuccess`/`onClose` were not called.
113
+ - **e2e auth** → seed `localStorage.accessToken`; stub only the write request, let reads hit
114
+ the configured backend (document any backend prerequisites in the spec).
115
+ - **Keep the component devServer config in sync with `vite.config.ts`** — when the app's
116
+ Vite aliases or `optimizeDeps` change, mirror the change into `cypress.config.ts` or
117
+ component mounts break.
118
+
119
+ ## package.json scripts
120
+
121
+ `cy:open`, `cy:run:ct` (component), `cy:run:e2e`, `test:ct`.
122
+ `.gitignore` excludes `cypress/videos|screenshots|downloads`.
123
+
124
+ ## Gotchas
125
+
126
+ - **Cypress's bundled Electron cannot launch in the sandboxed macOS dev environment** — it
127
+ fails the pre-run smoke test (`bad option: --no-sandbox / --smoke-test / --ping`), which
128
+ blocks **all** specs before any run. The specs in this harness were validated by
129
+ `tsc -p cypress/tsconfig.json` (0 errors) and `eslint cypress` (clean) but **could not be
130
+ executed in that sandbox** — run them locally with `npm run cy:run:ct`.
131
+ - **`eslint.config.js` is protected by a config-protection hook.** When a spec/support file
132
+ trips a lint rule (e.g. `no-explicit-any`), **fix the source to satisfy the linter** (use
133
+ `unknown` / typed shapes) — do **not** weaken the shared eslint config to make tests pass.
134
+ The override edit to `eslint.config.js` is blocked by the hook by design.
135
+ - **`@agilant/toga-blox` consumes `dist/`** — same as the app, the harness sees built
136
+ toga-blox, so a toga-blox source change needs a rebuild before tests observe it.
137
+
138
+ ## Change history
139
+ - 2026-06-30 — Bootstrapped the Cypress harness from scratch (dual component + e2e config,
140
+ `cy.mountWithProviders`, window-backed hook-mock pattern, shared fixtures off the real
141
+ QUAD `approvalActionFields.json`) and added the first specs for the Sales Order approval
142
+ modal flow. Documented the sandbox-Electron blocker and the protected-eslint decision.
143
+ (apeterson)
@@ -28,7 +28,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
28
28
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
29
29
  - **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
30
30
  - **toga2-commerce** (TOGa Commerce) — 7 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
31
- - **toga25-supply** (TOGa 2.5 Supply) — 7 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
31
+ - **toga25-supply** (TOGa 2.5 Supply) — 8 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
32
32
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
33
33
 
34
34
  ## standalone framework
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.239",
3
+ "version": "1.0.241",
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",