toga-ai 1.0.782 → 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.
@@ -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-08-12
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).
@@ -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
@@ -1101,6 +1123,13 @@ unused).
1101
1123
  **slug** but transfer-order terms carry the stage **NAME** (`TransferOrderStages` has no slug
1102
1124
  column), and a wrong key field yields a correctly-labelled badge with no colour and no log line.
1103
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)
1104
1133
  - 2026-09-03 — Recorded that the 2.5 app **still calls `GET /v2/pages/meta`** from five view
1105
1134
  models (SalesOrders — twice per load — VendorItems, Inventory, Bundles, ItemFulfillmentModal)
1106
1135
  even though nothing in `src/` reads a field off the result. Found by the new Cypress
@@ -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)_ — 24 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
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-03
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
- - **`IS_ENABLED_INTEGRATION_INVOICES = false` — TEMPORARY.** The invoice section scans the whole
85
- NetSuite transaction table per window, kept running out of time, and **starved the item-receipt and
86
- item-fulfillment sections that follow it** which are what `_qtyOnHand` derives from. Re-enable
87
- once inventory has caught up. (Note the invoice section is also capped at `now 86400s` by design,
88
- so it always trails.)
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-08-18
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-08-26
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.782",
3
+ "version": "1.0.783",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",