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.
- 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/toga25-supply/features/surface-frontend.md +30 -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).
|
|
@@ -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
|
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