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.
- package/knowledge/1.0/apps/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +72 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md +15 -0
- package/knowledge/2.0/apps/toga25-supply/workflows/cypress-testing.md +143 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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