toga-ai 1.0.781 → 1.0.783
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/netsuite-item-assettype-sync.md +202 -0
- package/knowledge/1.0/apps/library/features/netsuite-item-isfulfillable-sync.md +28 -2
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +52 -1
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +62 -0
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/column-visibility.md +11 -1
- package/knowledge/2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md +32 -0
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +76 -1
- package/knowledge/2.0/apps/toga25-supply/features/toolbar-button-chrome.md +26 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/elite/INDEX.md +2 -2
- package/knowledge/clients/elite/features/netsuite-togasupply-sync.md +79 -6
- package/knowledge/clients/elite/features/supply2-tableview-config-drift.md +74 -2
- package/knowledge/clients/elite/profile.md +11 -2
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| [HTTP 500 Error Monitor (App_SystemMonitor_500Error) — and why its \"Error Type\" is not a diagnosis](features/http-500-error-monitor.md) | `App_SystemMonitor_500Error` (`library/app/systemmonitor/500error.php`, title **"HTTP 500 Error Alert"**) is the 1.0 system monitor that watches **`Logs.Api` fo | library/app/systemmonitor/500error.php, worker/crons/infrastructure/system_monitors.php, api2/Controller/Index.php, _underscore/Error.php |
|
|
17
17
|
| [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding pages to one — the folder-based MVC routing, the page lifecycle, t | library/app/framework.php, library/app/frameworkindex.php, library/app/mvc.php, library/app/database.php, library/app/model.php, library/app/config.php |
|
|
18
18
|
| [NetSuite File Cabinet Content Retrieval via RESTlet (fetchInvoiceFile)](features/netsuite-filecabinet-restlet.md) | How 1.0 pulls **File Cabinet binary content** (invoice PDFs) out of NetSuite over REST. | library/app/api/netsuite/rest.php, worker/crons/toga2/netsuite/netsuite_restlet_filecontent.js, worker/config.worker.ini |
|
|
19
|
+
| [assetType from NetSuite itemtype during Item Sync (opt-in per client)](features/netsuite-item-assettype-sync.md) | `getCreateItem()` never sent `assetType`, so **every item the NetSuite importer created had `Items.assetTypeId = NULL`** — for every client, since the importer | library/app/api/toga2.php, library/app/api/netsuite/rest.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php |
|
|
19
20
|
| [isFulfillable from NetSuite during Item Sync (Phase 1)](features/netsuite-item-isfulfillable-sync.md) | This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite `isfulfillable` flag during item sync and stamping it onto the **Agilant | library/app/netsuite.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php |
|
|
20
21
|
| [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem, test/@dave/Junk Drawer/nsq.php |
|
|
21
22
|
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: assetType from NetSuite itemtype during Item Sync (opt-in per client)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [rgirish]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/api/toga2.php
|
|
13
|
+
- library/app/api/netsuite/rest.php
|
|
14
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
15
|
+
- worker/crons/toga2/netsuite/sync_togasupply_elite.php
|
|
16
|
+
related:
|
|
17
|
+
- netsuite-item-isfulfillable-sync.md
|
|
18
|
+
- netsuite-suiteql-rest-shim.md
|
|
19
|
+
- toga2-api-client-and-bridge.md
|
|
20
|
+
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
21
|
+
- ../../../2.0/apps/api2/features/nested-relationship-writes.md
|
|
22
|
+
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
23
|
+
- ../../../clients/elite/features/netsuite-togasupply-sync.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
`getCreateItem()` never sent `assetType`, so **every item the NetSuite importer created had
|
|
29
|
+
`Items.assetTypeId = NULL`** — for every client, since the importer began. The string `assetType`
|
|
30
|
+
appeared nowhere in `library/app/api/toga2.php` nor in `worker/crons/toga2/netsuite/*`.
|
|
31
|
+
|
|
32
|
+
Measured on production `Client_Elite.Items`: **113 rows, 110 with `assetTypeId = NULL`**. The only
|
|
33
|
+
3 non-NULL rows (`Laptop`, `Test`, `Test 2`) have `c_netsuiteInternalItemId = NULL` and
|
|
34
|
+
`inventoryType = HYBRID` — hand-made test rows, not imported ones.
|
|
35
|
+
|
|
36
|
+
This doc covers the fix: NetSuite's `item.itemtype` is now mapped to an `AssetTypes.uuid` and
|
|
37
|
+
stamped on the item. It is **opt-in per client** and deliberately narrow, because two platform
|
|
38
|
+
facts (below) make the obvious implementation break item imports outright.
|
|
39
|
+
|
|
40
|
+
Sibling doc: [isFulfillable](netsuite-item-isfulfillable-sync.md) — the other NetSuite flag stamped
|
|
41
|
+
by the same function. **The two are not symmetric** and must not be implemented the same way; see
|
|
42
|
+
[Why this is not modeled on isFulfillable](#why-this-is-not-modeled-on-isfulfillable).
|
|
43
|
+
|
|
44
|
+
## ⚠⚠ TWO blockers — read before adding this for another client
|
|
45
|
+
|
|
46
|
+
Both were found under CTO review of a first proposal that looked obviously correct. Either one
|
|
47
|
+
turns "the asset type is not set" into **"the item create fails outright."**
|
|
48
|
+
|
|
49
|
+
### 1. `Items.assetTypeId` is not writable by the API role in most tenants
|
|
50
|
+
|
|
51
|
+
api2 **aborts the entire record write** when a payload names a field the authenticating role cannot
|
|
52
|
+
write. The NetSuite sync authenticates as **roleId 3 (API)**. So simply adding `assetType` to the
|
|
53
|
+
payload does not merely fail to set the asset type — it **fails the item create**.
|
|
54
|
+
|
|
55
|
+
Verified on production `Client_Elite.AclFieldPermissions`:
|
|
56
|
+
|
|
57
|
+
| recordFieldId | Field | Roles granted |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| **269** | `Items.assetTypeId` | roleId **1 (Base) only** |
|
|
60
|
+
| 107 | `Items.manufacturerId` | roleId **1 and 3 (API)** |
|
|
61
|
+
|
|
62
|
+
**A `dbchanges2/Client_<Tenant>/` grant must land BEFORE the code deploy.** Elite's is
|
|
63
|
+
`dbchanges2/Client_Elite/2026-09-04a - EliteItemAssetTypeApiWritePermission.sql`. Mechanics:
|
|
64
|
+
[ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
|
|
65
|
+
|
|
66
|
+
### 2. `assetTypeId`'s childPolicy is `MATCH`, not `MATCH_UPSERT` — so get-or-create does NOT work
|
|
67
|
+
|
|
68
|
+
`getCreateManufacturer()` sits in the **same function** and does a get-or-create by name. **That
|
|
69
|
+
pattern does not transfer.** Manufacturers auto-create precisely because their field is
|
|
70
|
+
`MATCH_UPSERT`; `assetTypeId` is plain `MATCH`.
|
|
71
|
+
|
|
72
|
+
Verified on production `Core.RecordFields`: id **269** (`assetTypeId`) = `MATCH`; id **107**
|
|
73
|
+
(`manufacturerId`) = `MATCH_UPSERT`.
|
|
74
|
+
|
|
75
|
+
With `MATCH`, sending `assetType: {name: 'Services'}` for a name that has no row is a
|
|
76
|
+
**field-reference error that fails the record** — it does not create the row. Correct approach:
|
|
77
|
+
**resolve name → uuid up front (`GET /asset-types`) and send `{uuid}`.** Policy semantics:
|
|
78
|
+
[nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
|
|
79
|
+
|
|
80
|
+
> **General lesson:** never reason by analogy from a sibling nested write in the same function.
|
|
81
|
+
> Check that field's own `Core.RecordFields.childPolicy` first. Two fields on the same record
|
|
82
|
+
> routinely differ.
|
|
83
|
+
|
|
84
|
+
## Key files / entry points
|
|
85
|
+
|
|
86
|
+
- `library/app/api/netsuite/rest.php` — `fetchItemFlagsByIds()` now selects `itemtype` in its
|
|
87
|
+
SuiteQL and exposes it as `->itemType`. **Free**: that query already selected from `item` once
|
|
88
|
+
per 500-id batch, so no extra round-trip. Returns `null` for an empty itemtype.
|
|
89
|
+
- `library/app/api/toga2.php`
|
|
90
|
+
- `getAssetTypeUuidForNetsuiteItemType()` (~6358) — new private helper. Resolves a NetSuite
|
|
91
|
+
itemtype to an `AssetTypes.uuid` through the launcher's opt-in map. Returns `null` when the
|
|
92
|
+
client has not opted in or the itemtype is unmapped; **throws** when a configured mapping names
|
|
93
|
+
an asset type the client does not have.
|
|
94
|
+
- `getCreateItem()` (~6474) — sends `assetType: {uuid}` on create, and **fills it on existing
|
|
95
|
+
items only when currently NULL**.
|
|
96
|
+
- `worker/crons/toga2/netsuite/common_sync_togasupply.php` (~298) — the bulk `/items` GET now
|
|
97
|
+
requests `assetType => ['uuid']`. **Without this the fill-NULL-only check has nothing to read and
|
|
98
|
+
would re-PUT every item every run** (the same trap `isFulfillable` fell into — see its doc).
|
|
99
|
+
- `worker/crons/toga2/netsuite/sync_togasupply_elite.php` — the only launcher that opts in today.
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
1. `fetchItemFlagsByIds()` returns `->itemType` alongside the serialized / fulfillable flags, one
|
|
104
|
+
SuiteQL query per 500 items.
|
|
105
|
+
2. `getCreateItem()` calls `getAssetTypeUuidForNetsuiteItemType($itemFlags->itemType, …)`.
|
|
106
|
+
3. The helper reads the launcher constant `NETSUITE_ITEM_TYPE_TO_ASSET_TYPE_NAME`
|
|
107
|
+
(`itemtype => AssetTypes.name`). **If the constant is not defined, it returns `null`** and the
|
|
108
|
+
`assetType` key is **omitted from the payload entirely**.
|
|
109
|
+
4. On first need it `GET /asset-types` once and builds a `strtoupper(name) => uuid` map, skipping
|
|
110
|
+
rows with a NULL/blank name. **The cache is keyed by client uuid** — one worker process can
|
|
111
|
+
handle several clients in a run.
|
|
112
|
+
5. Name not found → **throw**, naming the itemtype, the mapped name, and the fix. Fail loud, per the
|
|
113
|
+
team rule for import/sync/cron code.
|
|
114
|
+
6. Create → `assetType: {uuid}` in the POST. Existing item → set it **only if
|
|
115
|
+
`$existingItem->assetType` is NULL**.
|
|
116
|
+
|
|
117
|
+
## Design rules (and why each one)
|
|
118
|
+
|
|
119
|
+
| Rule | Why |
|
|
120
|
+
|---|---|
|
|
121
|
+
| **Opt-in via a launcher constant** | `getCreateItem()` is shared by ~20 client sync launchers. Undefined constant ⇒ the payload key is absent ⇒ the other ~19 clients are **byte-for-byte unaffected**. |
|
|
122
|
+
| **Link by uuid, never by name** | `assetTypeId` is `MATCH` — a name for a missing row fails the whole record (blocker 2). |
|
|
123
|
+
| **Never auto-create an `AssetTypes` row** | `AssetTypes` is free text with per-client `AUTO_INCREMENT` ids and **no slug or code**, so nothing marks a row as sync-made vs. operator-made. Verified: `Client_Elite.AssetTypes` has 2 rows (1=`Laptop`, 2=`Services`); `Client_Compass.AssetTypes` has **38 hand-curated rows** (`ACCESSORY`, `EQUIPMENT`, `FEE`, `CONSULTING`, `WARRANTY`, `HP LAPTOP ACCESSORIES`, … plus an **empty-name row at id 23**) and **no `Services` row at all**. Auto-creating would pollute that taxonomy. |
|
|
124
|
+
| **Fail loudly on a missing asset type** | A silent skip leaves a client half-stamped with no signal. |
|
|
125
|
+
| **Fill-NULL-only on existing items** | `assetTypeId` is shared, **hand-curated per-client** data — unlike `isFulfillable`, which is a boolean NetSuite owns. Also means a manual TOGa correction of a NetSuite-mis-typed item **survives later syncs**. |
|
|
126
|
+
| **Cache keyed by client uuid** | One worker process can service several clients. |
|
|
127
|
+
|
|
128
|
+
## Why this is not modeled on isFulfillable
|
|
129
|
+
|
|
130
|
+
[isFulfillable](netsuite-item-isfulfillable-sync.md) **refreshes on every difference** — and that
|
|
131
|
+
doc's own ⚠ CRITICAL section records the cost: the refresh **reverts any local override**, so a
|
|
132
|
+
hand fix silently disappears within a day.
|
|
133
|
+
|
|
134
|
+
`assetType` deliberately does the opposite (**fill-NULL-only**). NetSuite does not own this field;
|
|
135
|
+
the client's operators do. Do not "make it consistent" with `isFulfillable` — the asymmetry is the
|
|
136
|
+
design.
|
|
137
|
+
|
|
138
|
+
## NetSuite `itemtype` does NOT cleanly identify services
|
|
139
|
+
|
|
140
|
+
`item.itemtype` **is** populated (account-wide: InvtPart 51336, NonInvtPart 2652, Group 585,
|
|
141
|
+
Service 472, Kit 50, OthCharge 24, Discount 17, Expense 6, Description 3). But **`NonInvtPart` is
|
|
142
|
+
mixed** — it holds real services. For Elite (113 items: 65 InvtPart, 11 Service, 17 NonInvtPart),
|
|
143
|
+
`itemtype = 'Service'` alone marks only **11 of ~28** real services; the rest sit in `NonInvtPart`
|
|
144
|
+
(`SVC-FS-DEPLOY`, `SVC-FS-SHIPPING-*`, `SVC-CI-RETAINER-RS`, `SVC-TS-Removal`, `Project - Cabling`).
|
|
145
|
+
|
|
146
|
+
So a usable mapping must include the non-inventory types, not just `Service`. Elite's map is in
|
|
147
|
+
[Elite's sync doc](../../../clients/elite/features/netsuite-togasupply-sync.md).
|
|
148
|
+
|
|
149
|
+
**Residual gap: NetSuite data hygiene, not a code problem.** Three Elite items are typed
|
|
150
|
+
`InvtPart` in NetSuite though they are services (`SVC-TS-ELITE-HDONBOARDING`, `CONFIG/INSTALL`,
|
|
151
|
+
`CONF-RM-INSTALL-SUPP`). **Decided: this is a client data-hygiene ask, NOT a code special case** —
|
|
152
|
+
a part-number allowlist inside a 20-client shared import is unmaintainable. Fill-NULL-only means a
|
|
153
|
+
manual TOGa correction sticks.
|
|
154
|
+
|
|
155
|
+
## Gotchas / known issues
|
|
156
|
+
|
|
157
|
+
- **⚠ Existing items stay NULL until a cursor reset.** `getCreateItem()` only runs when a
|
|
158
|
+
transaction line references an item, so already-imported items are **not** back-stamped by
|
|
159
|
+
turning this on. Elite's 110 NULL items stay NULL until a cursor rollback makes the sync re-walk
|
|
160
|
+
transactions that reference them. Cursors live in the **client** database `Parameters` table
|
|
161
|
+
(key `NETSUITE_LAST_SYNC_CURSOR_*`, value `"<lastmodifieddate>|<netsuiteInternalId>"`), read via
|
|
162
|
+
`GET /parameters`. See [per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md).
|
|
163
|
+
- **⚠ Lumping `Discount` / `Expense` / `Description` into a "Services" asset type is semantically
|
|
164
|
+
loose.** Harmless while `assetTypeId` only hides rows on a page — a **latent bug if it ever drives
|
|
165
|
+
billing or reporting.** Revisit the map before wiring `assetTypeId` into either.
|
|
166
|
+
- **An empty-name `AssetTypes` row exists in the wild** (`Client_Compass` id 23). The name→uuid
|
|
167
|
+
cache skips NULL/blank names so it can never be matched by accident.
|
|
168
|
+
- **⚠ PRE-EXISTING BUG, deliberately NOT fixed — needs its own ticket.** The fulfillability refresh
|
|
169
|
+
(`toga2.php` ~6547) reads `$existingItem->isFulfillable`, but the bulk `GET /items` lookup in
|
|
170
|
+
`common_sync_togasupply.php` **never requested that field**. So `$currentFulfillable` is always
|
|
171
|
+
`null` and the sync almost certainly **PUTs `isFulfillable` on every item, every run, for every
|
|
172
|
+
client**. Left untouched on purpose: fixing it changes behavior for all ~20 clients and was out of
|
|
173
|
+
scope. This is the exact trap `assetType` avoids by requesting `assetType => ['uuid']` in the same
|
|
174
|
+
lookup.
|
|
175
|
+
|
|
176
|
+
## Change history
|
|
177
|
+
|
|
178
|
+
- 2026-09-04 — **Built assetType stamping from NetSuite `itemtype`.** Root-caused that
|
|
179
|
+
`getCreateItem()` never sent `assetType`, so every imported item had `assetTypeId = NULL` (prod
|
|
180
|
+
`Client_Elite`: 110 of 113). Added `itemtype` to `fetchItemFlagsByIds()`'s existing SuiteQL
|
|
181
|
+
(free), the `getAssetTypeUuidForNetsuiteItemType()` helper, `assetType => ['uuid']` to the bulk
|
|
182
|
+
`/items` GET, and the create/fill paths. Recorded the **two blockers** that break the naive
|
|
183
|
+
version — `Items.assetTypeId` (recordField 269) is granted to **roleId 1 only** while the sync
|
|
184
|
+
authenticates as roleId 3, and api2 fails the **whole** record write on a non-writable field; and
|
|
185
|
+
269's childPolicy is **`MATCH`**, not the `MATCH_UPSERT` that makes the sibling
|
|
186
|
+
`getCreateManufacturer()` get-or-create work, so a name-only nested object is a hard failure.
|
|
187
|
+
Design: opt-in per launcher, link by uuid, never auto-create an `AssetTypes` row, fail loud on a
|
|
188
|
+
missing one, **fill-NULL-only** (the deliberate opposite of `isFulfillable`'s revert-everything
|
|
189
|
+
refresh). Also collapsed the existing-item branch's **three separate PUTs to the same
|
|
190
|
+
`/items/<uuid>`** into one, and recorded the pre-existing `isFulfillable`-never-requested bug as a
|
|
191
|
+
separate ticket. (rgirish)
|
|
192
|
+
|
|
193
|
+
## Related docs
|
|
194
|
+
|
|
195
|
+
- [isFulfillable from NetSuite during Item Sync](netsuite-item-isfulfillable-sync.md) — the sibling
|
|
196
|
+
flag on the same function, with the opposite refresh policy.
|
|
197
|
+
- [Nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md) —
|
|
198
|
+
`MATCH` vs `MATCH_UPSERT` and why link-by-uuid is the only safe form.
|
|
199
|
+
- [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md) — the
|
|
200
|
+
field-write grant this feature needs per tenant.
|
|
201
|
+
- [Elite NetSuite → TOGa Supply sync](../../../clients/elite/features/netsuite-togasupply-sync.md)
|
|
202
|
+
— the only client opted in, and its itemtype map.
|
|
@@ -6,14 +6,15 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [bala, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/netsuite.php
|
|
13
13
|
- library/app/api/toga2.php
|
|
14
14
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
15
15
|
- worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php
|
|
16
16
|
related:
|
|
17
|
+
- netsuite-item-assettype-sync.md
|
|
17
18
|
- toga2-api-client-and-bridge.md
|
|
18
19
|
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
19
20
|
- ../../worker/workflows/isfulfillable-multi-client-backfill.md
|
|
@@ -116,6 +117,22 @@ sweep that started 13:45 and set every item to `1`.
|
|
|
116
117
|
each verified to trace through an Agilant source.
|
|
117
118
|
|
|
118
119
|
## Gotchas / known issues
|
|
120
|
+
- **⚠⚠ The refresh's diff is BROKEN — `isFulfillable` is never read back, so it is PUT on every
|
|
121
|
+
item, every run, for every client.** The refresh at `toga2.php` ~6547 compares NetSuite's value
|
|
122
|
+
against `$existingItem->isFulfillable`, but the bulk `GET /items` lookup in
|
|
123
|
+
`common_sync_togasupply.php` **never requested that field** — so `$currentFulfillable` is always
|
|
124
|
+
`null` and the diff can never match. The 2026-07-28 entry below claims the field was added to that
|
|
125
|
+
field list; **it is not there** (verified 2026-09-04). Net effect: the "diff-only PUT keeps
|
|
126
|
+
re-syncs no-op" guarantee does not hold. **Not fixed — needs its own ticket**, because a fix
|
|
127
|
+
changes write volume for all ~20 clients. The sibling
|
|
128
|
+
[assetType feature](netsuite-item-assettype-sync.md) avoids this by requesting
|
|
129
|
+
`assetType => ['uuid']` in the same lookup.
|
|
130
|
+
- **The existing-item branch now sends ONE PUT, not three.** It previously issued up to three
|
|
131
|
+
separate PUTs to the same `/items/<uuid>` with identical options (`inventoryType`,
|
|
132
|
+
`isFulfillable`, and now `assetType`). One payload is collected and a single PUT is sent when
|
|
133
|
+
non-empty. Side effect worth knowing: `inventoryType` was previously never written back to the
|
|
134
|
+
in-run lookup, so a repeat line for the same part number re-PUT it — all three fields now update
|
|
135
|
+
the lookup.
|
|
119
136
|
- **Value stamped only on the Agilant source item here** — the client-facing copy is set by the 2.0
|
|
120
137
|
interceptor. If the interceptor rows aren't deployed in the target env, api2 **403s the whole item
|
|
121
138
|
write** on the unknown `isFulfillable` field (see the Phase-2 doc's deploy gotchas).
|
|
@@ -125,6 +142,15 @@ sweep that started 13:45 and set every item to `1`.
|
|
|
125
142
|
in `Client_Compass.Apis` (name `Agilant`) — never reproduce the secret value.
|
|
126
143
|
|
|
127
144
|
## Change history
|
|
145
|
+
- 2026-09-04 — **Two corrections found while building the sibling
|
|
146
|
+
[assetType stamping](netsuite-item-assettype-sync.md) on the same function.** (1) The
|
|
147
|
+
existing-item **refresh diff never works**: the bulk `GET /items` field list in
|
|
148
|
+
`common_sync_togasupply.php` does **not** request `isFulfillable` (contrary to the 2026-07-28
|
|
149
|
+
entry below), so `$currentFulfillable` is always `null` and the flag is almost certainly PUT on
|
|
150
|
+
every item, every run, for every client. Recorded as a separate ticket — not fixed, because it
|
|
151
|
+
changes write volume for ~20 clients. (2) The existing-item branch's up-to-**three** PUTs to the
|
|
152
|
+
same `/items/<uuid>` were collapsed into a **single** PUT, and `inventoryType` is now written back
|
|
153
|
+
to the in-run lookup (it previously was not, so a repeat part number re-PUT it). (rgirish)
|
|
128
154
|
- 2026-08-12 — Prod investigation (no code change): recorded that the **existing-item refresh
|
|
129
155
|
reverts any local override** (NetSuite returns `T` for services; audit-log proof on Compass items
|
|
130
156
|
2382/2384/2385, stamped NULL→1 on 2026-08-04), so an override must be enforced by a write-time
|
|
@@ -7,13 +7,14 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-09-04
|
|
10
|
-
owners: ["bala", "mhammontree", "tcox", "apeterson"]
|
|
10
|
+
owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/ContactEmailAddress.php
|
|
14
14
|
- _underscore/Model/Client/TransferOrderStage.php
|
|
15
15
|
related:
|
|
16
16
|
- ../architecture.md
|
|
17
|
+
- ../../../1.0/apps/library/features/netsuite-item-assettype-sync.md
|
|
17
18
|
- ../../toga2-supply/features/currency-amount-lines-editor.md
|
|
18
19
|
- cxml-shipnotice-gateway.md
|
|
19
20
|
- ../../../clients/aig/features/entitlement-intake.md
|
|
@@ -215,6 +216,46 @@ business keys to uuids in the caller. (Also note `Units.itemId` is NOT NULL, so
|
|
|
215
216
|
omitted; and the `Units` identifier set is `uuid`/`itemId`/`serialNumber`/`assetTag`/`vendorId`/
|
|
216
217
|
`macAddress`, so item + serialNumber is what gives find-or-create on a device.)
|
|
217
218
|
|
|
219
|
+
### Plain `MATCH` (no UPSERT) makes a name-only nested object a HARD FAILURE — and a sibling field is no guide
|
|
220
|
+
|
|
221
|
+
`MATCH_UPSERT` creates the missing child. **Plain `MATCH` does not** — sending a nested object that
|
|
222
|
+
matches nothing is a **field-reference error that fails the whole record write**. It does not
|
|
223
|
+
"skip the field."
|
|
224
|
+
|
|
225
|
+
This bites hardest when the same function already does a working get-or-create on a *sibling* field
|
|
226
|
+
of the *same* record, because the pattern looks obviously reusable and is not. Measured on
|
|
227
|
+
production `Core.RecordFields` for `Items`:
|
|
228
|
+
|
|
229
|
+
| recordFieldId | Field | childPolicy | So a name-only nested object… |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| **107** | `Items.manufacturerId` | `MATCH_UPSERT` | **creates** the manufacturer — get-or-create by name works |
|
|
232
|
+
| **269** | `Items.assetTypeId` | `MATCH` | **fails the entire item write** |
|
|
233
|
+
|
|
234
|
+
The 1.0 NetSuite importer's `getCreateItem()` contains **both** calls. `getCreateManufacturer()`
|
|
235
|
+
sends `manufacturer: {name}` and has always worked; copying that shape for `assetType: {name}`
|
|
236
|
+
would have broken item creation outright.
|
|
237
|
+
|
|
238
|
+
> **Rule: before writing a nested object, check that field's OWN
|
|
239
|
+
> `Core.RecordFields.childPolicy`.** Never reason by analogy from a sibling field on the same
|
|
240
|
+
> record — two fields on one record routinely differ (the ASN tracking bridges above are the same
|
|
241
|
+
> lesson on three sibling bridges).
|
|
242
|
+
>
|
|
243
|
+
> **On a `MATCH` field, always resolve the business key to a `uuid` in the caller and send
|
|
244
|
+
> `{uuid}`.** For `AssetTypes` that means a `GET /asset-types` name→uuid map up front.
|
|
245
|
+
|
|
246
|
+
Worked case:
|
|
247
|
+
[assetType from NetSuite itemtype](../../../1.0/apps/library/features/netsuite-item-assettype-sync.md).
|
|
248
|
+
|
|
249
|
+
#### The other half of the same trap: a non-writable field fails the WHOLE record, not just the field
|
|
250
|
+
|
|
251
|
+
Same write, different gate. api2 **aborts the entire record write** when the payload names a field
|
|
252
|
+
the authenticating role cannot write — so an ACL gap on one nested FK reads as "creates stopped
|
|
253
|
+
working," not "that field is empty." Verified on production `Client_Elite.AclFieldPermissions`:
|
|
254
|
+
recordField **269** (`assetTypeId`) had **roleId 1 only**, while **107** (`manufacturerId`) had
|
|
255
|
+
**1 and 3**; the NetSuite sync authenticates as roleId 3. **The `dbchanges2/Client_<Tenant>/` grant
|
|
256
|
+
must land before the code that sends the field.** Chain details:
|
|
257
|
+
[ACL permission chain](../../_underscore/features/acl-permission-chain.md).
|
|
258
|
+
|
|
218
259
|
### Fix it with a migration — a manual one-row data patch on non-prod DOES NOT SURVIVE
|
|
219
260
|
|
|
220
261
|
The TRUE-79978 beta fix was applied by hand as a single `UPDATE` on 2026-07-09. The **identical
|
|
@@ -357,6 +398,16 @@ transferOrderStage: { id: 7 }
|
|
|
357
398
|
request time from a stable natural key (name/slug), because ids differ per tenant DB. Distinguished
|
|
358
399
|
this from the existing `searchableIdentifierFields: ["id"]` flavor, which is a missing
|
|
359
400
|
`isIdentifier` on a `c_` field. Found building the transfer-order create POST. (apeterson)
|
|
401
|
+
- 2026-09-04 — Recorded that **plain `MATCH` (without UPSERT) makes a name-only nested object a hard
|
|
402
|
+
failure of the whole record write**, not a skipped field — and that **a sibling field on the same
|
|
403
|
+
record is no guide**. Prod `Core.RecordFields` on `Items`: **107** (`manufacturerId`) is
|
|
404
|
+
`MATCH_UPSERT` so `getCreateManufacturer()`'s get-or-create-by-name works, while **269**
|
|
405
|
+
(`assetTypeId`) is `MATCH`, so copying that shape into the same function would have broken item
|
|
406
|
+
creation. Rule: check the field's own `childPolicy`, and on a `MATCH` field resolve the business
|
|
407
|
+
key to a `uuid` in the caller. Also recorded the paired ACL gate — api2 aborts the **entire**
|
|
408
|
+
record write on a non-writable field (269 was granted roleId 1 only; the NetSuite sync
|
|
409
|
+
authenticates as roleId 3), so the tenant grant migration must precede the code deploy. Surfaced
|
|
410
|
+
under CTO review of the 1.0 NetSuite assetType import. (rgirish)
|
|
360
411
|
|
|
361
412
|
- 2026-08-26 — Added: **the UPDATE path sets status 200 unconditionally after its children loop**, so
|
|
362
413
|
a nested `PUT` reports success even when a child write failed (unique-key rejection, ACL denial).
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
6
|
| [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql |
|
|
7
|
-
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client_Compass/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql, dbchanges2/Client_CompassCanada/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Core/2026-09-03c - ColumnsModalSurfaceSeed.sql, dbchanges2/Core/2026-09-03d - ColumnsModalRetireConfigModal.sql |
|
|
7
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client_Compass/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql, dbchanges2/Client_CompassCanada/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql, dbchanges2/Core/2026-09-03a - TransferOrderStatusBadgeRecordFields.sql, dbchanges2/Core/2026-09-03b - InventoryCreateTransferOrderAction.sql, dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql, dbchanges2/Client_Nychh/2026-09-03b - InventoryCreateTransferOrderShow.sql, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Core/2026-09-03c - ColumnsModalSurfaceSeed.sql, dbchanges2/Core/2026-09-03d - ColumnsModalRetireConfigModal.sql |
|
|
8
8
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
9
9
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
10
10
|
| [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
|
|
@@ -11,6 +11,10 @@ owners: [jcardinal, apeterson, tcox]
|
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql
|
|
13
13
|
- dbchanges2/Client_CompassCanada/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql
|
|
14
|
+
- dbchanges2/Core/2026-09-03a - TransferOrderStatusBadgeRecordFields.sql
|
|
15
|
+
- dbchanges2/Core/2026-09-03b - InventoryCreateTransferOrderAction.sql
|
|
16
|
+
- dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql
|
|
17
|
+
- dbchanges2/Client_Nychh/2026-09-03b - InventoryCreateTransferOrderShow.sql
|
|
14
18
|
- dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql
|
|
15
19
|
- dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql
|
|
16
20
|
- dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
|
|
@@ -145,6 +149,25 @@ per-tenant deltas live in **Client** (mirrors `TableViews`/ACL/`ItemTranslations
|
|
|
145
149
|
`value varchar(255)` + `c_longValue mediumtext`. Precedence **base < client < persona < role**
|
|
146
150
|
with language as an orthogonal overlay; the resolver applies it most-specific-last in one pass.
|
|
147
151
|
|
|
152
|
+
### Which family is this table in? — the cheat sheet a UI-layer migration actually needs
|
|
153
|
+
|
|
154
|
+
Every `.sql` file for a UI-layer change has to be checked statement-by-statement against the
|
|
155
|
+
**cluster-isolation hard rule** (see the [dbchanges2 architecture](../architecture.md)), and the
|
|
156
|
+
tables that come up over and over split cleanly. **Verified placement:**
|
|
157
|
+
|
|
158
|
+
| Family | Tables |
|
|
159
|
+
|---|---|
|
|
160
|
+
| **CORE** | `Records`, `RecordFields`, `Messages`, `Surfaces`, `SurfaceElements`, `Actions`, `Vocabularies`, `VocabularyTerms` |
|
|
161
|
+
| **CLIENT** (per tenant) | `TableViews`, `TableViewJoins`, `TableViewFields`, `AclRecordPermissions` / `AclFieldPermissions` / the rest of `Acl*`, `SurfaceOverrides`, `MessageTranslations`, `ThemeTokens` |
|
|
162
|
+
|
|
163
|
+
Proof for the TableView half, rather than assumption: `_underscore/Model/Client/TableView.php:74`
|
|
164
|
+
declares `database: _underscore::DB_CLIENT`. The Core half is the `Model/Core/*` counterpart.
|
|
165
|
+
|
|
166
|
+
**The practical consequence:** a badge, a button or a column almost always needs **two files** — a
|
|
167
|
+
`Core/` one and a `Client_<Tenant>/` one — and **every Core id the client file needs must be inlined
|
|
168
|
+
as a literal, never sub-selected**. A `SELECT … FROM Core.…` inside a `Client_*` file cannot resolve
|
|
169
|
+
in production and the hook refuses the write.
|
|
170
|
+
|
|
148
171
|
## SurfaceElements — two orthogonal axes + edit-mode columns (one tree serves view + edit)
|
|
149
172
|
|
|
150
173
|
A record modal is modeled as **ONE** set of `<record>-record-*` SECTION surfaces (a header +
|
|
@@ -872,6 +895,14 @@ rule resumes.
|
|
|
872
895
|
|
|
873
896
|
## Gotchas
|
|
874
897
|
|
|
898
|
+
- **🚨 A reserved-id comment block is NOT evidence.** Repo comments claimed `SurfaceElements`
|
|
899
|
+
reserved through 218; prod's `MAX(id)` was 220 and sandbox-client's was 232 (2026-09-03). Seeds
|
|
900
|
+
using AUTO_INCREMENT + `NOT EXISTS` consume ids invisibly and land differently per environment.
|
|
901
|
+
**Measure `MAX(id)` in every environment before picking a literal.**
|
|
902
|
+
- **`TableViews` / `TableViewJoins` / `TableViewFields` / `Acl*` are CLIENT tables**
|
|
903
|
+
(`_underscore/Model/Client/TableView.php:74` → `DB_CLIENT`); `Records` / `RecordFields` /
|
|
904
|
+
`Messages` / `Surfaces` / `SurfaceElements` are **CORE**. A UI-layer change therefore needs two
|
|
905
|
+
files, and the client file must inline every Core id as a literal.
|
|
875
906
|
- **🚨 SHIP-TOGETHER: `Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql`
|
|
876
907
|
MUST be in the SAME release as `Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql`.**
|
|
877
908
|
`Client_Nychh/2026-08-13a` resolved its Core ids via a cross-DB join and **stored the authoring
|
|
@@ -1058,6 +1089,22 @@ exactly (`MAX(Surfaces.id)` 44, `MAX(SurfaceElements.id)` 160) — that was **ve
|
|
|
1058
1089
|
Using literals is still correct: a collision then fails loudly on the primary key instead of silently
|
|
1059
1090
|
mis-wiring (which is what `INSERT IGNORE` did in the 2026-08-24 incident above).
|
|
1060
1091
|
|
|
1092
|
+
#### 🚨 The reserved-id COMMENTS in this repo do not match reality — and they drift PER ENVIRONMENT
|
|
1093
|
+
|
|
1094
|
+
The comment blocks at the top of the seed files are a record of what was **claimed**, not of what is
|
|
1095
|
+
**taken**. Verified 2026-09-03: the repo comments said `SurfaceElements` were reserved through
|
|
1096
|
+
**218**, while `SELECT MAX(id)` returned **220 on production** and **232 on sandbox-client**.
|
|
1097
|
+
|
|
1098
|
+
**Cause:** several seeds insert `SurfaceElements` with **AUTO_INCREMENT behind a `NOT EXISTS`
|
|
1099
|
+
guard** (the purely-additive pattern documented below) rather than with a literal id. Those rows are
|
|
1100
|
+
real and consume ids, but they leave no literal anywhere for a later author to read — and because
|
|
1101
|
+
each environment runs them at a different time against a different ceiling, the ids they land on
|
|
1102
|
+
**differ per environment**.
|
|
1103
|
+
|
|
1104
|
+
> **Rule: measure `MAX(id)` in EVERY environment before picking a literal id.** Never take the
|
|
1105
|
+
> number from a comment block, a change-history entry, or this doc. The two authoring patterns are
|
|
1106
|
+
> both legitimate, but mixing them means the repo can never be an id ledger.
|
|
1107
|
+
|
|
1061
1108
|
#### Ceiling snapshot, verified read-only 2026-08-28 (before reserving the transfer-order block)
|
|
1062
1109
|
|
|
1063
1110
|
| Environment | `MAX(Surfaces.id)` | `MAX(SurfaceElements.id)` | `MAX(Messages.id)` |
|
|
@@ -1383,6 +1430,21 @@ still unrun at the time, so this is a reservation, not a deployed state.
|
|
|
1383
1430
|
rendered empty. Also recorded the **FE-first ship order**: the override alone exposes an
|
|
1384
1431
|
unformatted value and crashes the modal (React error #31). Run on **client-sandbox `Client_Compass`
|
|
1385
1432
|
only**; prod and Canada pending. (tcox)
|
|
1433
|
+
- 2026-09-04 — 🚨 **The reserved-id comment blocks in this repo do NOT match reality, and drift per
|
|
1434
|
+
environment.** Comments said `SurfaceElements` were reserved through **218**; `SELECT MAX(id)`
|
|
1435
|
+
returned **220 on production** and **232 on sandbox-client**. Cause: several seeds insert
|
|
1436
|
+
`SurfaceElements` with **AUTO_INCREMENT behind a `NOT EXISTS` guard** instead of a literal id, so
|
|
1437
|
+
the rows consume ids invisibly and land on different numbers in each environment. Rule recorded:
|
|
1438
|
+
**measure `MAX(id)` in every environment before picking a literal** — a comment records a claim,
|
|
1439
|
+
not a reservation. Also added a **verified Core-vs-Client placement cheat sheet** for the tables a
|
|
1440
|
+
UI-layer migration touches (`TableViews`/`TableViewJoins`/`TableViewFields`/`Acl*`/
|
|
1441
|
+
`SurfaceOverrides`/`MessageTranslations`/`ThemeTokens` = **CLIENT**, proven at
|
|
1442
|
+
`_underscore/Model/Client/TableView.php:74` → `DB_CLIENT`; `Records`/`RecordFields`/`Messages`/
|
|
1443
|
+
`Surfaces`/`SurfaceElements`/`Actions`/`Vocabularies`/`VocabularyTerms` = **CORE**), because a
|
|
1444
|
+
badge or button change is almost always a two-file change and every Core id in the client file
|
|
1445
|
+
must be an inlined literal. Confirmed on the transfer-order status badge + Inventory
|
|
1446
|
+
create-transfer-order button seeds (`Core/2026-09-03a`+`b`, `Client_Nychh/2026-09-03a`+`b`), which
|
|
1447
|
+
again followed the Core-seeds-OFF / tenant-opts-in default. (apeterson)
|
|
1386
1448
|
- 2026-09-03 — Recorded the rule that **a modal needing surface-driven copy becomes its own Surface**:
|
|
1387
1449
|
the resolver reads messages only from `*MessageId` columns and never from `config`, so
|
|
1388
1450
|
`Core/2026-08-28b`'s `config.modal` keys could never resolve and the Columns modal rendered raw
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
|
|
11
11
|
| [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/TransferOrders/viewModel/useTransferOrdersPageViewModel.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 |
|
|
12
12
|
| [Persisted React Query cache (localStorage `supply-chain-query-cache`)](features/persisted-query-cache.md) | `localStorage["supply-chain-query-cache"]` is **not a hand-written cache**. | toga25-supply/src/App.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/hooks/useCurrentUser.ts, toga25-supply/src/hooks/useStatusTypeValues.ts, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/vite.config.ts |
|
|
13
|
-
| [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/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, 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/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
|
|
13
|
+
| [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/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
|
|
14
14
|
| [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
15
15
|
| [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
|
|
16
16
|
| [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/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, 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/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, 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, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
@@ -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-04
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/components/ColumnVisibilityModal/
|
|
@@ -187,6 +187,11 @@ leaves a clean URL, and a column added to the meta later still shows up for that
|
|
|
187
187
|
|
|
188
188
|
## Gotchas
|
|
189
189
|
|
|
190
|
+
- **⚠ The six-page `useColumnState` migration cannot be reverted file-by-file.** The shared
|
|
191
|
+
`ColumnVisibilityModalLabels` type gained **required** fields (`shown`, `hidden`, `lockedTip`,
|
|
192
|
+
`pinnedTip`), so restoring an old call site no longer type-checks — the build breaks. Roll the
|
|
193
|
+
whole change back together or not at all. (The dead `columnsButton` return in the SalesOrders view
|
|
194
|
+
model was deleted in the same pass.)
|
|
190
195
|
- **No hardcoded copy in the component** — every user-facing string (incl. Close `aria-label`) is a
|
|
191
196
|
required `labels` prop. On the **legacy** path the fallbacks live in the view model's resolution
|
|
192
197
|
(never in the component or default props); on the **surface** path there are **no fallbacks at
|
|
@@ -211,6 +216,11 @@ leaves a clean URL, and a column added to the meta later still shows up for that
|
|
|
211
216
|
meta later won't appear for an old link until Reset (intended).
|
|
212
217
|
|
|
213
218
|
## Change history
|
|
219
|
+
- 2026-09-04 — ⚠ Recorded that the six-page `useColumnState` migration is **not reversible
|
|
220
|
+
file-by-file**: the shared `ColumnVisibilityModalLabels` type gained required fields (`shown`,
|
|
221
|
+
`hidden`, `lockedTip`, `pinnedTip`), so an old call site no longer type-checks and a partial revert
|
|
222
|
+
breaks the build. Roll it back whole or not at all. The dead `columnsButton` return in the
|
|
223
|
+
SalesOrders view model was removed in the same pass. (apeterson)
|
|
214
224
|
- 2026-09-03 — The modal became **its own MODAL Surface** (`columns-modal`, **Core 60**; Messages
|
|
215
225
|
293–298, SurfaceElements 222–232, `Core/2026-09-03c`). The 08-28 approach was unworkable: the
|
|
216
226
|
resolver only collects messages from `*MessageId` columns and **never scans `config`**, so the
|
|
@@ -19,6 +19,7 @@ files:
|
|
|
19
19
|
- toga25-supply/src/layout/SalesOrderRecordModalLayout/
|
|
20
20
|
- toga25-supply/src/layout/SalesOrderItemsTableLayout/
|
|
21
21
|
- toga25-supply/src/layout/ItemFulfillmentModal/
|
|
22
|
+
- toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx
|
|
22
23
|
- toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx
|
|
23
24
|
- toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx
|
|
24
25
|
- toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
|
|
@@ -110,6 +111,29 @@ navigation). `ItemFulfillmentModal` sets it because the fulfillment table is rea
|
|
|
110
111
|
scoped so all other `PrimaryTableServerLayout` consumers keep normal row-click behavior — pass it
|
|
111
112
|
only for genuinely read-only tables.
|
|
112
113
|
|
|
114
|
+
### ⚠ `ItemFulfillmentModal`'s `slug` prop IS a TableView slug — and it is still hardcoded to sales orders
|
|
115
|
+
|
|
116
|
+
The shared "Qty Fulfilled" drill-down modal (`src/layout/ItemFulfillmentModal/`) reads as if `slug`
|
|
117
|
+
were a component variant. It is not: **`slug` is a TableView slug**, and the whole modal is built
|
|
118
|
+
from it — `useTablePageMeta({ slug })` supplies the columns and
|
|
119
|
+
`useTableData({ slug, tableViewMeta })` supplies the rows
|
|
120
|
+
(`useItemFulfillmentModalViewModel.tsx`).
|
|
121
|
+
|
|
122
|
+
**No TableView row for that slug → the modal opens EMPTY.** No error, no failed request. Reusing this
|
|
123
|
+
modal for a new record type is therefore a **migration** first, a front-end change second.
|
|
124
|
+
|
|
125
|
+
**Two spots are still hardcoded to sales orders** (blocking reuse as of 2026-09-04):
|
|
126
|
+
|
|
127
|
+
1. **line ~27** — the label fetch always asks for page meta `"item-fulfillments-for-sales-orders"`,
|
|
128
|
+
whatever slug was passed in.
|
|
129
|
+
2. **lines ~55–60** — the record filter falls back to **`"SalesOrders.uuid"`** for any unrecognised
|
|
130
|
+
slug, so a non-sales-order slug filters on the wrong column and returns nothing.
|
|
131
|
+
|
|
132
|
+
**Measured in `Client_Nychh`: only TableViews 12 (`item-fulfillments-for-sales-orders`) and 13
|
|
133
|
+
(`item-fulfillments-for-sales-order-items`) exist, both on `recordId` 29.** There is no
|
|
134
|
+
transfer-order equivalent in any environment — see
|
|
135
|
+
[transfer-orders-page](./transfer-orders-page.md).
|
|
136
|
+
|
|
113
137
|
## Pattern 5 — `GenericNestedTables` (config-driven multi-level / swap tables)
|
|
114
138
|
|
|
115
139
|
A config-driven renderer: declare an array of `TableLevel` objects instead of hand-wiring each
|
|
@@ -343,6 +367,14 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
|
|
|
343
367
|
EDIT is still JSON**: a field visible only in edit mode means a missing client `SurfaceOverride`,
|
|
344
368
|
not a data bug (found on Compass's "Restrict to Persona" field). Detail in
|
|
345
369
|
[surface-frontend](surface-frontend.md). (tcox)
|
|
370
|
+
- 2026-09-04 — ⚠ Recorded that **`ItemFulfillmentModal`'s `slug` prop is a TableView slug**, not a
|
|
371
|
+
component variant: `useTablePageMeta`/`useTableData` build the entire modal from it, so a missing
|
|
372
|
+
TableView row opens the modal **empty and silently**. Reusing it for a new record type is a
|
|
373
|
+
migration before it is a front-end change. Also flagged the **two spots still hardcoded to sales
|
|
374
|
+
orders** — the label fetch always requests `item-fulfillments-for-sales-orders` (line ~27) and the
|
|
375
|
+
record filter falls back to `SalesOrders.uuid` for any unrecognised slug (lines ~55–60), so a
|
|
376
|
+
transfer-order slug today filters the wrong column and returns nothing. `Client_Nychh` has only
|
|
377
|
+
TableViews 12/13, both on `recordId` 29; no transfer-order equivalent exists anywhere. (apeterson)
|
|
346
378
|
- 2026-09-03 — Added the **deep-link-from-a-modal-into-a-nested-table** pattern (pin `grouping`;
|
|
347
379
|
build the column-filter chip with the `useServerTableUrlState` serializer, keyed by the table-meta
|
|
348
380
|
**column id**; set `<slug>=<uuid>` to pre-expand the row) and the rule that makes it work:
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-09-04
|
|
10
|
-
owners: [jcardinal, apeterson, tcox]
|
|
10
|
+
owners: [jcardinal, apeterson, tcox, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts
|
|
13
13
|
- toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts
|
|
@@ -242,6 +242,28 @@ field even though its API returned the data. **It has been removed entirely.**
|
|
|
242
242
|
`FIELDS[clientSlug]`) are the **legacy JSON fallback layer, NOT surface gating**, and are correctly left
|
|
243
243
|
in place (they serve clients the backend doesn't yet drive via Surface).
|
|
244
244
|
|
|
245
|
+
### ⚠ Corollary — NEVER resolve which table view a client's page uses by grepping `src`
|
|
246
|
+
|
|
247
|
+
Because surface is backend-driven, the repo's grouping JSON is only the **DEFAULT**. A client's
|
|
248
|
+
`SurfaceOverrides` can repoint a level at a completely different `fetchSlug`, and **that slug may
|
|
249
|
+
appear nowhere in `src`** — which reads to a developer as "that view is dead code." It is not.
|
|
250
|
+
|
|
251
|
+
**Rule: query that client's `SurfaceOverrides` in the target environment. A `grep` of the frontend
|
|
252
|
+
repo cannot answer "which view does this client's page read?"**
|
|
253
|
+
|
|
254
|
+
Worked case (Elite Inventory, prod, 2026-09-04):
|
|
255
|
+
`src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json` uses `inventory_items` and
|
|
256
|
+
`units-for-items-for-purchase-orders`, and **`inventory_units` appears nowhere in `src`**. But
|
|
257
|
+
`Client_Elite.SurfaceOverrides` element **120** CONFIG is
|
|
258
|
+
`{"key":"units","levels":[{"fetchSlug":"inventory_units","role":"flat"}]}` and element **118**
|
|
259
|
+
(`unitsByItems`) also points at `inventory_units`, with 116/117 `IS_VISIBLE 0`. So Elite's live
|
|
260
|
+
Units view is `inventory_units` (`TableViews` id 21), **not** id 17 — and a row filter applied to
|
|
261
|
+
view 17 would have had no visible effect. See
|
|
262
|
+
[Elite tableview config drift](../../../clients/elite/features/supply2-tableview-config-drift.md).
|
|
263
|
+
|
|
264
|
+
This pairs with the standing rule that **a committed migration records intent, not deployed
|
|
265
|
+
state** — the frontend repo records the default, not this tenant's state.
|
|
266
|
+
|
|
245
267
|
## SalesOrders record-modal sections — migrated via the registered-renderer seam
|
|
246
268
|
|
|
247
269
|
The SalesOrders detail sections are migrated onto Surface via the registered-renderer approach (not a
|
|
@@ -783,8 +805,45 @@ in an edit to the shared blox stylesheet.
|
|
|
783
805
|
⚠ This is **shared infra** — `SurfaceActionBar` renders every migrated screen's chrome, so a change
|
|
784
806
|
here is a cross-screen change. Re-check Sales Orders, Items and Vendor Items after touching it.
|
|
785
807
|
|
|
808
|
+
## Hidden elements ARRIVE in the bundle — every hand-rolled builder must filter on `isVisible`
|
|
809
|
+
|
|
810
|
+
The resolver ships the **whole** element tree and marks state on it; it does **not** prune. So a
|
|
811
|
+
component that maps `bundle.elements` to its own controls **renders elements the Core seed
|
|
812
|
+
deliberately turned off** unless it checks `el.isVisible` itself. `SurfaceActionBar` does this for
|
|
813
|
+
you — a page that builds its own header buttons does not.
|
|
814
|
+
|
|
815
|
+
Found 2026-09-03 on the **Inventory** page: `useInventoryPageViewModel` built its header buttons
|
|
816
|
+
without the filter. It had no symptom only because no hidden element existed on that bar yet; the
|
|
817
|
+
moment `Core/2026-09-03b` seeded the Create Transfer Order button `isVisible = 0`, it would have
|
|
818
|
+
appeared for **every tenant** — defeating the whole Core-neutral-default / tenant-opt-in model. See
|
|
819
|
+
[transfer-orders-page](./transfer-orders-page.md).
|
|
820
|
+
|
|
821
|
+
> **Check for this wherever a view model reads `elements` directly.** `isVisible = 0` is the platform's
|
|
822
|
+
> off switch; ignoring it turns every neutral default into an accidental release.
|
|
823
|
+
|
|
824
|
+
## `useStatusColors` is keyed by `{ route, keyField }` — vocabularies do NOT all key on slug
|
|
825
|
+
|
|
826
|
+
The original hook mapped a vocabulary slug to an API **route** and assumed the term `value` was
|
|
827
|
+
always the record's **slug**. That held for sales orders and breaks for transfer orders:
|
|
828
|
+
`TransferOrderStages` has no slug column, so its `VocabularyTerms.value` holds the stage **NAME**.
|
|
829
|
+
|
|
830
|
+
`src/surface/useStatusColors.ts` therefore keys each vocabulary to a **`{ route, keyField }` pair**,
|
|
831
|
+
and matches the term value against that field of the fetched rows
|
|
832
|
+
(`?fields=slug,name,colorHex`). Adding a third STATUS vocabulary means answering *"what does its
|
|
833
|
+
`VocabularyTerms.value` actually hold?"* before wiring it — a wrong `keyField` produces a badge with
|
|
834
|
+
the right label and no colour, and logs nothing.
|
|
835
|
+
|
|
836
|
+
Full badge chain (`RecordFields.type='STATUS'` → `Records.statusRecordId` → route → typeValues →
|
|
837
|
+
blox `StatusCell`, which is a plain `typeValues[value]` lookup):
|
|
838
|
+
[transfer-orders-page](./transfer-orders-page.md).
|
|
839
|
+
|
|
786
840
|
## Gotchas
|
|
787
841
|
|
|
842
|
+
- **⚠ Hidden elements are IN the bundle** — a hand-rolled control builder that ignores
|
|
843
|
+
`el.isVisible` ships every element the Core seed turned off (see the section above).
|
|
844
|
+
- **⚠ A STATUS badge renders label-but-no-colour when the vocabulary's `keyField` is wrong** —
|
|
845
|
+
`StatusCell` is a plain map lookup on the cell value, so a name against a slug-keyed vocabulary
|
|
846
|
+
just misses, silently.
|
|
788
847
|
- **A vitest file that reaches `@agilant/toga-blox` must stub the whole package:
|
|
789
848
|
`vi.mock("@agilant/toga-blox", () => ({}))`.** blox's `dist` entry imports `global.css`, which
|
|
790
849
|
node's ESM loader rejects, so **any** import chain that transitively pulls blox in fails to load
|
|
@@ -1055,6 +1114,22 @@ unused).
|
|
|
1055
1114
|
"Unexpected Application Error!" because the app has no top-level `ErrorBoundary`.
|
|
1056
1115
|
`formatPersonaItems` now accepts both value shapes and is registered under both keys. **Deploy the
|
|
1057
1116
|
FE first or together — the SQL alone crashes the modal.** (tcox)
|
|
1117
|
+
- 2026-09-04 — Two shared-infra findings from the transfer-order status badge. (1) ⚠ **Hidden
|
|
1118
|
+
elements arrive in the resolved bundle** — the resolver marks state, it never prunes — so any view
|
|
1119
|
+
model that builds its own controls from `bundle.elements` must filter on `el.isVisible`. The
|
|
1120
|
+
Inventory header-button builder did not; it was symptomless only until `Core/2026-09-03b` seeded a
|
|
1121
|
+
deliberately-hidden button, which would then have shown for every tenant. (2) **`useStatusColors`
|
|
1122
|
+
is now keyed by `{ route, keyField }`, not by route alone**: sales-order terms carry the status
|
|
1123
|
+
**slug** but transfer-order terms carry the stage **NAME** (`TransferOrderStages` has no slug
|
|
1124
|
+
column), and a wrong key field yields a correctly-labelled badge with no colour and no log line.
|
|
1125
|
+
(apeterson)
|
|
1126
|
+
- 2026-09-04 — Added the diagnostic corollary to "surface is 100% backend-driven": **a client's
|
|
1127
|
+
live `fetchSlug` may appear nowhere in `src`, so grepping the frontend repo cannot tell you which
|
|
1128
|
+
table view a client's page reads — query that client's `SurfaceOverrides` in the target
|
|
1129
|
+
environment.** Worked case: Elite's Inventory Units level resolves to `inventory_units`
|
|
1130
|
+
(`TableViews` 21) via prod `SurfaceOverrides` elements 120/118, while the repo DEFAULT JSON names
|
|
1131
|
+
`units-for-items-for-purchase-orders` (17) and `inventory_units` is absent from `src` entirely.
|
|
1132
|
+
No code change. (rgirish)
|
|
1058
1133
|
- 2026-09-03 — Recorded that the 2.5 app **still calls `GET /v2/pages/meta`** from five view
|
|
1059
1134
|
models (SalesOrders — twice per load — VendorItems, Inventory, Bundles, ItemFulfillmentModal)
|
|
1060
1135
|
even though nothing in `src/` reads a field off the result. Found by the new Cypress
|
|
@@ -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-04
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/components/ToolbarButton/ToolbarButton.tsx
|
|
@@ -64,6 +64,24 @@ than by luck with whatever font metrics resolve.
|
|
|
64
64
|
**The "applied" signal never fills the button.** Instead the glyph moves into a small filled rounded
|
|
65
65
|
square — Group by: 18×18, radius 4, `#3F5DCA`, white glyph.
|
|
66
66
|
|
|
67
|
+
### The API — `shape` + `activeAppearance`, generalised from `ColumnsToolbarButton`
|
|
68
|
+
|
|
69
|
+
`ToolbarButton` was **generalised out of the one-off `ColumnsToolbarButton`**, not written fresh, so
|
|
70
|
+
the Columns button is the reference call site.
|
|
71
|
+
|
|
72
|
+
- **`shape`** picks between the two design shapes above: **`pill`** for actions (Refresh, Reset),
|
|
73
|
+
**`chip`** for management controls (Columns, Group by). One component, two shapes — not two
|
|
74
|
+
components.
|
|
75
|
+
- **`activeAppearance`** separates the **transient** active state (pressed / in-flight) from the
|
|
76
|
+
**persistent** one (a filter or grouping is currently applied). They look different by design and
|
|
77
|
+
had previously been conflated, so a button could look permanently "on" after a single click.
|
|
78
|
+
|
|
79
|
+
### Toolbar spacing — `gap-x-2` (8px), not `gap-x-5` (20px)
|
|
80
|
+
|
|
81
|
+
Three labeled-mode pages (Transfer Orders, Sales Orders, Inventory) shipped with `gap-x-5` = **20px**
|
|
82
|
+
between toolbar buttons; the mockup is **8px**. Corrected to `gap-x-2` on all three. Worth stating
|
|
83
|
+
explicitly because 20px looks deliberate rather than wrong until it is next to the design.
|
|
84
|
+
|
|
67
85
|
**Buttons the design does not specify keep `BaseButton`:** PRIMARY create actions, and the Approvals
|
|
68
86
|
filter button (SurfaceElement 22, `config.role = filterButton`), whose navy fill is a clearer "on"
|
|
69
87
|
signal than a chip.
|
|
@@ -84,6 +102,13 @@ signal than a chip.
|
|
|
84
102
|
`Columns (N)` label when the columns differ from default.
|
|
85
103
|
|
|
86
104
|
## Change history
|
|
105
|
+
- 2026-09-04 — Recorded the component's actual API and the spacing fix: `ToolbarButton` was
|
|
106
|
+
**generalised out of `ColumnsToolbarButton`** and takes a **`shape`** prop (`pill` for actions,
|
|
107
|
+
`chip` for management controls) plus an **`activeAppearance`** prop that separates a **transient**
|
|
108
|
+
active state (pressed/in-flight) from a **persistent** one (a filter or grouping is applied) —
|
|
109
|
+
previously conflated, which left a button looking permanently on. Also corrected the toolbar gap
|
|
110
|
+
from `gap-x-5` (20px) to **`gap-x-2` (8px)** on the three labeled-mode pages (Transfer Orders,
|
|
111
|
+
Sales Orders, Inventory) to match the mockup. (apeterson)
|
|
87
112
|
- 2026-09-03 — Created. A page toolbar rendered three buttons at three different heights; the cause is
|
|
88
113
|
that blox `.secondaryBtnAction` hard-codes `height: 26px` with **no theme token**, while
|
|
89
114
|
`.primaryBtn` sets no height at all. Rethemeing would hit every button in the app, so the toolbar
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 25 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 34 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). | worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/PurchaseOrder.php, _underscore/Model/Elite/ItemReceipt.php, dbchanges2/Client_Elite/_modules.txt, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
|
|
5
|
+
| [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). | worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/PurchaseOrder.php, _underscore/Model/Elite/ItemReceipt.php, dbchanges2/Client_Elite/_modules.txt, dbchanges2/Client_Elite/2026-09-04a - EliteItemAssetTypeApiWritePermission.sql, library/app/api/toga2.php, library/app/api/netsuite/rest.php, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
|
|
6
6
|
| [Elite SalesOrder → NetSuite Push (postPost/postPut interceptors → worker2)](features/salesorder-netsuite-push.md) | 2.0 | Elite orders created in Toga are pushed into NetSuite **event-driven**, not on a cron. | _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
|
|
7
7
|
| [Elite — Sales Order stage change posts a reply on the TOGa Desk (1.0) ticket](features/salesorder-status-togadesk-reply.md) | 2.0 | When an Elite sales order's **stage** changes, a reply is posted on the originating **TOGa Desk (1.0)** ticket so the requester sees progress where they raised | worker2/Worker/Sync/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrder.php |
|
|
8
8
|
| [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
9
|
-
| [Elite — stale TableView config (11 dead Core.RecordFields across 9 views; 2 now fixed)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Elite/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
9
|
+
| [Elite — stale TableView config (11 dead Core.RecordFields across 9 views; 2 now fixed)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Elite/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Client_Elite/2026-08-13a - InventoryGroupingsSurfaceOverrides.sql, dbchanges2/Client_Elite/2026-09-04b - EliteInventoryHideServiceItems.sql, dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
10
10
|
| [Elite — raising a Service Request from a TOGa Desk ticket (App_Api_ServiceRequest)](features/togadesk-service-request-intake.md) | 1.0 | An Elite agent raises a **Service Request** from a TOGa Desk (1.0) ticket via a modal. | library/app/api/servicerequest.php, library/app/model/togadesk/ticket.php, togadesk/desk/includes/classes/class.ticket.php, togadesk/desk/template/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php, dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql, test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php |
|
|
11
11
|
| [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: elite
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["snaredla", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/sync_togasupply_elite.php
|
|
@@ -16,10 +16,14 @@ files:
|
|
|
16
16
|
- _underscore/Model/Elite/PurchaseOrder.php
|
|
17
17
|
- _underscore/Model/Elite/ItemReceipt.php
|
|
18
18
|
- dbchanges2/Client_Elite/_modules.txt
|
|
19
|
+
- dbchanges2/Client_Elite/2026-09-04a - EliteItemAssetTypeApiWritePermission.sql
|
|
20
|
+
- library/app/api/toga2.php
|
|
21
|
+
- library/app/api/netsuite/rest.php
|
|
19
22
|
- test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php
|
|
20
23
|
- test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php
|
|
21
24
|
related:
|
|
22
25
|
- ../profile.md
|
|
26
|
+
- ../../../1.0/apps/library/features/netsuite-item-assettype-sync.md
|
|
23
27
|
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
24
28
|
- ../../../1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md
|
|
25
29
|
- ./salesorder-netsuite-push.md
|
|
@@ -81,11 +85,16 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
|
|
|
81
85
|
other 17 wrappers inherit — Elite is backfilling from 2024 and 5-day windows on a 5-minute cron were
|
|
82
86
|
too slow. This is only possible because the shared engine now `define()`s the cap instead of
|
|
83
87
|
`const`-ing it. **Reduce it to the default once the backfill is caught up.**
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
- **⚠ EVERY integration flag is now `true` (2026-09-04) — including invoices, and that carries a
|
|
89
|
+
live risk.** `_SALES_ORDERS`, `_INVOICES`, `_ITEM_RECEIPTS`, `_ITEM_FULFILLMENTS`,
|
|
90
|
+
`_INVENTORY_ADJUSTMENTS` and `_TRANSFER_ORDERS` were all flipped `false → true`
|
|
91
|
+
(`_PURCHASE_ORDERS` was already on); stale comments were removed, including one that still said
|
|
92
|
+
transfer orders were off. **Invoices was deliberately disabled** because its section scans the
|
|
93
|
+
whole NetSuite transaction table per window, kept running out of time, and **starved the
|
|
94
|
+
item-receipt and item-fulfillment sections that follow it** — the ones `_qtyOnHand` derives from.
|
|
95
|
+
It is back on **at the same time as a planned PO cursor backfill**, so the starvation risk is
|
|
96
|
+
live. **If item receipts stop advancing, invoices is the first suspect.** (The invoice section is
|
|
97
|
+
also capped at `now − 86400s` by design, so it always trails.)
|
|
89
98
|
- **`MIN_DATETIME_TO_CHECK_FOR_NETSUITE_DATA = '2018-01-01'`**, with the real start controlled by the
|
|
90
99
|
seeded `Parameters` cursors.
|
|
91
100
|
- **`TRANSFER_ORDER_DETECTION_MODE` is pre-set but NOT in use (2026-09-03).** Elite's wrapper declares
|
|
@@ -96,6 +105,54 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
|
|
|
96
105
|
transfer orders as $0 held sales orders; do not assume the pre-set mode is correct for them. Mode
|
|
97
106
|
mechanics: [per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
98
107
|
|
|
108
|
+
### Elite is the ONLY client opted in to assetType stamping
|
|
109
|
+
|
|
110
|
+
Elite's wrapper defines `NETSUITE_ITEM_TYPE_TO_ASSET_TYPE_NAME`, which turns on
|
|
111
|
+
[assetType stamping from NetSuite `itemtype`](../../../1.0/apps/library/features/netsuite-item-assettype-sync.md).
|
|
112
|
+
No other launcher defines it, so the other ~19 clients send no `assetType` key at all.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
'InvtPart' => 'Laptop', 'Kit' => 'Laptop', 'Group' => 'Laptop',
|
|
116
|
+
'Service' => 'Services', 'NonInvtPart' => 'Services', 'OthCharge' => 'Services',
|
|
117
|
+
'Discount' => 'Services', 'Expense' => 'Services', 'Description' => 'Services'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`Client_Elite.AssetTypes` has exactly **2 rows: 1 = `Laptop`, 2 = `Services`.** The non-inventory
|
|
121
|
+
types are in the map on purpose — `itemtype = 'Service'` alone marks only **11 of ~28** real Elite
|
|
122
|
+
services, because `NonInvtPart` is mixed (`SVC-FS-DEPLOY`, `SVC-FS-SHIPPING-*`,
|
|
123
|
+
`SVC-CI-RETAINER-RS`, `SVC-TS-Removal`, `Project - Cabling`). Elite's 113 items break down as
|
|
124
|
+
65 InvtPart / 11 Service / 17 NonInvtPart.
|
|
125
|
+
|
|
126
|
+
**Two deploy prerequisites, in this order:**
|
|
127
|
+
|
|
128
|
+
1. **`dbchanges2/Client_Elite/2026-09-04a - EliteItemAssetTypeApiWritePermission.sql` must run
|
|
129
|
+
BEFORE the library/worker deploy.** It grants `AclFieldPermissions` write on recordFieldId
|
|
130
|
+
**269** (`Items.assetTypeId`) to **roleId 3 (API)**, which previously held roleId 1 only. api2
|
|
131
|
+
fails the **entire** item write on a non-writable field, so deploying the code first breaks item
|
|
132
|
+
creation for Elite. **Applied in production and re-verified** (269 now carries roleId 1 and 3).
|
|
133
|
+
2. **A cursor rollback is required to stamp existing items.** `getCreateItem()` only runs when a
|
|
134
|
+
transaction line references an item, so Elite's **110 already-NULL items stay NULL** until the
|
|
135
|
+
sync re-walks transactions that reference them. Cursors are in `Client_Elite.Parameters`
|
|
136
|
+
(`NETSUITE_LAST_SYNC_CURSOR_*`, value `"<lastmodifieddate>|<netsuiteInternalId>"`).
|
|
137
|
+
|
|
138
|
+
Prod cursor state at the time of this work, `NETSUITE_EXECUTION_MODE_*` all `IDLE`:
|
|
139
|
+
|
|
140
|
+
| Section | Cursor |
|
|
141
|
+
|---|---|
|
|
142
|
+
| PURCHASE_ORDERS (the only section then enabled) | `2026-09-04 12:56:00\|0` |
|
|
143
|
+
| SALES_ORDERS | `2026-09-01 15:45:56\|0` |
|
|
144
|
+
| ITEM_RECEIPTS | `2026-09-01 16:04:00\|0` |
|
|
145
|
+
| ITEM_FULFILLMENTS | `2026-09-01 16:04:11\|0` |
|
|
146
|
+
| INVOICES | `2026-08-20 00:00:00\|0` |
|
|
147
|
+
| INVENTORY_ADJUSTMENTS | `2026-09-01 16:04:21\|0` |
|
|
148
|
+
|
|
149
|
+
**3 Elite items are typed `InvtPart` in NetSuite but are really services** —
|
|
150
|
+
`SVC-TS-ELITE-HDONBOARDING`, `CONFIG/INSTALL`, `CONF-RM-INSTALL-SUPP`. **Decided: a NetSuite
|
|
151
|
+
data-hygiene ask for the client, not a code special case.** Stamping is fill-NULL-only, so a manual
|
|
152
|
+
TOGa correction survives later syncs. The Inventory filter also keeps a `SVC-` part-number prefix
|
|
153
|
+
rule that still catches these three — see
|
|
154
|
+
[supply2-tableview-config-drift](./supply2-tableview-config-drift.md).
|
|
155
|
+
|
|
99
156
|
## Gotchas / known issues
|
|
100
157
|
|
|
101
158
|
- **⚠ Two files named `sync_togasupply_elite.php`, both scheduled.**
|
|
@@ -132,6 +189,22 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
|
|
|
132
189
|
|
|
133
190
|
## Change history
|
|
134
191
|
|
|
192
|
+
- 2026-09-04 — **Elite became the first (and only) client opted in to assetType stamping**, and
|
|
193
|
+
**all integration flags were switched on.** Added `NETSUITE_ITEM_TYPE_TO_ASSET_TYPE_NAME` to the
|
|
194
|
+
wrapper (InvtPart/Kit/Group → `Laptop`; Service/NonInvtPart/OthCharge/Discount/Expense/Description
|
|
195
|
+
→ `Services`) — the non-inventory types are needed because `NonInvtPart` is mixed and
|
|
196
|
+
`itemtype = 'Service'` alone catches only 11 of ~28 real services. Shipped
|
|
197
|
+
`2026-09-04a - EliteItemAssetTypeApiWritePermission.sql` (recordField **269** write grant to
|
|
198
|
+
**roleId 3**; applied and re-verified in prod) which **must run before the library/worker
|
|
199
|
+
deploy**, since api2 fails the whole item write on a non-writable field. Elite's 110 NULL items
|
|
200
|
+
need a **PO cursor rollback** to be stamped — `getCreateItem()` only fires on a referenced
|
|
201
|
+
transaction line; prod cursor state recorded above. Flipped `_SALES_ORDERS`, `_INVOICES`,
|
|
202
|
+
`_ITEM_RECEIPTS`, `_ITEM_FULFILLMENTS`, `_INVENTORY_ADJUSTMENTS`, `_TRANSFER_ORDERS` to `true` and
|
|
203
|
+
removed stale comments — **note the invoices risk**: it was disabled for starving the
|
|
204
|
+
receipt/fulfillment sections, and it is back on alongside the cursor backfill. Engine mechanics
|
|
205
|
+
and the two platform blockers:
|
|
206
|
+
[assetType sync](../../../1.0/apps/library/features/netsuite-item-assettype-sync.md). (rgirish)
|
|
207
|
+
|
|
135
208
|
- 2026-09-03 — Elite's wrapper now declares `TRANSFER_ORDER_DETECTION_MODE =
|
|
136
209
|
App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` as part of the shared
|
|
137
210
|
transfer-order detection-mode refactor (the old `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`
|
|
@@ -6,17 +6,22 @@ project: Database Changes
|
|
|
6
6
|
client: elite
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [tcox, bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [tcox, bala, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Elite/
|
|
13
13
|
- dbchanges2/Client_Elite/2026-08-07 - ServiceRequests TableView.sql
|
|
14
14
|
- dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql
|
|
15
|
+
- dbchanges2/Client_Elite/2026-08-13a - InventoryGroupingsSurfaceOverrides.sql
|
|
16
|
+
- dbchanges2/Client_Elite/2026-09-04b - EliteInventoryHideServiceItems.sql
|
|
15
17
|
- dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql
|
|
16
18
|
- dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
|
|
17
19
|
- dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql
|
|
18
20
|
related:
|
|
19
21
|
- ./supply2-scope.md
|
|
22
|
+
- ./netsuite-togasupply-sync.md
|
|
23
|
+
- ../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md
|
|
24
|
+
- ../../../2.0/apps/toga25-supply/features/surface-frontend.md
|
|
20
25
|
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
21
26
|
- ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
|
|
22
27
|
- ../../../2.0/apps/_underscore/features/page-meta-context-field-settings.md
|
|
@@ -241,6 +246,61 @@ Do not revive this without first re-testing finding 2 above.
|
|
|
241
246
|
|
|
242
247
|
</details>
|
|
243
248
|
|
|
249
|
+
## ⚠⚠ Which table view Elite's Inventory page uses comes from `SurfaceOverrides` — NOT the repo JSON
|
|
250
|
+
|
|
251
|
+
**Durable rule: never conclude which table view a client's page uses by grepping the frontend repo.
|
|
252
|
+
Query that client's `SurfaceOverrides` in the target environment.** This corrected a wrong
|
|
253
|
+
conclusion mid-session and is the most reusable finding here.
|
|
254
|
+
|
|
255
|
+
Reading `toga25-supply/src` is actively misleading for Elite:
|
|
256
|
+
|
|
257
|
+
- `src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json` uses `inventory_items` and
|
|
258
|
+
`units-for-items-for-purchase-orders`.
|
|
259
|
+
- **`inventory_units` appears nowhere in `src`** — which reads as *"`inventory_units` is a dead
|
|
260
|
+
view."* **It is not.**
|
|
261
|
+
|
|
262
|
+
Verified on production `Client_Elite.SurfaceOverrides`:
|
|
263
|
+
|
|
264
|
+
| Element | CONFIG / setting |
|
|
265
|
+
|---|---|
|
|
266
|
+
| **120** | `{"key":"units","levels":[{"fetchSlug":"inventory_units","role":"flat"}]}` |
|
|
267
|
+
| **118** | `unitsByItems` — also points at `inventory_units` |
|
|
268
|
+
| 116 / 117 | `IS_VISIBLE 0` (hidden) |
|
|
269
|
+
|
|
270
|
+
Surface config **overrides** the repo JSON (surface resolution is 100% backend-driven — see
|
|
271
|
+
[surface-frontend](../../../2.0/apps/toga25-supply/features/surface-frontend.md)). So **for Elite
|
|
272
|
+
the live Units view is `inventory_units` (`TableViews` id 21), not
|
|
273
|
+
`units-for-items-for-purchase-orders` (id 17).** Elite's overrides were seeded by
|
|
274
|
+
`2026-08-13a - InventoryGroupingsSurfaceOverrides.sql`.
|
|
275
|
+
|
|
276
|
+
This pairs with the existing team rule that **a committed migration records intent, not deployed
|
|
277
|
+
state** — the frontend repo records the *default*, not this client's state.
|
|
278
|
+
|
|
279
|
+
## Hiding service lines from Inventory — `2026-09-04b`
|
|
280
|
+
|
|
281
|
+
`2026-09-04b - EliteInventoryHideServiceItems.sql` sets `TableViews.apiWhereClause`:
|
|
282
|
+
|
|
283
|
+
| View | Clause |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` |
|
|
286
|
+
| `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` |
|
|
287
|
+
|
|
288
|
+
`assetTypeId 2` = Elite's `Services` row. Notes for anyone porting this:
|
|
289
|
+
|
|
290
|
+
- **The filter belongs in the database table view, not the frontend** — an explicit scope decision.
|
|
291
|
+
- **`IFNULL(...,0):ne:2` is mandatory, not cosmetic.** A bare `:ne:2` drops every `NULL` row and
|
|
292
|
+
would hide **all unstamped items** — and 110 of Elite's 113 items are unstamped until the cursor
|
|
293
|
+
backfill runs. The shape is copied from Elite's existing `item-fulfillments-for-sales-orders`
|
|
294
|
+
view. Grammar and the NULL-drop rule:
|
|
295
|
+
[apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md).
|
|
296
|
+
- **The `SVC-` prefix rule is kept on `inventory_items`** because it still catches the three items
|
|
297
|
+
NetSuite mis-types as `InvtPart` (`SVC-TS-ELITE-HDONBOARDING`, `CONFIG/INSTALL`,
|
|
298
|
+
`CONF-RM-INSTALL-SUPP`) — see
|
|
299
|
+
[netsuite-togasupply-sync](./netsuite-togasupply-sync.md).
|
|
300
|
+
- **Both `UPDATE`s are guarded on the current value**, so a re-run is a no-op.
|
|
301
|
+
- `inventory_units` was targeted **because of the `SurfaceOverrides` finding above** — filtering
|
|
302
|
+
view 17 would have had no visible effect for Elite.
|
|
303
|
+
|
|
244
304
|
## Gotchas / known issues
|
|
245
305
|
|
|
246
306
|
- **A `Client_*` DB that predates a platform migration fails silently until a user opens the
|
|
@@ -252,6 +312,18 @@ Do not revive this without first re-testing finding 2 above.
|
|
|
252
312
|
[error-reporting-issue-event](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
|
|
253
313
|
|
|
254
314
|
## Change history
|
|
315
|
+
- 2026-09-04 — **Corrected which table view Elite's Inventory page actually reads, then filtered
|
|
316
|
+
it.** `toga25-supply/src` shows the DEFAULT groupings using `inventory_items` +
|
|
317
|
+
`units-for-items-for-purchase-orders`, and `inventory_units` appears nowhere in `src` — reading as
|
|
318
|
+
a dead view. Production `Client_Elite.SurfaceOverrides` proves otherwise: element **120** CONFIG
|
|
319
|
+
points `units` at `inventory_units` and element **118** (`unitsByItems`) does too, with 116/117
|
|
320
|
+
hidden. **Durable rule recorded: never resolve a client's table view by grepping the frontend
|
|
321
|
+
repo — query that client's `SurfaceOverrides` in the target environment.** Shipped
|
|
322
|
+
`2026-09-04b - EliteInventoryHideServiceItems.sql` to hide service lines via `apiWhereClause` on
|
|
323
|
+
`inventory_units` and `inventory_items`, using the mandatory `IFNULL(Items.assetTypeId,0):ne:2`
|
|
324
|
+
shape (a bare `:ne:2` would hide all 110 unstamped items) and keeping the existing `SVC-` prefix
|
|
325
|
+
rule for the three NetSuite-mis-typed items. Both UPDATEs are guarded so a re-run is a no-op.
|
|
326
|
+
(rgirish)
|
|
255
327
|
- 2026-08-18 - **The Elite table views shipped as migrations, and two of the planned approaches
|
|
256
328
|
turned out to be wrong.** Built the `service-requests` view (record 35, 5 OUTER joins, 9 columns)
|
|
257
329
|
and the all-clients `inventory_units` view (record 31, Items as spine, 15 joins, 13 columns);
|
|
@@ -16,8 +16,8 @@ project: Worker
|
|
|
16
16
|
client: elite
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-
|
|
20
|
-
owners: [snaredla, apeterson, tcox, bala]
|
|
19
|
+
updated: 2026-09-04
|
|
20
|
+
owners: [snaredla, apeterson, tcox, bala, rgirish]
|
|
21
21
|
files:
|
|
22
22
|
- worker2/Worker/Elite.php
|
|
23
23
|
- worker2/Worker/Sync/ServiceRequest.php
|
|
@@ -55,6 +55,15 @@ remaining stale views are still an open production go-live risk** - see
|
|
|
55
55
|
dbchanges2 **PR #454** (TRUE-80499, snaredla24, 2026-08-05) added the **netsuite** module to
|
|
56
56
|
`Client_Elite/_modules.txt` — the supply-chain schema layer is arriving.
|
|
57
57
|
|
|
58
|
+
**Elite is the only client opted in to NetSuite `itemtype` → `assetType` item stamping
|
|
59
|
+
(2026-09-04),** which is how service lines are hidden from the Inventory page. It needed a
|
|
60
|
+
`Client_Elite` API-role write grant on `Items.assetTypeId` **before** the code deploy, and its
|
|
61
|
+
existing 110 NULL items need a PO cursor rollback to be stamped — see
|
|
62
|
+
[netsuite-togasupply-sync](features/netsuite-togasupply-sync.md) and the
|
|
63
|
+
[shared assetType feature](../../1.0/apps/library/features/netsuite-item-assettype-sync.md). **All
|
|
64
|
+
of Elite's integration flags are now on, invoices included** — invoices had been disabled for
|
|
65
|
+
starving the receipt/fulfillment sections, so that risk is live again.
|
|
66
|
+
|
|
58
67
|
**Elite is live on the inbound NetSuite → TOGa Supply sync as of 2026-08-12 (TRUE-80499).** A
|
|
59
68
|
`*/5` worker cron backfilled April 2024 → Aug 2026 in ~90 minutes (257 sales orders, 234 purchase
|
|
60
69
|
orders, 201 item fulfillments, 153 items, 229 units) — this is what adds **`worker`** (1.0) to
|
package/package.json
CHANGED