toga-ai 1.0.886 → 1.0.888
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/features/netsuite-togasupply-per-client-sync.md +2 -2
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +24 -1
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +23 -1
- package/knowledge/2.0/apps/saml/workflows/rotating-the-saml-sp-certificate.md +17 -3
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/features/bundle-item-pricing.md +89 -0
- package/knowledge/2.0/apps/toga25-supply/features/persisted-query-cache.md +5 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +1 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-24
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -96,7 +96,7 @@ The single most expensive misreading of this engine. `startModeIteration()` writ
|
|
|
96
96
|
`<seconds>-RUNNING` at the **start** of every section and `finishModeIteration()` writes `-IDLE` at
|
|
97
97
|
the end; the ÷3 `REDUCE_INTERVAL_FACTOR` shrink fires only because the *previous* run never reached
|
|
98
98
|
its IDLE write. **There is no timeout, memory cap, or resource limit anywhere in the script** — so
|
|
99
|
-
"didn't finish" can only mean **it threw**. A collapsing window therefore means *go read the error
|
|
99
|
+
"didn't finish" can only mean **it threw**. **Exception (pre-2026-09-24 fix): INVOICES collapses with NO throw whenever its cursor is within 24h of now** — see Gotchas. A collapsing window therefore means *go read the error
|
|
100
100
|
in prod* (see below), never *tune the window size down*. (Corollary: a window that has grown back to
|
|
101
101
|
the cap is positive evidence the section is completing.)
|
|
102
102
|
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-24
|
|
10
10
|
owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- _underscore/Model/Elite/TransferOrderStage.php
|
|
18
18
|
related:
|
|
19
19
|
- ../architecture.md
|
|
20
|
+
- ../../toga25-supply/features/bundle-item-pricing.md
|
|
20
21
|
- ../../../1.0/apps/library/features/netsuite-item-assettype-sync.md
|
|
21
22
|
- ../../toga2-supply/features/currency-amount-lines-editor.md
|
|
22
23
|
- cxml-shipnotice-gateway.md
|
|
@@ -157,6 +158,22 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
|
|
|
157
158
|
|
|
158
159
|
`recordId 351` is `transfer-order-stages`; the equivalent row exists per tenant with a **different `id`** but often the **same `uuid`** (both Elite and NYCHH carry `b273f4d1-dac0-4093-ad1d-7c3a526ab894`), the tell that the row came from one shared seed and only one tenant's copy has the wrong flag.
|
|
159
160
|
|
|
161
|
+
### ⚠ An inherent child nested in a **PUT** is ALWAYS written as a CREATE
|
|
162
|
+
|
|
163
|
+
The engine recurses into a nested one-to-many child (`Core.InherentRecordChildren`) with
|
|
164
|
+
`httpMethod: 'POST'` **hardcoded** (`V2.php:5415`). There is no uuid check and no PUT branch, so a
|
|
165
|
+
child nested in an update is never updated:
|
|
166
|
+
|
|
167
|
+
| Nested child payload on a `PUT` | Result |
|
|
168
|
+
|---|---|
|
|
169
|
+
| no `uuid` | a **duplicate row** is inserted; the parent looks unchanged |
|
|
170
|
+
| `uuid` of the existing row | fails — `EV-10` / MySQL `Error #: 1062 Duplicate entry … for key '<Table>_UNIQUE'` |
|
|
171
|
+
|
|
172
|
+
**Correct pattern: nest children only on a CREATE.** To change an existing child, send its own
|
|
173
|
+
`PUT /v2/<child-route>/{uuid}` after the parent write (two calls). This is general engine
|
|
174
|
+
behaviour — it bites any write to a nested child, not one feature. Worked example:
|
|
175
|
+
[bundle item pricing](../../toga25-supply/features/bundle-item-pricing.md).
|
|
176
|
+
|
|
160
177
|
### Nested inherent-child create — parent FK is auto-set, any depth
|
|
161
178
|
|
|
162
179
|
- V2 auto-sets the child's FK to the parent: first field whose `FIELDOPT_FOREIGNKEY_MODEL` equals the parent class (exact normalized `_Model_Client_` string), then break (`V2.php` ~5211-5221). Payload supplies the OTHER FK only (e.g. bridge row `{ purchaseOrder: { uuid } }` under a transfer order).
|
|
@@ -166,6 +183,7 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
|
|
|
166
183
|
|
|
167
184
|
## Gotchas
|
|
168
185
|
|
|
186
|
+
- **⚠ A nested inherent child on a PUT is created, never updated** (`V2.php:5415` hardcodes `POST`) — you get a duplicate row, or a 1062 duplicate-key error if you send the child's `uuid`. Update children through their own route.
|
|
169
187
|
- **Nested write with only a non-identifier field silently creates duplicates.** A duplicate-generating footgun: each write inserts a new empty child and the parent links to a fresh shell (or is left with a NULL FK). It compounds on any cron/integration that writes the same entity repeatedly. Root-caused in production on the Prudential device-sync cron, which sent `contact: {c_employeeXid}` and generated ~893k empty Contact rows (~5,600/day). See [Prudential device import + contact linking](../../../clients/prudential/features/device-information-import-and-contact-linking.md).
|
|
170
188
|
- **A matched nested object is UPDATED, not just linked** — partner data can mutate shared catalog rows. Matching is not read-only.
|
|
171
189
|
- **A `NULL` `overrideIsIdentifier` unsets the field as an identifier** — a row added only to set a child policy silently strips it, causing EV-12. Correct only where `Core.RecordFields.isIdentifier = 1`; ship it as a `dbchanges2/Client_<Tenant>/` migration (a manual non-prod patch evaporates on reseed).
|
|
@@ -177,6 +195,11 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
|
|
|
177
195
|
- **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
|
|
178
196
|
- **Singular-FK back-reference defects:** UPDATE path drops the parent's forward FK (echoes `null`); CREATE path re-parents a value-matched child to the newest writer (stolen child row). One defect, two symptoms.
|
|
179
197
|
|
|
198
|
+
## Change history
|
|
199
|
+
- 2026-09-24 — Documented that the engine recurses into a nested inherent child with `POST`
|
|
200
|
+
hardcoded (`V2.php:5415`), so a child nested in a PUT is duplicated or fails 1062; update
|
|
201
|
+
children through their own route (TRUE-82138). (apeterson)
|
|
202
|
+
|
|
180
203
|
## Related
|
|
181
204
|
|
|
182
205
|
- [api2 architecture](../architecture.md) — the V2 CRUD engine (`processRoutePairs`).
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-24
|
|
10
10
|
owners: [tcox, bala, apeterson, jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -64,6 +64,24 @@ controller.
|
|
|
64
64
|
the join is genuinely 1:1, or the duplicates agree. Worked example:
|
|
65
65
|
[category tile sort order](../../toga2-commerce/features/category-tile-sort-order.md).
|
|
66
66
|
|
|
67
|
+
### ⚠ `depth` and `calcDepth` are IGNORED inside the base64 `_` options blob
|
|
68
|
+
|
|
69
|
+
`fields`, `where`, `sort`, `join` work inside the `_` blob. **`depth` and `calcDepth` do not** —
|
|
70
|
+
they are only read as plain query params. Silent: no error, the request just runs at the defaults
|
|
71
|
+
(`depth 3`, `calcDepth 1`), so nested calculated fields come back absent and the engine looks
|
|
72
|
+
incapable of returning them. Mixing is fine — put the blob in `_` and add `&calcDepth=2`.
|
|
73
|
+
(toga-blox `assembleOptions` already appends any unrecognised option key to the query string,
|
|
74
|
+
which is why mixed requests work from the FE.) Verified 2026-09-24 (TRUE-82138).
|
|
75
|
+
|
|
76
|
+
### ⚠ A field THREE levels under an inherent child is silently DROPPED, at any depth
|
|
77
|
+
|
|
78
|
+
Distinct from the max-depth omission above — raising `depth` does not help. A field two hops below
|
|
79
|
+
an inherent child (`bundleItems.bundleItemPrices.priceType` on `GET /v2/bundles/{uuid}`) is absent
|
|
80
|
+
from the payload with no error and no message; the same field is returned by the shallower
|
|
81
|
+
`GET /v2/bundle-items/{uuid}`. **Consequence:** a caller cannot tell one child row from another
|
|
82
|
+
(e.g. a unit-price row from a lease-price row) on the parent payload, and must not guess — fetch
|
|
83
|
+
the child route instead.
|
|
84
|
+
|
|
67
85
|
### How `sort` becomes `ORDER BY` — and why "descending sort is broken" is usually just NULLs
|
|
68
86
|
|
|
69
87
|
`ORDER BY` is a **plain string concatenation** of the resolved column and `ASC`/`DESC` — no cast, no
|
|
@@ -565,6 +583,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
565
583
|
- **⚠ A 500 / `EO-1` on a `where` over a calculated field ⇒ you table-PREFIXED the field name.**
|
|
566
584
|
Drop the prefix: `_qtyAvailable`, not `PurchaseOrderItems._qtyAvailable`. The prefixed form is
|
|
567
585
|
routed to a `HAVING` against an alias that does not exist.
|
|
586
|
+
- **⚠ `depth`/`calcDepth` inside the `_` blob do nothing** — send them as plain query params.
|
|
568
587
|
- **⚠ Do not raise `calcDepth` for a calculated field you named in `fields=`** — it is classified
|
|
569
588
|
before the gate and already resolves at the default `1`.
|
|
570
589
|
- **Do not trust a 200 with missing fields.** Without an explicit `fields=` list, ACL removal is
|
|
@@ -585,6 +604,9 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
585
604
|
[V2 request logging](request-logging.md).
|
|
586
605
|
|
|
587
606
|
## Change history
|
|
607
|
+
- 2026-09-24 — Recorded that `depth`/`calcDepth` are ignored inside the base64 `_` options blob
|
|
608
|
+
(plain query params only), and that a field three levels under an inherent child is silently
|
|
609
|
+
dropped at any depth (TRUE-82138). (apeterson)
|
|
588
610
|
- 2026-09-23 — Documented how `sort` becomes `ORDER BY` (plain concatenation, no cast or NULL
|
|
589
611
|
handling), that the same expression doubles as the keyset-pagination cursor so any change must be
|
|
590
612
|
mirrored in the seek `WHERE`/`getSeekFieldType()`, and that "descending sort is broken" reports are
|
|
@@ -6,8 +6,8 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-09-24
|
|
10
|
+
owners: ["jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/Controller/Index.php
|
|
13
13
|
- _underscore/Cloud.php
|
|
@@ -46,7 +46,7 @@ Because a cert change is not picked up by every IdP at once, safe rotation split
|
|
|
46
46
|
|
|
47
47
|
### Multi-cert decryption from Secrets Manager (2026-09-15)
|
|
48
48
|
`/acs` no longer reads SP encryption keys from disk. It sources **N** SP certs from AWS Secrets Manager and decrypts against all at once, so a partner can move to a different cert while other IdPs stay on the existing one — a superset of the earlier new/old overlap.
|
|
49
|
-
- **Two class constants in `saml/Controller/Index.php` drive it:** `ENCRYPTION_SECRET_NAMES` — an **ordered list of Secrets Manager secret
|
|
49
|
+
- **Two class constants in `saml/Controller/Index.php` drive it:** `ENCRYPTION_SECRET_NAMES` — an **ordered list of Secrets Manager secret full ARNs** (not values; full ARN incl. random suffix required since 2026-09-24 — see Cross-account access); `ENCRYPTION_SECRET_REGION` — `'us-east-1'`.
|
|
50
50
|
- **How `/acs` builds credentials:** for each name it calls `_Cloud::getSecret($name, $region)` (see [_Cloud Secrets Manager helper](../../_underscore/features/cloud-secrets-manager.md)), builds an `X509Credential`, then calls `decryptMultiAssertion($credentials, ...)`. An assertion encrypted to ANY listed cert decrypts.
|
|
51
51
|
- **Fails closed with coded responses:** `Invalid SAMLResponse (Code = 5)` if no usable credential can be built at all; `Code = 6` if credentials build but every decrypt fails. Each per-secret build is wrapped in try/catch — a bad/unreadable secret is skipped and logged, so one broken secret does not 500 every login.
|
|
52
52
|
- **Passphrase-aware, format-tolerant key loading:** PEM stored with escaped `\n` / `\r\n` is normalized to real newlines before loading. The secret's `passphrase` is passed to `KeyHelper::createPrivateKey` **only** when the private-key PEM header is the `ENCRYPTED PRIVATE KEY` variant (ACM public-cert exports are always encrypted); a plaintext key loads with no passphrase. A large "how this works" comment sits above `ENCRYPTION_SECRET_NAMES` in the controller.
|
|
@@ -60,6 +60,16 @@ AWS Secrets Manager, account **654654170868**, region **us-east-1**. Each secret
|
|
|
60
60
|
- `toga/ssl/togahub_com-casigned-2027-03-31` — publicly-trusted (ACM exportable) cert.
|
|
61
61
|
- `toga/ssl/togahub_com-casigned-2026-09-16` — publicly-trusted (ACM exportable) cert.
|
|
62
62
|
|
|
63
|
+
Full ARNs (suffixes): `...:secret:toga/ssl/togahub_com-selfsigned-2036-S2r3SR`, `...-casigned-2027-03-31-7CgTyg`, `...-casigned-2026-09-16-7FW9ng`.
|
|
64
|
+
|
|
65
|
+
### Cross-account access for non-prod (2026-09-24)
|
|
66
|
+
Decision: ONE copy of each SP secret, in prod acct **654654170868**; non-prod accounts read it cross-account (no copies). Fixed non-prod `Invalid SAMLResponse (Code = 5)` (secrets did not exist in sandbox acct).
|
|
67
|
+
- **KMS:** secrets re-encrypted from `aws/secretsmanager` (AWS-managed key — cannot be shared cross-account) to prod customer-managed key **`alias/toga-secrets`** (us-east-1, key id `bb201536-5e61-4c53-bff2-63cb2bff7b86`). Key policy: prod root `kms:*`; each non-prod account root `kms:Decrypt` + `kms:DescribeKey` with condition `kms:ViaService = secretsmanager.us-east-1.amazonaws.com`.
|
|
68
|
+
- **Secret resource policy** on each `toga/ssl/*` secret: allow the non-prod role `GetSecretValue` + `DescribeSecret`.
|
|
69
|
+
- **Non-prod role inline policy** `SecretsManagerCrossAccountRead`: secretsmanager on `arn:aws:secretsmanager:us-east-1:654654170868:secret:toga/ssl/*` + kms `Decrypt`/`DescribeKey` on the key ARN.
|
|
70
|
+
- **Accounts granted:** sandbox-client **975050298201**, role `aws-elasticbeanstalk-ec2-role` — done. Beta **502614707982** (`true-saml-beta`) — **NOT done**; repeat all 3 steps with that account id.
|
|
71
|
+
- **Add a non-prod account** = key policy + every secret resource policy + inline policy on that account's EB instance role. New secret = new resource policy + encrypt with `alias/toga-secrets` (not the default key).
|
|
72
|
+
|
|
63
73
|
### Why a public cert was added (Prudential)
|
|
64
74
|
Prudential's IdP **refuses a self-signed cert, pins our exact leaf, and cannot auto-refresh our metadata by URL** — that combination forces a publicly-trusted cert. The correct source is an **ACM exportable public cert** (gives cert + private key), not ACM Private CA. Prudential moves to the public cert while other IdPs stay on the self-signed one; all coexist in the `/acs` multi-cert array. Full rationale: [SSL certificate trust-model strategy](../../standards/ssl-certificate-trust-model.md).
|
|
65
75
|
|
|
@@ -101,6 +111,10 @@ Remaining follow-ups (not yet done):
|
|
|
101
111
|
|
|
102
112
|
- **Trim each cert line (CRLF).** The `.crt` files in `_underscore/Assets/ssl` are **CRLF-encoded**. The `/meta` cert-format code does `explode("\n", ...)` then indents each line. Without a per-line `trim()`, a stray carriage return (`\r`) is embedded on every base64 line inside `<ds:X509Certificate>` in the metadata XML. Fix: `trim()` each line in the cert-format code — applied to both the signing and encryption blocks. (Latent bug that pre-dated this change in the single original block.)
|
|
103
113
|
- **Signing cannot flip until every IdP loads the new metadata** (IdPs cache up to 7 days) — flipping early makes signature-verifying IdPs reject our login requests.
|
|
114
|
+
- **Use full secret ARNs in `ENCRYPTION_SECRET_NAMES`.** A short name resolves only in the caller's own account; full ARNs work from prod and cross-account.
|
|
115
|
+
- **Non-prod SAML depends on prod KMS + Secrets Manager.** One SP key serves every env — a sandbox leak is a prod key leak. Separate non-prod cert = open decision.
|
|
116
|
+
- **New secrets must use `alias/toga-secrets`**, not `aws/secretsmanager` — the AWS-managed key cannot be shared, so non-prod gets Code 5.
|
|
117
|
+
- **Claude Code auto mode blocks cross-account grants** ("Permission Grant") even with `Bash(aws:*)` allowed — run those aws commands with auto mode off.
|
|
104
118
|
- **The private key is only in Secrets Manager** (`toga/ssl/togahub_com-selfsigned-2036`) — retrieve at cutover, never commit or paste.
|
|
105
119
|
|
|
106
120
|
## Related
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|-----|---------|
|
|
5
5
|
| [TOGa 2.5 Supply — Architecture](architecture.md) | Architecture of toga25-supply — the React/TS Vite SPA frontend for 2.0 Supply, multi-client + meta-driven, built on `@agilant/toga-blox`; open before any struct |
|
|
6
6
|
| [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | Declarative config-driven Flag/Rule grammar that resolves `isEnabled`/`isVisible`/`isComplete` on SalesOrder action-button options and approval stage inputs (ev |
|
|
7
|
+
| [Bundle item pricing (unit / lease rows, edit save path, duplicate rows)](features/bundle-item-pricing.md) | How a bundle item's unit/lease price is read, displayed and SAVED in supply — the two-call save (never nest the price in a PUT), `_unitPrice`/`_leasePrice` + `c |
|
|
7
8
|
| [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | Resolves a client's API-fetch projection (`fields`/`ojoin`/`join`/`where`) from one colocated per-page `apiFields.json` keyed by client slug (useClientApiFields |
|
|
8
9
|
| [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | Config that varies by client (or client × role) — field overrides, filter buttons, group-by, column pickers, layout toggles — resolved through `src/fieldsConfig |
|
|
9
10
|
| [Column State (URL-driven show/hide, order & pinning)](features/column-visibility.md) | URL-driven "Columns" button + modal (`cols` param) for show/hide + order + pinning of table columns, client-configurable then Surface-driven; open for column-vi |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Bundle item pricing (unit / lease rows, edit save path, duplicate rows)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga25-supply
|
|
5
|
+
project: TOGa 2.5 Supply
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-24
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- toga25-supply/src/layout/BundleItemRecordModalLayout/hooks/useBundleItemEditForm.tsx
|
|
13
|
+
- toga25-supply/src/layout/BundleRecordModalLayout/types.ts
|
|
14
|
+
- toga25-supply/src/layout/BundleRecordModalLayout/viewModel/FIELDS/DEFAULT/bundleRecordViewFields.json
|
|
15
|
+
- toga25-supply/src/layout/BundleRecordModalLayout/components/BundleItemsTable.tsx
|
|
16
|
+
- dbchanges2/Client_Compass/2026-09-24a - DeduplicateBundleItemPrices.sql
|
|
17
|
+
related:
|
|
18
|
+
- ../../api2/features/nested-relationship-writes.md
|
|
19
|
+
- ../../api2/features/v2-rest-query-contract.md
|
|
20
|
+
- ../../_underscore/features/calculated-sql-fields.md
|
|
21
|
+
- ./record-modals-and-nested-tables.md
|
|
22
|
+
- ../../../clients/compass-usa/workflows/changing-a-kit-line-item.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
How a bundle item's unit/lease price is read, displayed and SAVED in supply — the two-call save (never nest the price in a PUT), `_unitPrice`/`_leasePrice` + `calcDepth: 2`, and the duplicate `BundleItemPrices` rows.
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
A bundle item has **many** `BundleItemPrices` rows, one per `priceTypeId` (unit, lease-48, …).
|
|
30
|
+
Supply shows Default Price Type / Unit Price / Lease Price in the bundle items table and edits a
|
|
31
|
+
price in the bundle-item modal. Two engine behaviours shape all of it: nested children are never
|
|
32
|
+
updated on a PUT, and the calculated price fields need `calcDepth: 2` when read through a bundle.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
### Reading — `_unitPrice` / `_leasePrice`
|
|
37
|
+
|
|
38
|
+
Both are `FIELD_SQL` calculated fields on `_Model_Client_BundleItem`: they resolve the row
|
|
39
|
+
matching `defaultPriceTypeId` with `ORDER BY id DESC LIMIT 1` (**newest** row wins).
|
|
40
|
+
|
|
41
|
+
- The table columns are sourced from these fields, not from the `bundleItemPrices` array.
|
|
42
|
+
- For **nested** bundle items (inside `GET /v2/bundles/{uuid}`) they need **`calcDepth: 2`**, sent
|
|
43
|
+
as a **plain query param** — inside the base64 `_` blob it is ignored. At the default `calcDepth: 1`
|
|
44
|
+
both fields are silently absent. See
|
|
45
|
+
[V2 REST query contract](../../api2/features/v2-rest-query-contract.md).
|
|
46
|
+
- `priceType` on a nested price row is **never returned** by `GET /v2/bundles/{uuid}` (three levels
|
|
47
|
+
under an inherent child — dropped silently). Re-fetch `GET /v2/bundle-items/{uuid}` when you need
|
|
48
|
+
typed rows.
|
|
49
|
+
|
|
50
|
+
### Saving — two calls, never a nested price in a PUT
|
|
51
|
+
|
|
52
|
+
The V2 engine writes a nested inherent child with `POST` hardcoded, so a price nested in a **PUT**
|
|
53
|
+
inserts a duplicate row (or fails 1062 if you send its uuid) —
|
|
54
|
+
[nested-relationship writes](../../api2/features/nested-relationship-writes.md).
|
|
55
|
+
|
|
56
|
+
1. `PUT /v2/bundle-items/{uuid}` — **without** the nested price.
|
|
57
|
+
2. `PUT /v2/bundle-item-prices/{uuid}` — the price row.
|
|
58
|
+
|
|
59
|
+
**Create still nests** (correct there — it is a POST).
|
|
60
|
+
|
|
61
|
+
`resolvePriceRowUuid` picks the row to update **strictly by `priceType`**. If the payload rows carry
|
|
62
|
+
no `priceType`, it re-fetches typed rows from `/bundle-items/{uuid}`; if the type still cannot be
|
|
63
|
+
established it **creates a new row** rather than overwriting an unknown one. Never fall back to
|
|
64
|
+
"the newest row" — during a unit-price edit that can overwrite the LEASE price.
|
|
65
|
+
|
|
66
|
+
## Gotchas
|
|
67
|
+
|
|
68
|
+
- **⚠ An edited price that looks unchanged on screen is the duplicate-row bug.** `_unitPrice` reads
|
|
69
|
+
the newest row (`ORDER BY id DESC`) while the FE rendered `bundleItemPrices[0]` (the oldest) — the
|
|
70
|
+
insert was invisible in the UI for a long time.
|
|
71
|
+
- **⚠ Duplicate `BundleItemPrices` rows already exist in production data**, predating this front end
|
|
72
|
+
(one bundle item has 15 rows, ids 13→4355). The table has no `dtCreated`/`dtUpdated`, so age cannot
|
|
73
|
+
be proven. Local-copy counts: `Client_Compass` 5117 rows / 2605 duplicates / 971 groups (36 groups
|
|
74
|
+
with differing prices); `Client_CompassCanada` 333 / 75 / 63; all other tenants zero. **Prod counts
|
|
75
|
+
not yet taken.**
|
|
76
|
+
- **Dedupe migrations exist but have only been run locally** —
|
|
77
|
+
`dbchanges2/Client_Compass/2026-09-24a - DeduplicateBundleItemPrices.sql` and the
|
|
78
|
+
`Client_CompassCanada` twin keep `MAX(id)` per `(bundleItemId, priceTypeId)` — the row the
|
|
79
|
+
calculated fields already read, so nothing changes on screen. Safe to run: no foreign key anywhere
|
|
80
|
+
references `BundleItemPrices`, and `SalesOrderItems` snapshots its own price column, so no order or
|
|
81
|
+
quote is affected. `currencyId`/`planId` are NULL on every row.
|
|
82
|
+
- **⚠ FIELDS JSON edits appear to do nothing** — the persisted React Query cache freezes them; see
|
|
83
|
+
[persisted query cache](./persisted-query-cache.md).
|
|
84
|
+
|
|
85
|
+
## Change history
|
|
86
|
+
- 2026-09-24 — Documented the two-call save (price edits were inserting duplicates via a nested PUT),
|
|
87
|
+
the strict-`priceType` row match with create-not-guess fallback, the `_unitPrice`/`_leasePrice`
|
|
88
|
+
columns needing `calcDepth: 2` as a plain query param, and the Compass/Compass Canada
|
|
89
|
+
`BundleItemPrices` dedupe migrations (TRUE-82138). (apeterson)
|
|
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-24
|
|
10
10
|
owners: ["apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/App.tsx
|
|
@@ -145,7 +145,11 @@ Type-check **both** `-p tsconfig.app.json` and `-p tsconfig.node.json` — `vite
|
|
|
145
145
|
|
|
146
146
|
- **⚠ A non-zero `staleTime` on a persisted query serves yesterday's data and never fetches.** Data hooks in this app use `staleTime: 0`. Hooks that currently break this rule **and are persisted**: `useClientFields`, `useCurrentUser`, `useStatusTypeValues`, `useStatusColors`, `useSalesOrderUuidForServiceRequest` (all `Infinity`), and the three VendorItem form hooks (`5 * 60_000`). Each can serve a previous session's answer.
|
|
147
147
|
- `useTalosSession` and `useTalosThreads` also set a non-zero `staleTime`, but their keys start with `talos`, so they are **not** persisted — in-memory only, safe.
|
|
148
|
+
- **⚠ An edit to a client `FIELDS` JSON looks like it does nothing — and it is NOT Vite HMR.** `useClientFields` is `staleTime: Infinity` **and** persisted, so the old config is frozen in `supply-chain-query-cache`. Clear that key to see the edit. Proper fix: exclude `clientFields` in `shouldDehydrateQuery` (`src/App.tsx:42`).
|
|
148
149
|
- **⚠ Never verify a config, surface, or ACL change on a warm browser.** Log out, or delete the `supply-chain-query-cache` key in devtools. Most "it didn't deploy" reports are a rehydrated cache.
|
|
149
150
|
- **⚠ `gcTime: 0` does not stop persistence.** It only evicts a query once it becomes *inactive*; an active query is still dehydrated. To keep something off disk, exclude its namespace in `shouldDehydrateQuery`.
|
|
150
151
|
- **⚠ Any new `queryClient.clear()` must `await awaitRestoration()` first**, or the restore race undoes it.
|
|
151
152
|
- The `maxAge` is one timestamp for the entire blob — you cannot expire a single query through it.
|
|
153
|
+
|
|
154
|
+
## Change history
|
|
155
|
+
- 2026-09-24 — Noted the frozen-`FIELDS`-JSON symptom (persisted `useClientFields` at `staleTime: Infinity`, mistaken for broken Vite HMR) and the `shouldDehydrateQuery` fix. (apeterson)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -33,7 +33,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
33
33
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
34
34
|
- **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
35
35
|
- **toga2-commerce** (TOGa Commerce) — 26 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
36
|
-
- **toga25-supply** (TOGa 2.5 Supply) —
|
|
36
|
+
- **toga25-supply** (TOGa 2.5 Supply) — 22 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
37
37
|
- **toga-blox** (TOGa Blox) — 15 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
38
38
|
- **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
|
|
39
39
|
|
|
@@ -70,6 +70,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
|
|
|
70
70
|
- 🚨 **sandbox-client (`Core` + `Client_Nychh`) was reset from PRODUCTION again on 2026-09-04** — second reset in two days (first, 2026-09-03, over the stale `purchase-orders` TableView). Every "verified on <date>" sandbox-client claim in this profile dated **before 2026-09-04 now describes whatever prod had**. The 2026-09-01 → 2026-09-03 `dbchanges2` migrations may need re-running on sandbox-client. Re-read the environment before trusting any sandbox state; procedure + re-run order in [Targeting the right database](../../2.0/apps/dbchanges2/workflows/targeting-the-right-database.md).
|
|
71
71
|
- **Transfer orders push OUT to NetSuite as $0 sales orders - LIVE and verified 2026-09-08.** NYCHH is the first and only client on that path; switched on purely by its `transfer-orders` (record 312) `POST`/`POST` interceptor row. NetSuite SO **289187** (id 7463537) was created from it. **The NetSuite customer is the CLIENT - 4490 / 28908 - on every transfer order, never the destination location** (a first cut sent the destination and put SO 7462031 on sub-customer 33674 "CHS"; `Locations` 1-26 are "Shipping" type mapping to sub-customers under 4490, and the bug hid because almost every order picks location 2, whose id *is* 28908). Origin location is still the NetSuite **warehouse**, and `TransferOrders.description` now goes out as the NetSuite `memo`, shown as **ORDER DESCRIPTION**. See [TransferOrder → NetSuite push](./features/transfer-order-netsuite-push.md).
|
|
72
72
|
- **Transfer orders closed in NetSuite never reached the Closed stage - 18 stuck on `Pending`, oldest 2022 (fix written 2026-09-08, NOT deployed).** The 1.0 sync skips a `Closed` order before the transfer-order branch, so the importer's `case 'Closed'` was dead code. Two NYCHH gates make this confusing to verify: the transfer-order status refresh is nested inside `IS_ENABLED_INTEGRATION_SALES_ORDERS` (Jeff enabled the feeds 2026-08-28 `b063f05f` and disabled sales orders, POs, invoices and item receipts on 2026-09-03 `2b321e9d` - where every NYCHH cursor stops), and the seek cursor never re-reads a passed order. 🚨 **Their `NETSUITE_LAST_SYNC_CURSOR_ITEM_RECEIPTS` is stale at 2024-08-06**, so re-enabling that feed replays ~2 years. See [NetSuite → TransferOrders import](./features/netsuite-transfer-order-import.md).
|
|
73
|
+
- **INVOICES mode stuck at `5-RUNNING` with no error (2026-09-24, worker fix NOT deployed).** Cursors were reset to 2026-09-24; INVOICES has a 24h hold-back and its skip path never wrote IDLE, so the window collapsed silently. After deploy set `NETSUITE_EXECUTION_MODE_INVOICES` to `864000-IDLE`; invoices modified before the reset need a rewind/re-import. See [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
73
74
|
- **Missing NetSuite records (item fulfillments + others) root-caused and fixed (2026-09-09).** Two separate bugs in the 1.0 NetSuite→TOGa Supply sync starved NYCHH imports, both deployed and verified in prod: (1) the resume cursor was **read on the wrong clock** (`strtotime()` in Chicago vs the Eastern-stored value), shifting the fetch window +1h and **silently skipping** any record in that 1-hour shadow — hit all 6 sections and all clients; and (2) the fulfillment `GET /sales-orders` ran at `depth=5` and **timed out at 900s**, freezing the section before the fulfillment row was written. Proven on IF `6138135` (Eastern 2025-09-04 17:26:58). Fixes + the standing "cursor is always Eastern" rule live in the [per-client sync feature doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
74
75
|
- **ITEM_RECEIPTS froze on a case-mismatched serial → duplicate-`Units` EV-10/1062, root-caused + fixed (2026-09-11, fix NOT deployed).** The receipts feed threw before its cursor PUT (freezing it and ITEM_FULFILLMENTS behind it) because `syncItemReceiptFromNetsuite` keyed its existing-`Units` PHP lookup on the DB's stored serial casing (`'na100'`) but read it with the uppercased serial (`'NA100'`); the missed row took the create path and collided on the case-insensitive `Units.itemId_serialNumber` unique key → MySQL 1062 / api2 EV-10. Proven on `Client_Nychh.Units` 23400-23409 (serials `'na100'..'na109'`, `c_netsuiteInternalInventoryAssignmentId` NULL). Library-only fix (`strtoupper()` on both the key and the reads). This is a **different** duplicate from the open sales-order `Items` duplicate below (658-BFZH). Mechanism + the EV-10 → `Logs_Nychh.Api` diagnosis traps: [per-client sync doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
75
76
|
- **ITEM_FULFILLMENTS freeze RESOLVED — five stacked reconcile bugs fixed, deployed + verified (2026-09-13).** The frozen section (prod: 25,766 unit links for 1,192 real units on one fulfillment) is cleared: (1) unit dedup now reads existing units **flat + fully paginated** instead of from a truncated nested GET; (2) an unmatched fulfillment line **fails loud** instead of creating a never-matchable NULL-parent orphan (self-heal deletes existing orphans); (3) the SO-items fetch is flat + paginated (api2 truncated the nested list at 25); (4) fulfillment→SO line matching prefers the stable `orderLineUniqueKey` over the drifting line number; (5) a part-number last-resort match for lines whose NetSuite link points at a TAX line. **Package tracking numbers now import to all three bridges (317/318/319)** — the shim fetches `/package?expandSubResources=true` (a bare sub-resource GET returned no fields), so one NetSuite tracking number fans to the header, item, and every serial unit. Engine + shim detail: [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md), [SuiteQL/REST shim](../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md).
|
package/package.json
CHANGED