toga-ai 1.0.833 → 1.0.835
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/features/netsuite-item-class-sync.md +15 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/workflows/rotating-public-tls-certificates.md +91 -5
- package/knowledge/2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md +46 -2
- package/knowledge/clients/elite/features/supply2-tableview-config-drift.md +101 -13
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: [rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -23,6 +23,7 @@ related:
|
|
|
23
23
|
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
24
24
|
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
25
25
|
- ../../../clients/elite/features/netsuite-togasupply-sync.md
|
|
26
|
+
- ../../../clients/elite/features/supply2-tableview-config-drift.md
|
|
26
27
|
---
|
|
27
28
|
|
|
28
29
|
## Summary
|
|
@@ -138,7 +139,20 @@ without `AclRecordPermissions` the API role cannot `POST /item-classes` at all.
|
|
|
138
139
|
sync re-walks transactions that reference them. That needs a
|
|
139
140
|
[cursor rewind](../../worker/features/netsuite-togasupply-per-client-sync.md).
|
|
140
141
|
|
|
142
|
+
- **The tree depth is not capped — consumers that walk it with fixed joins can break silently.**
|
|
143
|
+
`Client_Elite` is exactly **5 levels** deep today (4 roots: 1 Lifecycle Services, 4 Technology
|
|
144
|
+
Sales, 22 Advisory and Modernization, 40 True Partner Services; verified 2026-09-15 that every
|
|
145
|
+
5-level chain ends at a true root). Nothing stops NetSuite adding a 6th level. Anything that
|
|
146
|
+
resolves an item to its root class with a fixed number of `LEFT JOIN`s — such as Elite's
|
|
147
|
+
[Inventory service-item filter](../../../clients/elite/features/supply2-tableview-config-drift.md) —
|
|
148
|
+
will then silently mis-classify the deeper items with no error. Re-check depth when the NetSuite
|
|
149
|
+
tree changes.
|
|
150
|
+
|
|
141
151
|
## Change history
|
|
152
|
+
- 2026-09-17 — Added the **unbounded tree depth** caveat: `Client_Elite` is exactly 5 levels with 4
|
|
153
|
+
roots (1 / 4 / 22 / 40) and every chain verified to end at a true root, but nothing caps depth, so
|
|
154
|
+
any consumer walking to the root with a fixed join count breaks silently if NetSuite adds a level.
|
|
155
|
+
No code change. (rgirish)
|
|
142
156
|
- 2026-09-15 — Documented the feature from an audit of the deployed code (library `37b55595`, worker
|
|
143
157
|
`6637ae75`). **Found and fixed a field-name mismatch that made the feature a complete no-op since
|
|
144
158
|
deploy:** the PHP sent `c_netsuiteInternalItemClassId` in 5 places while the live column and
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
| [USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)](features/usps-dpv-deliverability.md) | **USPS returning HTTP 200 with a populated address is NOT evidence that the address is deliverable.** The authoritative signal is USPS's **DPV (Delivery Point V |
|
|
62
62
|
| [Refreshing a Local Dev Database from Beta (dev-sandbox)](workflows/local-db-refresh-from-beta.md) | How to reset a local 2.0 dev database from the **beta / dev-sandbox** environment: dump each schema (`Core`, `Client_<Id>`, `Logs_<Id>`, …) from the beta host, |
|
|
63
63
|
| [Deleting a shared branch does not remove bad commits — a stale local clone merges them back](workflows/recreated-shared-branch-stale-local-remerge.md) | **Deleting and recreating a shared environment branch removes only the *ref*.** Every teammate who still has that branch checked out locally keeps the full pre- |
|
|
64
|
-
| [Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk)](workflows/rotating-public-tls-certificates.md) | How to replace the public TLS certificates that terminate HTTPS for TOGa front-end domains (togahub, togacommerce, togadesk, togaretail, togasupply, togaview, t |
|
|
64
|
+
| [Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk, API Gateway)](workflows/rotating-public-tls-certificates.md) | How to replace the public TLS certificates that terminate HTTPS for TOGa front-end domains (togahub, togacommerce, togadesk, togaretail, togasupply, togaview, t |
|
|
65
65
|
| [Running a 2.0 App Locally (browser, end-to-end via api2)](workflows/running-a-2.0-app-locally.md) | The full dependency chain required to run a 2.0 client app **through the browser**, end-to-end, against a **local `api2`** (e.g. |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk)
|
|
2
|
+
title: Rotating Public TLS Certificates (ACM, CloudFront, ALB, Elastic Beanstalk, API Gateway)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: ["rgirish"]
|
|
11
11
|
files: []
|
|
12
12
|
related:
|
|
@@ -27,13 +27,15 @@ This runbook was written from the 2026-09-16 rotation in AWS account **654654170
|
|
|
27
27
|
one IMPORTED multi-domain cert (14 SANs, copied into us-east-1, us-west-2 and eu-west-1) served
|
|
28
28
|
4 CloudFront distributions and 12 ALB listeners and expired the same day.
|
|
29
29
|
|
|
30
|
-
**The
|
|
30
|
+
**The four things that bite you:**
|
|
31
31
|
1. **Elastic Beanstalk keeps its own saved cert setting**, separate from the ALB listener. Fixing
|
|
32
32
|
the listener is not enough — the next deploy puts the old cert straight back.
|
|
33
33
|
2. **CloudFront holds exactly ONE cert per distribution.** There is no staging. The swap *is* the
|
|
34
34
|
cutover, and it takes 5-15 minutes.
|
|
35
35
|
3. **An ACM wildcard matches exactly one label.** `*.togasupply.com` covers
|
|
36
36
|
`elite.togasupply.com` but **not** `compass.beta.togasupply.com`.
|
|
37
|
+
4. **API Gateway custom domains hold their own cert** and are invisible to a CloudFront/ALB/EB
|
|
38
|
+
sweep. This is what the 2026-09-16 rotation missed — see step 6b.
|
|
37
39
|
|
|
38
40
|
**Root cause to avoid repeating:** an **IMPORTED** ACM cert never auto-renews. Always replace with
|
|
39
41
|
**Amazon-issued, DNS-validated** ACM certs, which renew themselves.
|
|
@@ -123,6 +125,60 @@ one), and every environment stayed Green/Ok through the update — no traffic di
|
|
|
123
125
|
**Gotcha:** an EB environment whose CloudFormation stack is in `DELETE_FAILED` cannot be updated at
|
|
124
126
|
all — `update-environment` is refused. That needs separate stack cleanup.
|
|
125
127
|
|
|
128
|
+
## Step 6b — Audit API Gateway custom domains (the 2026-09-16 miss)
|
|
129
|
+
|
|
130
|
+
**API Gateway is a FOURTH resource type.** The 2026-09-16 rotation swept CloudFront, ALB listeners
|
|
131
|
+
and EB saved configs, and reported "0 endpoints at risk". The next morning `webhook.togahub.com`
|
|
132
|
+
was hard-down — it is an API Gateway custom domain, so none of those three sweeps could ever have
|
|
133
|
+
found it. Proof of the failure, not just a stale cert:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
$ curl -sS -o /dev/null -w "http=%{http_code} ssl=%{ssl_verify_result}\n" https://webhook.togahub.com/
|
|
137
|
+
curl: (60) SSL certificate problem: certificate has expired
|
|
138
|
+
http=000 ssl=10
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Find them from DNS — an API Gateway domain CNAMEs to `*.execute-api.<region>.amazonaws.com`:**
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
aws route53 list-resource-record-sets --hosted-zone-id <zid> --max-items 400 \
|
|
145
|
+
--query "ResourceRecordSets[?Type=='CNAME'].[Name,ResourceRecords[0].Value]" --output text \
|
|
146
|
+
| grep -i execute-api
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Across all seven TOGa zones this found exactly one: `webhook.togahub.com` →
|
|
150
|
+
`d-yiy4od2ade.execute-api.us-east-1.amazonaws.com`.
|
|
151
|
+
|
|
152
|
+
**An unknown account ID in `InUseBy` can be AWS itself.** The expired cert's `InUseBy` listed 3
|
|
153
|
+
ALBs in account **250044486744**, which no one has a profile for. That is AWS-managed edge
|
|
154
|
+
infrastructure — an edge-optimized API Gateway domain terminates TLS on AWS's own load balancers.
|
|
155
|
+
Treat it as a pointer to API Gateway, **not** as a rogue account to go hunt credentials for.
|
|
156
|
+
|
|
157
|
+
**Permissions block this from the CLI.** The `GoAgilant-Developers` SSO role has **no**
|
|
158
|
+
`apigateway:GET` in any of the three accounts (654654170868, 502614707982, 975050298201) —
|
|
159
|
+
`get-domain-names`, `get-domain-name` and the `apigatewayv2` equivalents all return
|
|
160
|
+
`AccessDeniedException`. The domain cannot be listed, read or fixed with the standard developer
|
|
161
|
+
role. Use the Console, or get `apigateway:GET` + `apigateway:PATCH` added first.
|
|
162
|
+
|
|
163
|
+
**Check the endpoint type FIRST — it decides which field you patch.** `get-domain-name` returns
|
|
164
|
+
`endpointConfiguration.types`. An **EDGE** domain reads its cert from `us-east-1` and uses
|
|
165
|
+
`/certificateArn`; a **REGIONAL** domain reads from its own region and uses
|
|
166
|
+
`/regionalCertificateArn`. Patching the wrong field silently does nothing useful.
|
|
167
|
+
`webhook.togahub.com` turned out to be **REGIONAL** (us-east-1), despite `InUseBy` showing
|
|
168
|
+
AWS-managed edge ALBs — so do not infer the type from `InUseBy`.
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
aws apigateway get-domain-name --domain-name webhook.togahub.com \
|
|
172
|
+
--query 'endpointConfiguration.types'
|
|
173
|
+
|
|
174
|
+
aws apigateway update-domain-name --domain-name webhook.togahub.com \
|
|
175
|
+
--patch-operations op=replace,path=/regionalCertificateArn,value=<new-arn>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The domain goes `UPDATING` and takes a few minutes to return to `AVAILABLE` (~4 min observed).
|
|
179
|
+
Poll `domainNameStatus` before verifying — checking too early shows the old cert and looks like a
|
|
180
|
+
failed patch. No downtime beyond the already-broken TLS; base path mappings are untouched.
|
|
181
|
+
|
|
126
182
|
## Step 7 — Check alias coverage before any CloudFront swap
|
|
127
183
|
|
|
128
184
|
CloudFront **rejects** an update if any alias on the distribution is not covered by the new cert.
|
|
@@ -155,11 +211,22 @@ echo | openssl s_client -servername HOST -connect HOST:443 2>/dev/null \
|
|
|
155
211
|
| openssl x509 -noout -issuer -enddate
|
|
156
212
|
```
|
|
157
213
|
|
|
214
|
+
`openssl` shows you the cert, but it does **not** prove a client can connect. Add a real verify —
|
|
215
|
+
and treat a known-bad host as the control that proves your check actually fails:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
curl -sS -o /dev/null -w "http=%{http_code} ssl=%{ssl_verify_result}\n" https://HOST/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A live expired cert returns `http=000` with `SSL certificate problem: certificate has expired`.
|
|
222
|
+
|
|
158
223
|
Also run it **without** `-servername` to check the no-SNI / default-cert path — that path can still
|
|
159
224
|
be serving the old cert after everything else looks fine.
|
|
160
225
|
|
|
161
|
-
Finish by re-running the step 1 audit: every CloudFront distribution, every ALB listener,
|
|
162
|
-
EB saved config should name a cert with a future
|
|
226
|
+
Finish by re-running the step 1 audit: every CloudFront distribution, every ALB listener, every
|
|
227
|
+
EB saved config **and every API Gateway custom domain** should name a cert with a future
|
|
228
|
+
`NotAfter`. A sweep that omits any one of those four resource types is not a clean result — it is
|
|
229
|
+
an untested one.
|
|
163
230
|
|
|
164
231
|
## Gotchas
|
|
165
232
|
|
|
@@ -170,9 +237,28 @@ EB saved config should name a cert with a future `NotAfter`.
|
|
|
170
237
|
- **CloudFront takes one cert; ALBs take many.** Two completely different risk profiles in the same
|
|
171
238
|
rotation — plan them separately.
|
|
172
239
|
- **Wildcards match one label only.** Verify alias coverage with code, not by eye.
|
|
240
|
+
- **API Gateway custom domains are a fourth resource type.** A CloudFront + ALB + EB sweep misses
|
|
241
|
+
them completely. Find them by `execute-api` CNAMEs in Route 53. See step 6b.
|
|
242
|
+
- **An unknown account ID in `InUseBy` may be AWS itself.** `250044486744` is AWS-managed edge
|
|
243
|
+
infrastructure for API Gateway, not a rogue account. See step 6b.
|
|
244
|
+
- **The `GoAgilant-Developers` role needs `apigateway:GET` + `apigateway:PATCH`** on
|
|
245
|
+
`arn:aws:apigateway:*::/domainnames*`. Added to the permission set 2026-09-17; it was missing
|
|
246
|
+
during the rotation. Granted via IAM Identity Center, then **provisioned** to the account — the
|
|
247
|
+
provision step is what actually applies it. See step 6b.
|
|
248
|
+
- **Patch the field that matches the endpoint type.** REGIONAL uses `/regionalCertificateArn`,
|
|
249
|
+
EDGE uses `/certificateArn`. See step 6b.
|
|
250
|
+
- **`openssl` is not proof of health.** It prints the cert even when clients cannot connect. Use
|
|
251
|
+
`curl` and check for `http=000`. See step 9.
|
|
173
252
|
- **Expired certs hide.** Sweep the whole account; do not trust `InUseBy`.
|
|
174
253
|
|
|
175
254
|
## Change history
|
|
255
|
+
- 2026-09-17 — Added step 6b (API Gateway custom domains) after `webhook.togahub.com` was found
|
|
256
|
+
serving an expired cert and failing TLS the day after the rotation reported all-clear; added the
|
|
257
|
+
`curl` proof step, the AWS-owned-account `InUseBy` tell, and the missing `apigateway` permission.
|
|
258
|
+
**Fixed the same day:** `apigateway:GET`/`PATCH` added to the permission set, domain confirmed
|
|
259
|
+
REGIONAL, `regionalCertificateArn` swapped to the Amazon-issued `*.togahub.com` cert
|
|
260
|
+
(`a53d1ff6-...`, expires 2027-03-31). Verified `http=404 ssl=0` (was `http=000 ssl=10`), with
|
|
261
|
+
`expired.badssl.com` as the control that still fails. (rgirish)
|
|
176
262
|
- 2026-09-16 — Created from the account 654654170868 rotation: replaced an expiring 14-SAN IMPORTED
|
|
177
263
|
cert with 6 per-domain Amazon-issued DNS-validated certs, relinked 12 ALB listeners, 6 CloudFront
|
|
178
264
|
distributions and 11 EB environment configs; documented the EB saved-config trap. (rgirish)
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["bala"]
|
|
9
|
+
updated: 2026-09-17
|
|
10
|
+
owners: ["bala", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/TableView.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
related:
|
|
16
16
|
- ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
|
|
17
17
|
- ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
|
|
18
|
+
- ../../../clients/elite/features/supply2-tableview-config-drift.md
|
|
18
19
|
- tableview-field-metadata.md
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -86,6 +87,34 @@ the existing Compass convention and the stored clauses stay uniform whether they
|
|
|
86
87
|
or five. (The "expression field must not start with `(`" gotcha below is about the *field slot* of a
|
|
87
88
|
condition **after** the wrapper is removed — the two are not in conflict.)
|
|
88
89
|
|
|
90
|
+
### Subqueries ARE possible — use `/**/` in place of every space
|
|
91
|
+
|
|
92
|
+
The parser splits the stored clause on **literal spaces**, so a normal SQL subquery (which needs
|
|
93
|
+
spaces) cannot be stored in `apiWhereClause`. The way around it: write `/**/` instead of every
|
|
94
|
+
space. MySQL treats `/**/` as whitespace, and the parser sees no space at all. That makes an
|
|
95
|
+
arbitrary **correlated subquery** usable as the *field* side of a condition.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
(SELECT/**/c5.id/**/FROM/**/ItemClasses/**/c1/**/WHERE/**/c1.id=Items.itemClassId):notin:1:22:40
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Confirmed by reading `parseOptionsWhere` and simulating it in PHP (2026-09-17):
|
|
102
|
+
|
|
103
|
+
- Conditions split on `,` **only at matching paren depth** — commas nested deeper (e.g. inside a
|
|
104
|
+
`COALESCE(a,b,c)` within the subquery) are safe.
|
|
105
|
+
- Each condition then splits on `:` into field / operator / value.
|
|
106
|
+
- **The field side passes through to SQL unescaped**, so it can be any expression that has no
|
|
107
|
+
literal space, `:` or `,` at top depth.
|
|
108
|
+
- `notin` splits its value on `:`, so `:notin:1:22:40` becomes the IN-list `(1,22,40)`.
|
|
109
|
+
|
|
110
|
+
**Precedent:** Elite's prod clause on slug `item-fulfillments-for-sales-orders` already uses this
|
|
111
|
+
same `/**/` trick with a `CONCAT((SELECT ...))` subquery, so it is an established pattern, not a
|
|
112
|
+
one-off.
|
|
113
|
+
|
|
114
|
+
**Caveat — leave a comment.** This is powerful but it makes the stored clause hard to read for
|
|
115
|
+
anyone looking straight at `TableViews`. Always add a `--` comment in the migration explaining
|
|
116
|
+
what the subquery does.
|
|
117
|
+
|
|
89
118
|
### Join alias rule (which table-qualified name to use)
|
|
90
119
|
|
|
91
120
|
In `_underscore/Model/Client/TableView.php` (~line 89-96), the **first** occurrence of a joined
|
|
@@ -129,12 +158,27 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
|
|
|
129
158
|
starts with a letter (e.g. `IFNULL(...)`), never a bare parenthesized expression.
|
|
130
159
|
- **NULL FKs drop silently.** A plain `field:ne:x` excludes rows where `field IS NULL` too. Wrap
|
|
131
160
|
nullable columns in `IFNULL(col,<sentinel>)` when the intent is "exclude only these values."
|
|
161
|
+
- **A literal space ends the clause element** — that is why a subquery needs `/**/` for every
|
|
162
|
+
space (see above). The same applies to any expression field: no spaces, anywhere.
|
|
163
|
+
- **Filter on ids, not names.** Names in lookup tables are free-text imported from NetSuite and
|
|
164
|
+
can be renamed without the id changing, so a `name LIKE` match is the fragile choice. Proven on
|
|
165
|
+
Elite: filtering root classes by `name LIKE '%Services%'` left 94 items visible instead of the
|
|
166
|
+
correct 82, because "Advisory and Modernization" contains no "Services".
|
|
132
167
|
- **Operator allowlist.** `parseOptionsWhere` whitelists the operators above and rejects unknown
|
|
133
168
|
ones (e.g. `regexp` returns a 500) — see the Compass cost-centers doc. Numeric/regex filtering
|
|
134
169
|
that the grammar can't express must be done client-side or by adding an operator to api2.
|
|
135
170
|
|
|
136
171
|
## Change history
|
|
137
172
|
|
|
173
|
+
- 2026-09-17 — **Recorded that subqueries ARE possible in a stored `apiWhereClause`**: the parser
|
|
174
|
+
splits on literal spaces, so writing `/**/` in place of every space (MySQL reads it as
|
|
175
|
+
whitespace, the parser sees no space) lets an arbitrary correlated subquery act as the field
|
|
176
|
+
side of a condition. Confirmed the parser mechanics by reading `parseOptionsWhere` and
|
|
177
|
+
simulating it in PHP — commas split only at matching paren depth, the field side passes to SQL
|
|
178
|
+
unescaped, and `notin` splits its value on `:` so `:notin:1:22:40` yields the IN-list (1,22,40).
|
|
179
|
+
Noted the existing prod precedent (Elite's `item-fulfillments-for-sales-orders` clause already
|
|
180
|
+
uses `CONCAT((SELECT ...))` this way), and added the "filter on ids, not names" gotcha —
|
|
181
|
+
name-matching left 94 Elite items visible instead of 82. No code change. (rgirish)
|
|
138
182
|
- 2026-09-01 — Recorded that the parser **strips one leading `(` and one trailing `)` from the whole
|
|
139
183
|
clause before splitting**, so the wrapped single-condition form `(Items.catalogId:eq:1)` is safe
|
|
140
184
|
and is the house convention (this was previously only documented for the multi-condition form).
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: elite
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: [tcox, bala, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Elite/
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql
|
|
15
15
|
- dbchanges2/Client_Elite/2026-08-13a - InventoryGroupingsSurfaceOverrides.sql
|
|
16
16
|
- dbchanges2/Client_Elite/2026-09-04b - EliteInventoryHideServiceItems.sql
|
|
17
|
+
- dbchanges2/Client_Elite/2026-09-15a - InventoryHideServiceClassItems.sql
|
|
17
18
|
- dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql
|
|
18
19
|
- dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
|
|
19
20
|
- dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql
|
|
@@ -25,6 +26,8 @@ related:
|
|
|
25
26
|
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
26
27
|
- ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
|
|
27
28
|
- ../../../2.0/apps/_underscore/features/page-meta-context-field-settings.md
|
|
29
|
+
- ../../../1.0/apps/library/features/netsuite-item-class-sync.md
|
|
30
|
+
- ../../../2.0/apps/_underscore/features/item-classification-itemclasses.md
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
## Summary
|
|
@@ -276,33 +279,106 @@ the live Units view is `inventory_units` (`TableViews` id 21), not
|
|
|
276
279
|
This pairs with the existing team rule that **a committed migration records intent, not deployed
|
|
277
280
|
state** — the frontend repo records the *default*, not this client's state.
|
|
278
281
|
|
|
279
|
-
## Hiding service lines from Inventory — `2026-09-04b`
|
|
282
|
+
## Hiding service lines from Inventory — `2026-09-04b`, replaced by `2026-09-15a`
|
|
280
283
|
|
|
281
|
-
|
|
284
|
+
> **⚠ Current rule (2026-09-15): service items are hidden by their ROOT ItemClass, not by the
|
|
285
|
+
> `SVC-` partNumber prefix.** `2026-09-15a - InventoryHideServiceClassItems.sql` rewrites the
|
|
286
|
+
> `inventory_items` clause. The 2026-09-04b history below is kept because it explains the
|
|
287
|
+
> `IFNULL(...)` NULL-drop rule, which still applies.
|
|
288
|
+
|
|
289
|
+
`2026-09-15a - InventoryHideServiceClassItems.sql` rewrites `TableViews.apiWhereClause` for slug
|
|
290
|
+
`inventory_items` in `Client_Elite`. It walks the `ItemClasses` parent chain and excludes any item
|
|
291
|
+
whose **root** ItemClass id is **1** (Lifecycle Services), **22** (Advisory and Modernization) or
|
|
292
|
+
**40** (True Partner Services), using a correlated subquery written with `/**/` as whitespace —
|
|
293
|
+
see [apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md#subqueries-are-possible--use--in-place-of-every-space)
|
|
294
|
+
for the technique and the parser mechanics.
|
|
295
|
+
|
|
296
|
+
**No `_underscore` deploy is needed.** The whole check lives in the stored clause. An earlier draft
|
|
297
|
+
required a `_isServiceClass` model field on `_Model_Elite_Item`; **that field does not exist and is
|
|
298
|
+
not needed** — do not add one.
|
|
299
|
+
|
|
300
|
+
### Why the class check replaced the `SVC-` prefix
|
|
301
|
+
|
|
302
|
+
The `SVC-` prefix was an unreliable signal. Verified on **prod `Client_Elite`** (2026-09-15):
|
|
303
|
+
|
|
304
|
+
| Measure | Count |
|
|
305
|
+
|---|---|
|
|
306
|
+
| Items total | 122 |
|
|
307
|
+
| Service items by root ItemClass | 40 |
|
|
308
|
+
| Service items by `SVC-` prefix | 27 |
|
|
309
|
+
| Visible after the class filter | 82 |
|
|
310
|
+
|
|
311
|
+
The class check catches **18 services the prefix missed** — Cisco `CON-SNT-*` / `CON-ROB-*`
|
|
312
|
+
contracts, `LIC-ENT-*` licenses, `CarePacks - Fixed`, `PROJECT - CABLING`, `CONFIG/INSTALL` — with
|
|
313
|
+
**zero false hides**.
|
|
314
|
+
|
|
315
|
+
### Match on ids, never on class NAMES
|
|
316
|
+
|
|
317
|
+
Hardcoded ids 1 / 22 / 40 are the stable reference. Names are free-text imported from NetSuite and
|
|
318
|
+
can be renamed without the id changing. Proven wrong in practice here: filtering on
|
|
319
|
+
`name LIKE '%Services%'` returned **94** visible instead of the correct **82**, because "Advisory
|
|
320
|
+
and Modernization" contains no "Services".
|
|
321
|
+
|
|
322
|
+
### `Client_Elite` ItemClasses hierarchy (prod, verified 2026-09-15)
|
|
323
|
+
|
|
324
|
+
The table is **`ItemClasses`**, not `ItemClassifications`. Exactly **4 root rows**
|
|
325
|
+
(`parentItemClassId IS NULL`):
|
|
326
|
+
|
|
327
|
+
| id | Name | Kind |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| 1 | Lifecycle Services | service |
|
|
330
|
+
| 4 | Technology Sales | product |
|
|
331
|
+
| 22 | Advisory and Modernization | service |
|
|
332
|
+
| 40 | True Partner Services | service |
|
|
333
|
+
|
|
334
|
+
Tree depth is exactly **5 levels**, and every 5-level chain terminates at a true root (a query for
|
|
335
|
+
level-5 classes with a non-null parent returned 0 rows). So the migration's fixed 5-alias
|
|
336
|
+
`LEFT JOIN` walk resolves every item to its true root **today**. See
|
|
337
|
+
[the hierarchy import](../../../1.0/apps/library/features/netsuite-item-class-sync.md).
|
|
338
|
+
|
|
339
|
+
### Unclassified items stay visible — and 5 real services leak
|
|
340
|
+
|
|
341
|
+
**29 Items have `itemClassId` NULL.** The clause is a blocklist, so unclassified items stay
|
|
342
|
+
**visible** — the safer default, since hiding stock people expect to see is worse than showing a
|
|
343
|
+
few extra lines. But 5 of those 29 are real services by name and will newly appear in Inventory:
|
|
344
|
+
|
|
345
|
+
- `SVC-FS-SHIPPING-RECLAIM-LAPTOP`
|
|
346
|
+
- `SVC-FS-SHIPPING-SYSTEM SWAP-LAPTOP`
|
|
347
|
+
- `SVC-FS-DEPLOY-GW`
|
|
348
|
+
- `SVC-FS-SHIPPING-2DAY`
|
|
349
|
+
- `SVC-FS-ELITE-PERIPHERAL`
|
|
350
|
+
|
|
351
|
+
**Open decision left with the developer:** either set an ItemClass on those 5 in NetSuite, or AND
|
|
352
|
+
the old `SVC-` prefix check back into the clause.
|
|
353
|
+
|
|
354
|
+
### The previous rule — `2026-09-04b` (superseded for `inventory_items`)
|
|
355
|
+
|
|
356
|
+
`2026-09-04b - EliteInventoryHideServiceItems.sql` set:
|
|
282
357
|
|
|
283
358
|
| View | Clause |
|
|
284
359
|
|---|---|
|
|
285
|
-
| `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` |
|
|
286
|
-
| `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` |
|
|
360
|
+
| `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` — **still in force** |
|
|
361
|
+
| `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` — replaced |
|
|
287
362
|
|
|
288
|
-
`assetTypeId 2` = Elite's `Services` row. Notes
|
|
363
|
+
`assetTypeId 2` = Elite's `Services` row. Notes that still apply to anyone porting this:
|
|
289
364
|
|
|
290
365
|
- **The filter belongs in the database table view, not the frontend** — an explicit scope decision.
|
|
291
366
|
- **`IFNULL(...,0):ne:2` is mandatory, not cosmetic.** A bare `:ne:2` drops every `NULL` row and
|
|
292
|
-
would hide **all unstamped items
|
|
293
|
-
|
|
294
|
-
view. Grammar and the NULL-drop rule:
|
|
367
|
+
would hide **all unstamped items**. The shape is copied from Elite's existing
|
|
368
|
+
`item-fulfillments-for-sales-orders` view. Grammar and the NULL-drop rule:
|
|
295
369
|
[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
370
|
- **Both `UPDATE`s are guarded on the current value**, so a re-run is a no-op.
|
|
301
371
|
- `inventory_units` was targeted **because of the `SurfaceOverrides` finding above** — filtering
|
|
302
372
|
view 17 would have had no visible effect for Elite.
|
|
303
373
|
|
|
304
374
|
## Gotchas / known issues
|
|
305
375
|
|
|
376
|
+
- **A 6th NetSuite class level would silently leak services back into Inventory.** The
|
|
377
|
+
`2026-09-15a` clause walks a **fixed 5-alias** chain because Elite's tree is exactly 5 deep
|
|
378
|
+
today — but nothing in the schema caps the depth. If NetSuite adds a level under a service root,
|
|
379
|
+
those items reappear in Inventory with no error. Re-check the depth whenever the classification
|
|
380
|
+
tree changes.
|
|
381
|
+
|
|
306
382
|
- **A `Client_*` DB that predates a platform migration fails silently until a user opens the
|
|
307
383
|
view.** Nothing validates `TableViewJoins.parentRecordFieldId` against live `Core.RecordFields`,
|
|
308
384
|
so a deleted id sits there until it 500s. Worth checking for **any** client onboarded before
|
|
@@ -312,6 +388,18 @@ state** — the frontend repo records the *default*, not this client's state.
|
|
|
312
388
|
[error-reporting-issue-event](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
|
|
313
389
|
|
|
314
390
|
## Change history
|
|
391
|
+
- 2026-09-17 — **Replaced the `SVC-` partNumber prefix filter with an ItemClass-hierarchy check**
|
|
392
|
+
on the `inventory_items` view (`2026-09-15a - InventoryHideServiceClassItems.sql`): the clause
|
|
393
|
+
now walks the `ItemClasses` parent chain and excludes items whose ROOT class id is 1, 22 or 40,
|
|
394
|
+
via a `/**/`-whitespace correlated subquery. Prod numbers: 122 items, 40 service by class vs 27
|
|
395
|
+
by prefix — the class check catches 18 the prefix missed (Cisco CON-SNT-*/CON-ROB-*, LIC-ENT-*,
|
|
396
|
+
CarePacks, PROJECT - CABLING, CONFIG/INSTALL) with zero false hides; 82 visible after filtering.
|
|
397
|
+
**No `_underscore` deploy** — an earlier draft's `_isServiceClass` model field does not exist and
|
|
398
|
+
is not needed. Recorded the verified `Client_Elite` hierarchy (4 roots, exactly 5 levels, every
|
|
399
|
+
chain terminating at a true root) and the decision to match on **ids, not names** (a
|
|
400
|
+
`name LIKE '%Services%'` filter left 94 visible, not 82, because "Advisory and Modernization"
|
|
401
|
+
has no "Services" in it). Open item: 29 items have a NULL `itemClassId` and stay visible by
|
|
402
|
+
design, but 5 of them are real services and will newly appear. (rgirish)
|
|
315
403
|
- 2026-09-04 — **Corrected which table view Elite's Inventory page actually reads, then filtered
|
|
316
404
|
it.** `toga25-supply/src` shows the DEFAULT groupings using `inventory_items` +
|
|
317
405
|
`units-for-items-for-purchase-orders`, and `inventory_units` appears nowhere in `src` — reading as
|
package/package.json
CHANGED