toga-ai 1.0.699 → 1.0.701
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-filecabinet-restlet.md +122 -0
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-api-reference.md +22 -2
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +53 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +101 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +11 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +74 -0
- package/knowledge/standalone/standards/session-priming.md +12 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
| [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | The 1.0 side of the platform error-reporting pipeline (TRUE-78188). | library/app/error/capture.php, library/app/error.php, library/app/exception/business.php, library/app/api/toga2.php, library/app/cloud.php, worker/config.worker.ini, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
15
15
|
| [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 |
|
|
16
16
|
| [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 |
|
|
17
|
+
| [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 |
|
|
17
18
|
| [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 |
|
|
18
19
|
| [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 |
|
|
19
20
|
| [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,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NetSuite File Cabinet Content Retrieval via RESTlet (fetchInvoiceFile)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-29
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/api/netsuite/rest.php
|
|
13
|
+
- worker/crons/toga2/netsuite/netsuite_restlet_filecontent.js
|
|
14
|
+
- worker/config.worker.ini
|
|
15
|
+
related:
|
|
16
|
+
- netsuite-suiteql-api-reference.md
|
|
17
|
+
- netsuite-suiteql-rest-shim.md
|
|
18
|
+
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
How 1.0 pulls **File Cabinet binary content** (invoice PDFs) out of NetSuite over REST. The
|
|
24
|
+
short version: **there is no token-only, code-only REST path to File Cabinet binary content in
|
|
25
|
+
this account.** The `/record/v1/file` Record Service is not enabled, SuiteQL exposes only file
|
|
26
|
+
*metadata* (no content column), and the `media.nl` UI URL is not fetchable with an OAuth token.
|
|
27
|
+
The only REST mechanism is a **SuiteScript 2.1 RESTlet** that loads the file server-side and
|
|
28
|
+
returns its bytes base64-encoded. `App_Api_Netsuite_Rest::fetchInvoiceFile()` uses exactly that.
|
|
29
|
+
|
|
30
|
+
This replaced the SOAP `App_NetSuite::getInvoiceFile` as part of completing the SOAP→REST
|
|
31
|
+
migration of the togasupply sync path (see the
|
|
32
|
+
[per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md)).
|
|
33
|
+
|
|
34
|
+
## Why there is no token-only REST path (verified live, 2026-08-29)
|
|
35
|
+
|
|
36
|
+
Three routes were probed against this account and all three are dead ends for binary content:
|
|
37
|
+
|
|
38
|
+
- **REST Record Service `GET /record/v1/file/{id}` → HTTP 404 `NONEXISTENT_ID`**, *"Record type
|
|
39
|
+
'file' does not exist."* The `file` record type is simply not in this account's REST Record
|
|
40
|
+
Service. (The earlier `fetchInvoiceFile` design assumed this endpoint worked — it never did.)
|
|
41
|
+
- **SuiteQL `file` table exposes ONLY metadata** — `id, name, filetype, filesize, folder, url,
|
|
42
|
+
createddate, lastmodifieddate` and `is*` flags. There is **no binary/content column**. Good for
|
|
43
|
+
*finding* the file id by name; useless for its bytes.
|
|
44
|
+
- **The SuiteQL `url` is a `media.nl` UI link, not an API resource.** It is a
|
|
45
|
+
`/core/media/media.nl?...&c=<acct>&h=<hash>` link on the `<acct>.app.netsuite.com` **UI domain**.
|
|
46
|
+
Fetching it — anonymous OR with an OAuth Bearer — is rejected (redirects to `page_not_found`),
|
|
47
|
+
because invoice PDFs are **not** flagged "Available Without Login" and the OAuth token does not
|
|
48
|
+
authorize the UI media servlet.
|
|
49
|
+
|
|
50
|
+
Conclusion: to get File Cabinet bytes over REST you must run server-side script. That is the
|
|
51
|
+
RESTlet.
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
### NetSuite side — the RESTlet
|
|
56
|
+
|
|
57
|
+
`worker/crons/toga2/netsuite/netsuite_restlet_filecontent.js` is a **SuiteScript 2.1 RESTlet**
|
|
58
|
+
that does `N/file.load(id).getContents()` and returns the content **base64-encoded**. Deployed in
|
|
59
|
+
NetSuite as script **`customscript2829`** / deployment **`customdeploy1`** (non-secret identifiers),
|
|
60
|
+
with the **RESTlets** OAuth scope enabled on the **"TOGA OAuth 2.0"** integration record.
|
|
61
|
+
|
|
62
|
+
### Client side
|
|
63
|
+
|
|
64
|
+
- **`App_Api_Netsuite_Rest::callRestlet()`** — GETs
|
|
65
|
+
`<acct>.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=<id>&deploy=<id>` (note the
|
|
66
|
+
**`restlets.api.netsuite.com`** host, distinct from the SuiteQL/REST `suitetalk` host).
|
|
67
|
+
- **`fetchInvoiceFile()`** — SuiteQL `file`-table lookup by name to get the file **id** →
|
|
68
|
+
`callRestlet()` → `base64_decode()` → raw bytes. **Fail-loud preserved:** no file row → `null`;
|
|
69
|
+
a file found but unretrievable, or a RESTlet error → **throws** (never silently returns empty).
|
|
70
|
+
- **Config:** the script/deploy ids live in `worker/config.worker.ini` `[netsuite]` as
|
|
71
|
+
`restletScriptId` / `restletDeployId`.
|
|
72
|
+
|
|
73
|
+
### Validated byte-identical to SOAP
|
|
74
|
+
|
|
75
|
+
RESTlet-retrieved PDFs were confirmed **byte-identical** (matching length + md5) to what SOAP
|
|
76
|
+
`getInvoiceFile` returned, across 3 Compass invoices (~265 KB each). Zero `fetchInvoiceFile` /
|
|
77
|
+
`INVALID_LOGIN` errors after deploy. This also unblocked Endeavor Health's whole pipeline — its
|
|
78
|
+
INVOICES section had been throwing on the broken file fetch and collateral-freezing
|
|
79
|
+
IR/IF/InvAdj.
|
|
80
|
+
|
|
81
|
+
## Gotchas / known issues
|
|
82
|
+
|
|
83
|
+
- **⚠ NetSuite issues ONE active OAuth 2.0 token per client credential — a second token with a
|
|
84
|
+
DIFFERENT scope INVALIDATES the first.** Fetching a separately-scoped `restlets` token caused the
|
|
85
|
+
very next SuiteQL call to fail with **HTTP 400 `INVALID_LOGIN`**. **Fix: request ONE
|
|
86
|
+
combined-scope token** — a single `OAUTH_SCOPE` of `rest_webservices restlets` — never two
|
|
87
|
+
separate tokens. This requires the **RESTlets scope enabled on the integration record**. The
|
|
88
|
+
initially-tried "isolated separate restlets token" design is what caused the `INVALID_LOGIN` and
|
|
89
|
+
was removed. (This is a general OAuth fact: any future added scope must be folded into the single
|
|
90
|
+
combined-scope request, not fetched as a second token.)
|
|
91
|
+
- **⚠ A RESTlet whose function returns an object requires the REQUEST to carry
|
|
92
|
+
`Content-Type: application/json`** — otherwise NetSuite rejects it with HTTP 400
|
|
93
|
+
`INVALID_RETURN_DATA_FORMAT` (*"Invalid data format. You should return text."*). Set the request
|
|
94
|
+
content type even though `callRestlet` is a GET with no meaningful body.
|
|
95
|
+
- **PHP 7.2 prod constraint** applies to `rest.php` as everywhere — lint with
|
|
96
|
+
`C:\xampp7\php\php.exe -l` before deploying (a single 7.4+ token parse-errors the whole class;
|
|
97
|
+
see the [api reference](netsuite-suiteql-api-reference.md) blast-radius gotcha).
|
|
98
|
+
|
|
99
|
+
## Change history
|
|
100
|
+
|
|
101
|
+
- 2026-08-29 — Created. Migrated invoice-PDF retrieval off SOAP `getInvoiceFile` to a SuiteScript
|
|
102
|
+
2.1 RESTlet (`customscript2829`/`customdeploy1`) after verifying live that **no token-only REST
|
|
103
|
+
path to File Cabinet binary exists** in this account: `/record/v1/file/{id}` 404s
|
|
104
|
+
(`NONEXISTENT_ID`, file record not in the REST Record Service), SuiteQL `file` is metadata-only
|
|
105
|
+
(no content column), and the `media.nl` UI url is not OAuth-fetchable. Added
|
|
106
|
+
`App_Api_Netsuite_Rest::callRestlet()` + rewrote `fetchInvoiceFile` (SuiteQL id lookup →
|
|
107
|
+
callRestlet → base64_decode, fail-loud). Recorded two RESTlet/OAuth gotchas: one active token per
|
|
108
|
+
client credential so scopes must be combined (`rest_webservices restlets` in a single request,
|
|
109
|
+
else the next SuiteQL call 400s `INVALID_LOGIN`), and an object-returning RESTlet needs request
|
|
110
|
+
`Content-Type: application/json` (else 400 `INVALID_RETURN_DATA_FORMAT`). RESTlet PDFs verified
|
|
111
|
+
byte-identical to SOAP across 3 Compass invoices; unblocked Endeavor Health's pipeline. (jcardinal)
|
|
112
|
+
|
|
113
|
+
## Related docs
|
|
114
|
+
|
|
115
|
+
- [`netsuite-suiteql-api-reference.md`](netsuite-suiteql-api-reference.md) — auth (OAuth2/JWT),
|
|
116
|
+
SuiteQL mechanics, the `fetch*ById()` helper family that `fetchInvoiceFile` belongs to.
|
|
117
|
+
- [`netsuite-suiteql-rest-shim.md`](netsuite-suiteql-rest-shim.md) — REST-shim field/shape
|
|
118
|
+
semantics.
|
|
119
|
+
- [per-client-sync](../../worker/features/netsuite-togasupply-per-client-sync.md) — the togasupply
|
|
120
|
+
engine whose INVOICES section consumes `fetchInvoiceFile`.
|
|
121
|
+
</content>
|
|
122
|
+
</invoke>
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- test/@dave/Junk Drawer/nsq.php
|
|
15
15
|
related:
|
|
16
16
|
- netsuite-suiteql-rest-shim.md
|
|
17
|
+
- netsuite-filecabinet-restlet.md
|
|
17
18
|
- ../architecture.md
|
|
18
19
|
- ../../worker/features/forecast2-netsuite-reconciliation.md
|
|
19
20
|
- ../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md
|
|
@@ -42,6 +43,12 @@ This doc is the **broad map** (mechanics, schemas, codes, performance). For the
|
|
|
42
43
|
a placeholder `CERTIFICATE_ID_HERE` that `initialize()` explicitly rejects.
|
|
43
44
|
- `readConfig()` supports two framework contexts: 1.0 via `App_Registry::get('config')['netsuite']`,
|
|
44
45
|
and 2.0 via `_Config::netsuite()`.
|
|
46
|
+
- **⚠ ONE active OAuth token per client credential — combine scopes, never fetch a second token.**
|
|
47
|
+
NetSuite invalidates the first token when a second is requested with a *different* scope: after a
|
|
48
|
+
separately-scoped `restlets` token was fetched, the next SuiteQL call 400'd `INVALID_LOGIN`. Request
|
|
49
|
+
a single combined `OAUTH_SCOPE` (currently `rest_webservices restlets`, the latter enabled on the
|
|
50
|
+
"TOGA OAuth 2.0" integration for `fetchInvoiceFile`). Any future scope must be folded into this one
|
|
51
|
+
request. See [File Cabinet content via RESTlet](netsuite-filecabinet-restlet.md).
|
|
45
52
|
|
|
46
53
|
## Bootstrapping the client from a standalone script
|
|
47
54
|
|
|
@@ -365,8 +372,13 @@ exclusively — zero `App_NetSuite::` SOAP network calls remain** in `common_syn
|
|
|
365
372
|
- **`fetchItemFlagsByIds`** — batch item serialized/fulfillable flags; replaces
|
|
366
373
|
`getItemSerializedAndFulfillable`.
|
|
367
374
|
- **`fetchInventoryAdjustmentById`** — replaces the `diagnose_hh` script's SOAP `getObject`.
|
|
368
|
-
- **`fetchInvoiceFile`** — SuiteQL `file`-table lookup
|
|
369
|
-
`base64_decode` → raw bytes. Replaces SOAP `getInvoiceFile`.
|
|
375
|
+
- **`fetchInvoiceFile`** — SuiteQL `file`-table lookup for the id → **RESTlet** call
|
|
376
|
+
(`callRestlet()`) → `base64_decode` → raw bytes. Replaces SOAP `getInvoiceFile`. **NOT** a REST
|
|
377
|
+
record GET — `GET /record/v1/file/{id}` 404s (`NONEXISTENT_ID`; the `file` record is not in this
|
|
378
|
+
account's REST Record Service), SuiteQL `file` is metadata-only, and the `media.nl` url is not
|
|
379
|
+
OAuth-fetchable, so a SuiteScript RESTlet is the only REST path to File Cabinet binary. Full
|
|
380
|
+
mechanics + the two OAuth/RESTlet gotchas: [File Cabinet content via
|
|
381
|
+
RESTlet](netsuite-filecabinet-restlet.md).
|
|
370
382
|
|
|
371
383
|
`listCustomers()` also gained a **`companyName`** field on its shim (the `netsuite_customers.php` cron
|
|
372
384
|
moved from `listAllCustomers` → `listCustomers`). REST-shim *shape* traps that bite these helpers (an
|
|
@@ -442,6 +454,14 @@ numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
|
442
454
|
|
|
443
455
|
## Change history
|
|
444
456
|
|
|
457
|
+
- 2026-08-29 — Corrected `fetchInvoiceFile` (deploy/verify session): it is **RESTlet-based**, not a
|
|
458
|
+
REST record GET — `GET /record/v1/file/{id}` 404s (`file` record not in this account's REST Record
|
|
459
|
+
Service), SuiteQL `file` is metadata-only, `media.nl` isn't OAuth-fetchable, so a SuiteScript 2.1
|
|
460
|
+
RESTlet is the only REST path to File Cabinet binary; new
|
|
461
|
+
[File Cabinet content via RESTlet](netsuite-filecabinet-restlet.md) doc holds the mechanics. Added the
|
|
462
|
+
**one-active-token-per-credential** auth rule to Authentication (combine scopes into a single
|
|
463
|
+
`rest_webservices restlets` request; a separate second token 400s the next SuiteQL call
|
|
464
|
+
`INVALID_LOGIN`). (jcardinal)
|
|
445
465
|
- 2026-08-29 — Documented the **`fetch*ById()` single-record helper family** (`fetchItemReceiptById`,
|
|
446
466
|
`fetchPurchaseOrderById`, `fetchSalesOrderByIdFull`, `fetchItemFlagsByIds`,
|
|
447
467
|
`fetchInventoryAdjustmentById`, `fetchInvoiceFile`) that replaced the SOAP `App_NetSuite::get*` calls,
|
|
@@ -122,6 +122,40 @@ Forecast2 tables already store.
|
|
|
122
122
|
skew — the field just reads as its default — so verify the exact scriptId string on both the write and
|
|
123
123
|
the read side. See [NYCHH TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
124
124
|
|
|
125
|
+
- **`fetchPurchaseOrderById()` omitted `entity` from its SELECT and never set `$shim->entity` — so
|
|
126
|
+
every PO shim had a null vendor and the fail-loud guard fired every run (2026-08-29).** Unlike its
|
|
127
|
+
sibling `listPurchaseOrders` (which DOES select `entity` + `BUILTIN.DF(entity)` and populates the
|
|
128
|
+
shim's vendor), `fetchPurchaseOrderById`'s SuiteQL header query left `entity` out and the shim never
|
|
129
|
+
built `$shim->entity`. The item-receipt path (`syncItemReceiptFromNetsuite` →
|
|
130
|
+
`syncPurchaseOrderFromNetsuite`) therefore always saw a null entity, and the fail-loud guard
|
|
131
|
+
`if (empty($nsPurchaseOrder->entity)) throw 'has no vendor/entity'` threw on **every** run — freezing
|
|
132
|
+
**Canon** and **Endeavor Health** ITEM_RECEIPTS at `1-RUNNING` (window collapsed ÷3 to 1s), watermarks
|
|
133
|
+
frozen ~8 and ~5 days. **Fix:** added `entity, BUILTIN.DF(entity) AS entity_name` to the header SELECT
|
|
134
|
+
and populated `$shim->entity` as `{internalId, name}` (matching the other shims); a genuinely
|
|
135
|
+
vendorless PO still throws. Same class as the `tl.location` regression below and the missing-entity
|
|
136
|
+
guard was correct — the SELECT was the defect. **When adding a shim, every field the guard/consumer
|
|
137
|
+
reads must actually be in the SELECT.**
|
|
138
|
+
- **The entity fix was only the first of a chain — the real fix completed the shim to MATCH
|
|
139
|
+
`listPurchaseOrders`' FULL shape (deploy/verify session, 2026-08-29).** `syncPurchaseOrderFromNetsuite`
|
|
140
|
+
(reached via the item-receipt path) reads a whole set of PO properties **unguarded**, so each one the
|
|
141
|
+
`fetchPurchaseOrderById` shim omitted threw a 1.0 warning → `App_Error` `exit()` → froze the section —
|
|
142
|
+
an "entity → createdFrom → shippingAddress → customForm → tranId → tranDate → line/description/rate"
|
|
143
|
+
whack-a-mole, one missing field surfacing the next. Fixed by making the per-id shim reproduce the
|
|
144
|
+
sibling **list** shape exactly: added `entity` + `trandate` to the header SuiteQL, `tl.rate` + `tl.memo`
|
|
145
|
+
to the line query, and populated `createdFrom` / `shippingAddress` / `customForm` from the per-id REST
|
|
146
|
+
GET. **Lesson (sharper than "a field the consumer reads must be in the SELECT"): a REST shim consumed
|
|
147
|
+
by a SHARED sync method must reproduce the FULL sibling-`list*` shape — every header field, every line
|
|
148
|
+
field, every nested object — not just the fields the one path you are testing happens to read.** This
|
|
149
|
+
cleared Canon(12)/Endeavor(11) ITEM_RECEIPTS to IDLE.
|
|
150
|
+
|
|
151
|
+
- **`buildSalesOrderShims()` / `fetchSalesOrderByIdFull()` SO-item shims now carry `createdPo` + `poRate`
|
|
152
|
+
(2026-08-29).** `syncPurchaseOrderFromNetsuite`'s SO-reconciliation loop reads
|
|
153
|
+
`$nsSalesOrderItem->createdPo` ({internalId}|null) and `$nsSalesOrderItem->poRate` (float|null)
|
|
154
|
+
**unguarded** — an `Undefined property $createdPo` → `exit()`. This loop was **newly reached** by the
|
|
155
|
+
`createdFrom` completion above, which is exactly how the next-missing-field surfaced. Both are now read
|
|
156
|
+
defensively from the per-id REST `salesOrder` sublist already fetched, so the SO-item shim matches what
|
|
157
|
+
the loop consumes. Same "reproduce the full sibling shape" class as the PO-shim completion.
|
|
158
|
+
|
|
125
159
|
- **A shim must produce the SOAP object's SHAPE, not just its scalar fields — an absent nested list
|
|
126
160
|
crashes 1.0 (2026-08-29).** `fetchCustomerById()`'s `addressbookList` was where the AIG lookup phase
|
|
127
161
|
crashed: a REST customer with no/odd address made the shim omit the nested `addressbook` structure the
|
|
@@ -215,6 +249,25 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
215
249
|
|
|
216
250
|
## Change history
|
|
217
251
|
|
|
252
|
+
- 2026-08-29 — **Completed `fetchPurchaseOrderById` to the full `listPurchaseOrders` shape** and added
|
|
253
|
+
the SO-item `createdPo`/`poRate` shim fields (same deploy/verify session, after the entity fix). The
|
|
254
|
+
entity SELECT was only the first missing field: `syncPurchaseOrderFromNetsuite` reads `createdFrom`,
|
|
255
|
+
`shippingAddress`, `customForm`, `tranId`, `tranDate`, and line `rate`/`description` unguarded, so each
|
|
256
|
+
omission threw in turn (whack-a-mole). Fixed by matching the sibling **list** shape exactly (header:
|
|
257
|
+
`entity`+`trandate`; line: `tl.rate`+`tl.memo`; populated `createdFrom`/`shippingAddress`/`customForm`
|
|
258
|
+
from the per-id REST GET). Completing `createdFrom` newly reached the SO-reconciliation loop, exposing
|
|
259
|
+
the next unguarded reads `$nsSalesOrderItem->createdPo`/`poRate` — now carried on the
|
|
260
|
+
`buildSalesOrderShims`/`fetchSalesOrderByIdFull` SO-item shims (read from the per-id REST `salesOrder`
|
|
261
|
+
sublist). Lesson: a shim consumed by a shared sync method must reproduce the FULL sibling-list shape,
|
|
262
|
+
not the fields the tested path happens to read. (jcardinal)
|
|
263
|
+
- 2026-08-29 — Recorded `fetchPurchaseOrderById()`'s **missing `entity` SELECT** (deploy/verify
|
|
264
|
+
session): its header query omitted `entity`/`BUILTIN.DF(entity)` and the shim never set
|
|
265
|
+
`$shim->entity` (unlike `listPurchaseOrders`), so the PO vendor was always null and the
|
|
266
|
+
`syncPurchaseOrderFromNetsuite` fail-loud missing-entity guard threw every run — the actual cause of
|
|
267
|
+
the **Canon + Endeavor Health ITEM_RECEIPTS** freeze at `1-RUNNING` (watermarks ~8/~5 days). Fixed
|
|
268
|
+
additively (add `entity, BUILTIN.DF(entity) AS entity_name`; populate `$shim->entity` as
|
|
269
|
+
`{internalId,name}`); same "a field the shim consumes must be in the SELECT" class as `tl.location`.
|
|
270
|
+
Deployed ~12:52; both sections self-healed RUNNING→IDLE and resumed advancing. (jcardinal)
|
|
218
271
|
- 2026-08-29 — Recorded two shim-shape facts from root-causing the 5 stuck togasupply integrations
|
|
219
272
|
(which were **shim-shape mismatches, not dead SOAP** — see the
|
|
220
273
|
[per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md)): a
|
|
@@ -583,8 +583,10 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
583
583
|
`App_NetSuite::` (SOAP) network calls remain** anywhere in the `common_sync_togasupply.php` engine
|
|
584
584
|
or the `*FromNetsuite()` methods it calls — so the whole tier-wide-freeze class above is now driven
|
|
585
585
|
by REST-shim shape, not SOAP availability. New REST completeness pieces built in `library`:
|
|
586
|
-
`fetchInvoiceFile` (SuiteQL `file` lookup +
|
|
587
|
-
|
|
586
|
+
`fetchInvoiceFile` (SuiteQL `file` id lookup + **RESTlet** content fetch → `base64_decode` → raw
|
|
587
|
+
bytes; replaces `getInvoiceFile` — the `/record/v1/file/{id}` approach 404s, so a SuiteScript RESTlet
|
|
588
|
+
is the only REST path — see [File Cabinet content via RESTlet](../../library/features/netsuite-filecabinet-restlet.md)),
|
|
589
|
+
`getItemSerializedAndFulfillable` → `fetchItemFlagsByIds`,
|
|
588
590
|
the `diagnose_hh` script's `getObject` → `fetchInventoryAdjustmentById`, and
|
|
589
591
|
`worker/crons/sync/netsuite/netsuite_customers.php` `listAllCustomers` → `listCustomers` (with
|
|
590
592
|
`companyName` added to the `listCustomers` shim).
|
|
@@ -601,8 +603,105 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
601
603
|
Note `SalesOrderItems.lineNumber` is a **`smallint`**, so the parked base must stay within smallint
|
|
602
604
|
range for the client's line counts (headroom 1000 is safe at observed volumes).
|
|
603
605
|
|
|
606
|
+
- **⚠ Canon + Endeavor Health ITEM_RECEIPTS froze because `fetchPurchaseOrderById` never populated the
|
|
607
|
+
PO vendor (deployed/verified 2026-08-29).** The PO shim's `->entity` was always null (its SuiteQL
|
|
608
|
+
SELECT omitted `entity`, unlike `listPurchaseOrders`), so the fail-loud missing-entity guard in
|
|
609
|
+
`syncPurchaseOrderFromNetsuite` (reached via `syncItemReceiptFromNetsuite`) threw **every** run —
|
|
610
|
+
freezing Canon (clientId 12) and Endeavor Health (clientId 11) ITEM_RECEIPTS at `1-RUNNING`,
|
|
611
|
+
watermarks frozen ~8/~5 days. Fix is in the shim
|
|
612
|
+
([rest-shim doc](../../library/features/netsuite-suiteql-rest-shim.md)): add
|
|
613
|
+
`entity, BUILTIN.DF(entity) AS entity_name` and build `$shim->entity`. `library`-only, additive; a
|
|
614
|
+
genuinely vendorless PO still throws. Deployed ~12:52 — both sections self-healed RUNNING→IDLE and
|
|
615
|
+
resumed advancing. Same class as the missing `tl.location` SELECT.
|
|
616
|
+
|
|
617
|
+
- **⚠ Diagnostic refinement — the stuck signal is specifically `1-RUNNING`, NOT any `*-RUNNING`.** A
|
|
618
|
+
section frozen because it throws every run collapses its window ÷3 all the way to **1 second**, so its
|
|
619
|
+
`NETSUITE_EXECUTION_MODE_*` reads exactly **`1-RUNNING`**. A **large-window** `<big>-RUNNING` (e.g.
|
|
620
|
+
`864000-RUNNING`) is usually just a healthy section caught **mid-execution** at the query instant —
|
|
621
|
+
not stuck. And a large-window value with a *years-old* watermark can be a legitimate long **backfill**
|
|
622
|
+
(e.g. NYCHH backfilling from 2020/2021), also not a bug. Read the window magnitude, not just the
|
|
623
|
+
RUNNING state. **Sweep method:** read every client's `Client_<X>.Parameters` for
|
|
624
|
+
`NETSUITE_EXECUTION_MODE_*` / `NETSUITE_LAST_SYNC_DATETIME_*`; a cross-schema `UNION` over many client
|
|
625
|
+
DBs needs `CONVERT(<col> USING utf8mb4)` on the value column or MySQL raises *"Illegal mix of
|
|
626
|
+
collations."* Then map each stuck client/section to an exact crash **file:line** via prod
|
|
627
|
+
`Logs.Event` — note **`Logs.Issue` has no `clientId` column but `Logs.Event` does**, so filter
|
|
628
|
+
`Logs.Event` by `clientId`.
|
|
629
|
+
|
|
630
|
+
- **OPEN, NOT FIXED (observed 2026-08-29, low priority — separate tickets):**
|
|
631
|
+
- **AIG (clientId 32, held account)** crashes in the **LOOKUPS** phase at
|
|
632
|
+
`common_sync_togasupply.php:463` with `Undefined property: stdClass::$companyName` — a
|
|
633
|
+
customer-shim shape gap that **survived** the earlier `fetchCustomerById` `addressbookList` fix.
|
|
634
|
+
(AIG remains a held/non-supply account — see the AIG gotcha above.)
|
|
635
|
+
- **Adyen (clientId 48)** crashes at `common_sync_togasupply.php:99` *"Invalid API Response"* from
|
|
636
|
+
`App_Api_Toga2::send()` at **setup** — it is **stuck-IDLE** (crashes before any section writes
|
|
637
|
+
RUNNING, so the `1-RUNNING` sweep misses it — same class as a LOOKUPS/setup abort). Likely an
|
|
638
|
+
**improperly onboarded** client (cf. AIG "listed ≠ functioning"). No knowledge-base client profile
|
|
639
|
+
exists for Adyen; no supply work has been done for it — confirm it's a real supply client before
|
|
640
|
+
onboarding effort.
|
|
641
|
+
Both are pre-existing and unrelated to this session's fixes.
|
|
642
|
+
|
|
643
|
+
- **⚠ A serialized unit's OWN location can differ from its receipt line's — auto-create it before the
|
|
644
|
+
`/units` write or api2 400s EV-12 (fixed 2026-08-29, `library/app/api/toga2.php`
|
|
645
|
+
`syncItemReceiptFromNetsuite`).** The unit carries its own `custitemnumber_location` (referenced on the
|
|
646
|
+
`/units` write by `c_netsuiteInternalLocationId`); when that location differs from the receipt line's
|
|
647
|
+
and isn't provisioned in the client DB, api2 rejects the unit write **EV-12**. The fix now **POSTs
|
|
648
|
+
`/locations` for the unit's own location before the write**, mirroring the existing item-line location
|
|
649
|
+
create one level up. Shared/general fix (any client running serialized item receipts), surfaced by
|
|
650
|
+
**GroWrk** location **1569**. Deployed.
|
|
651
|
+
|
|
652
|
+
- **⚠ The exact api2 rejection detail lives in `Logs_<Client>.Api`, not `Logs.Event` (which TRUNCATES
|
|
653
|
+
it).** When a section throws on an api2 non-success, `Logs.Event` records the error envelope but
|
|
654
|
+
**truncates** the useful detail (the `EV-9`/`EV-12` field/`searchCriteria` specifics). The full
|
|
655
|
+
request/response is in the **per-client** `Logs_<Client>.Api` table — **join on the `transactionId`
|
|
656
|
+
carried in the error envelope**; it has `method`, `route`, `responseCode`, `requestPayload`, and
|
|
657
|
+
`responsePayload`. So the diagnosis chain is: `Client_<X>.Parameters` (localize the stuck section) →
|
|
658
|
+
`Logs.Event` filtered by `clientId` (crash file:line + `transactionId`) → `Logs_<Client>.Api` on that
|
|
659
|
+
`transactionId` (the untruncated api2 rejection). (Distinct from the fatal-before-logging case where the
|
|
660
|
+
api2 request dies before the middleware and writes **no** `Logs_<Client>.Api` row — then read
|
|
661
|
+
`Logs.Issue` instead; see the 2026-08-18 poison-order diagnostic trap above.)
|
|
662
|
+
|
|
663
|
+
- **⚠ The shared `syncTransferOrderFromNetsuite` item-sync is NON-IDEMPOTENT — it creates DUPLICATE
|
|
664
|
+
`TransferOrderItems` every run (found 2026-08-29, NOT fixed; affects stocking-SO TOs too).** The shared
|
|
665
|
+
item loop does not reconcile against existing rows, so every sync appends the TO's items again: an
|
|
666
|
+
attempted real-TrnfrOrd import reached **22 duplicate items all `lineNumber` 1** on TO #15, and a
|
|
667
|
+
**pre-existing stocking-SO** transfer order (`Client_Nychh.TransferOrders` id **1** / number **62541**)
|
|
668
|
+
already carries **13 duplicates** — proving the bug is in the **shared** loop, not the real-TO path.
|
|
669
|
+
Because of this (plus an api2 `transferOrderItem`-reference gap and a receipt-line↔TO-line mapping
|
|
670
|
+
mismatch), the **real NetSuite Transfer Order (TrnfrOrd) item-receipt feature is INTENTIONALLY SKIPPED**
|
|
671
|
+
— a documented skip in `common_sync_togasupply.php`, exactly like the existing Return-Authorization
|
|
672
|
+
(`RtnAuth`) receipt skip — pending a proper rebuild. Full state, the inactive scaffolding left staged,
|
|
673
|
+
and the rebuild spec are on the
|
|
674
|
+
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md)
|
|
675
|
+
doc. **Do not re-enable real-TO receipts until the shared item loop is made idempotent (which also
|
|
676
|
+
fixes the stocking-SO dups and cleans the pre-existing TO 1).**
|
|
677
|
+
|
|
604
678
|
## Change history
|
|
605
679
|
|
|
680
|
+
- 2026-08-29 — **Stabilize session, additional durable findings.** (a) `syncItemReceiptFromNetsuite`
|
|
681
|
+
now **auto-creates a serialized unit's OWN location** (`custitemnumber_location` → POST `/locations`)
|
|
682
|
+
before the `/units` write, mirroring the item-line location create — a general fix surfaced by GroWrk
|
|
683
|
+
location 1569 (unit location ≠ receipt-line location → EV-12). (b) Recorded the diagnostic that
|
|
684
|
+
`Logs.Event` **truncates** the api2 rejection detail — the untruncated `method`/`route`/`responseCode`/
|
|
685
|
+
`requestPayload`/`responsePayload` is in the per-client `Logs_<Client>.Api` table, joined on the
|
|
686
|
+
envelope's `transactionId`. (c) Recorded that the shared `syncTransferOrderFromNetsuite` item-sync is
|
|
687
|
+
**non-idempotent** (duplicate `TransferOrderItems` every run — TO #15 reached 22 dups, pre-existing
|
|
688
|
+
stocking-SO TO 1/62541 has 13), so it affects stocking-SO TOs too; the **real-TrnfrOrd item-receipt
|
|
689
|
+
feature was intentionally SKIPPED** in `common_sync_togasupply.php` (like the RtnAuth skip) pending a
|
|
690
|
+
rebuild — rebuild spec on the NYCHH TransferOrders-import doc. (jcardinal)
|
|
691
|
+
- 2026-08-29 — **Deploy/verify/stabilize session for the SOAP→REST conversion.** Root-caused the
|
|
692
|
+
**Canon + Endeavor Health ITEM_RECEIPTS** freeze: `fetchPurchaseOrderById` never populated the PO
|
|
693
|
+
vendor (`entity` missing from its SuiteQL SELECT), so `syncPurchaseOrderFromNetsuite`'s fail-loud
|
|
694
|
+
missing-entity guard threw every run — fixed in the shim (add `entity`/`BUILTIN.DF(entity)`, build
|
|
695
|
+
`$shim->entity`); deployed ~12:52, both sections self-healed. Migrated **invoice-PDF retrieval to a
|
|
696
|
+
RESTlet** (`/record/v1/file/{id}` 404s — no token-only REST path to File Cabinet binary exists), which
|
|
697
|
+
unblocked Endeavor's whole pipeline; new [File Cabinet content via
|
|
698
|
+
RESTlet](../../library/features/netsuite-filecabinet-restlet.md) doc. Recorded the **`1-RUNNING`
|
|
699
|
+
(window collapsed to 1s) = stuck** vs large-window `*-RUNNING` (healthy/mid-run or legit backfill)
|
|
700
|
+
diagnostic, the cross-schema `Parameters` sweep needing `CONVERT(... USING utf8mb4)`, and that
|
|
701
|
+
`Logs.Event` (not `Logs.Issue`) carries `clientId`. Logged two OPEN, unfixed items: **AIG** (clientId
|
|
702
|
+
32) LOOKUPS crash at `:463` (`Undefined property $companyName`, customer-shim gap that survived the
|
|
703
|
+
addressbook fix) and **Adyen** (clientId 48) setup crash at `:99` (stuck-IDLE, likely improperly
|
|
704
|
+
onboarded). (jcardinal)
|
|
606
705
|
- 2026-08-29 — **Root-caused the 5 stuck togasupply integrations as REST-shim SHAPE mismatches, not
|
|
607
706
|
dead SOAP, and fixed them.** SOAP was up; the earlier SOAP→REST conversion left shims returning
|
|
608
707
|
null/absent where SOAP had values, and a 1.0 unguarded read of that null → PHP warning →
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
|
|
6
6
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
7
|
-
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql |
|
|
7
|
+
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
|
|
8
8
|
| [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | When a business rule says *"only one X per physical address"*, comparing address rows field-for-field does **not** work: the same dwelling is spelled many diffe | _underscore/Model/Rate/Entitlement.php |
|
|
9
9
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php, _underscore/Component/Library/Carriers/Usps/Usps.php |
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
|
|
@@ -19,6 +19,8 @@ files:
|
|
|
19
19
|
- dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql
|
|
20
20
|
- dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
|
|
21
21
|
- dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql
|
|
22
|
+
- dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql
|
|
23
|
+
- dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
## Summary
|
|
@@ -490,6 +492,15 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
490
492
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
491
493
|
|
|
492
494
|
## Change history
|
|
495
|
+
- **2026-08-29** — Two more worked examples of the **standard-field write grant via hardcoded
|
|
496
|
+
`Core.RecordFields` id literal** (the cross-cluster caveat — a `Client_*`→`Core` JOIN is refused on the
|
|
497
|
+
prod client cluster), each cloning a working sibling: `ItemReceipts.transferOrderId` (Core RF **2153**,
|
|
498
|
+
clone `purchaseOrderId` **85**) and `ItemReceiptItems.transferOrderItemId` (Core RF **2152**, clone
|
|
499
|
+
`purchaseOrderItemId` **471**). Correct + deployed but **dormant** — they only take effect once NYCHH's
|
|
500
|
+
real-`TrnfrOrd` item-receipt path is re-enabled (deferred; see
|
|
501
|
+
[NYCHH TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md)). Note
|
|
502
|
+
for the team: this recipe was in this doc all along, yet the field-ACL model was reverse-engineered from
|
|
503
|
+
DB/code first this session before the doc was consulted — search the KB first. (jcardinal)
|
|
493
504
|
- **2026-08-28** — Scoped the **third gate**: `Core.AclActions` + `AclActionPermissions` is
|
|
494
505
|
`_Model_Core_Page::meta()` behavior only, and is **dead config in the surface-driven `toga25-supply`
|
|
495
506
|
app** — `_Model_Core_Surface::resolve()` drops an element only when the element itself declares an
|
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)_ — 22 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 31 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)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [NYCHH inventory-adjustment → item-fulfillment linking (direct FK columns, REST)](features/netsuite-inventory-adjustment-fulfillment-link.md) | 1.0 | For NYCHH stock adjustments, the NetSuite **inventory adjustment** carries a link to the **Item Fulfillment** it corrects, on the multi-select body custom field | 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_hh.php, worker/crons/toga2/netsuite/diagnose_hh_import_inventory_adjustment.php |
|
|
6
|
-
| [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql |
|
|
6
|
+
| [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
|
|
7
7
|
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
8
8
|
| [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
9
9
|
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
@@ -18,6 +18,8 @@ files:
|
|
|
18
18
|
- dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql
|
|
19
19
|
- dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql
|
|
20
20
|
- dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql
|
|
21
|
+
- dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql
|
|
22
|
+
- dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql
|
|
21
23
|
related:
|
|
22
24
|
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
23
25
|
- ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
@@ -160,8 +162,68 @@ that rethrows warnings as a catchable `ErrorException` with exact `file:line`. T
|
|
|
160
162
|
`transferOrderStage` crash after the stale `Logs.Issue` trace misdirected. **When a 1.0 cron freeze's
|
|
161
163
|
`Logs.Issue` trace looks wrong, trust a single-record harness over the aggregated trace.**
|
|
162
164
|
|
|
165
|
+
## Real NetSuite Transfer Order (TrnfrOrd) item receipts — DEFERRED / SKIPPED (2026-08-29)
|
|
166
|
+
|
|
167
|
+
Everything above concerns transfer orders that arrive **as NetSuite sales orders** (the stocking-flag
|
|
168
|
+
routing). A *separate* case is a NetSuite item receipt whose **`createdFrom` is a real
|
|
169
|
+
`TrnfrOrd` record** ("Transfer Order #N"), not a stocking sales order presenting as `'Sales Order'`.
|
|
170
|
+
Importing that path was **attempted this session and INTENTIONALLY DISABLED** — a documented skip in
|
|
171
|
+
`common_sync_togasupply.php`, exactly like the existing Return-Authorization (`RtnAuth`) receipt skip —
|
|
172
|
+
because the feature is **not production-ready**. Do not re-enable it without doing the rebuild below.
|
|
173
|
+
|
|
174
|
+
**Why it was disabled (three blockers):**
|
|
175
|
+
|
|
176
|
+
1. **The shared `syncTransferOrderFromNetsuite` item-sync is NON-IDEMPOTENT.** It appends the TO's items
|
|
177
|
+
every run instead of reconciling — the attempted TO #15 reached **22 duplicate items all `lineNumber`
|
|
178
|
+
1**, and a **pre-existing stocking-SO** transfer order (`Client_Nychh.TransferOrders` id **1** /
|
|
179
|
+
number **62541**) already carries **13 duplicates**. So the duplicate bug is in the **shared** item
|
|
180
|
+
loop and affects **stocking-SO TOs too**, not just the real-TO path (see the shared
|
|
181
|
+
[per-client-sync doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)).
|
|
182
|
+
2. **api2 cannot resolve the receipt's `transferOrderItem` reference — `EV-12` "no searchable
|
|
183
|
+
identifiers."** `TransferOrderItems` is **not** set up to be referenced by `(transferOrder,
|
|
184
|
+
lineNumber)` the way `PurchaseOrderItems` is (which is how a receipt line normally links to its
|
|
185
|
+
order line). This needs api2 **reference metadata** making a `TransferOrderItems` identifier
|
|
186
|
+
searchable.
|
|
187
|
+
3. **Receipt-line ↔ TO-line mapping is wrong.** The receipt line's `orderLine` did not match the TO
|
|
188
|
+
item's `lineNumber`, so even past the EV-12 the line link would be incorrect.
|
|
189
|
+
|
|
190
|
+
**Cleanup done:** deleted the TO #15 duplicate artifact (`TransferOrder` 114 + its items).
|
|
191
|
+
|
|
192
|
+
**Inactive scaffolding left STAGED for the rebuild (present but not wired live):**
|
|
193
|
+
|
|
194
|
+
- `App_Api_Netsuite_Rest::fetchTransferOrderById` — real `TrnfrOrd` shim (`source` = location,
|
|
195
|
+
`dest` = `transferLocation`, `ordPicked`/`ordReceived` flags).
|
|
196
|
+
- `syncTransferOrderFromNetsuite($isRealTransferOrderRecord = true)` — dest resolved from the
|
|
197
|
+
`transferLocation` warehouse; stage derived from the picked/received flags.
|
|
198
|
+
- a location → client lookup in the cron.
|
|
199
|
+
- the guarded `transferOrderItem` link in the receipt-item builder.
|
|
200
|
+
|
|
201
|
+
**ACL grants written + deployed this session (correct, but DORMANT until real-TO receipts are
|
|
202
|
+
re-enabled):** two standard-field write grants cloning a working sibling per the
|
|
203
|
+
[ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md) recipe, using
|
|
204
|
+
**hardcoded `Core.RecordFields` id literals** (the cross-cluster caveat — a `Client_*`→`Core` JOIN is
|
|
205
|
+
refused on the prod client cluster):
|
|
206
|
+
|
|
207
|
+
- `dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql` —
|
|
208
|
+
`ItemReceipts.transferOrderId` (Core RecordField **2153**), cloning `purchaseOrderId` (**85**).
|
|
209
|
+
- `dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql` —
|
|
210
|
+
`ItemReceiptItems.transferOrderItemId` (Core RecordField **2152**), cloning `purchaseOrderItemId`
|
|
211
|
+
(**471**).
|
|
212
|
+
|
|
213
|
+
**REBUILD SPEC (all four, in order, before re-enabling + removing the skip):**
|
|
214
|
+
1. Make the shared `syncTransferOrderFromNetsuite` item-sync **idempotent** (reconcile against existing
|
|
215
|
+
`TransferOrderItems` by a stable key) — this also fixes the stocking-SO dups and must **clean the
|
|
216
|
+
pre-existing TO 1** (13 dups).
|
|
217
|
+
2. Add api2 **`TransferOrderItems` reference metadata** so a receipt line can resolve its TO item by a
|
|
218
|
+
searchable identifier (the analogue of the `PurchaseOrderItems` `(purchaseOrder, lineNumber)` setup).
|
|
219
|
+
3. **Verify the receipt-line ↔ TO-line mapping** (the `orderLine` ↔ TO-item `lineNumber` correspondence).
|
|
220
|
+
4. Re-enable the path, confirm the (now non-dormant) ACL grants take effect, and **remove the skip**.
|
|
221
|
+
|
|
163
222
|
## Gotchas / known issues
|
|
164
223
|
|
|
224
|
+
- **Real-TrnfrOrd item receipts are disabled — see the DEFERRED section above.** A receipt whose
|
|
225
|
+
`createdFrom` is a real Transfer Order is skipped by design; do not read that as the importer being
|
|
226
|
+
broken.
|
|
165
227
|
- **Order 128829 (no location anywhere) is unresolved** — see the origin-resolution section. Do not
|
|
166
228
|
assume the importer is broken if this single order is stuck; every located order imports.
|
|
167
229
|
- **Routing is gated by the wrapper constant.** If a future client should route transfer orders,
|
|
@@ -174,6 +236,18 @@ that rethrows warnings as a catchable `ErrorException` with exact `file:line`. T
|
|
|
174
236
|
|
|
175
237
|
## Change history
|
|
176
238
|
|
|
239
|
+
- 2026-08-29 — Recorded the **real NetSuite `TrnfrOrd` item-receipt feature as DEFERRED/SKIPPED**
|
|
240
|
+
(documented skip in `common_sync_togasupply.php`, like the `RtnAuth` skip) after attempting it and
|
|
241
|
+
hitting three blockers: the shared `syncTransferOrderFromNetsuite` item-sync is **non-idempotent**
|
|
242
|
+
(TO #15 got 22 dup items; pre-existing stocking-SO TO 1/62541 has 13 — shared-loop bug, affects
|
|
243
|
+
stocking-SO TOs too), api2 can't resolve the receipt's `transferOrderItem` reference (`EV-12`, no
|
|
244
|
+
searchable identifier on `TransferOrderItems`), and the receipt-line `orderLine` ≠ TO-item `lineNumber`.
|
|
245
|
+
Deleted the TO #15 dup artifact (TransferOrder 114). Left inactive scaffolding staged
|
|
246
|
+
(`fetchTransferOrderById`, `syncTransferOrderFromNetsuite($isRealTransferOrderRecord=true)`,
|
|
247
|
+
location→client lookup, guarded `transferOrderItem` link). Wrote + deployed (but **dormant**) two ACL
|
|
248
|
+
field-permission grants using hardcoded Core RecordFields literals: `ItemReceipts.transferOrderId`
|
|
249
|
+
(RF 2153, clone `purchaseOrderId` 85) and `ItemReceiptItems.transferOrderItemId` (RF 2152, clone
|
|
250
|
+
`purchaseOrderItemId` 471). Documented the four-step rebuild spec. (jcardinal)
|
|
177
251
|
- 2026-08-28 — Unfroze the `SALES_ORDERS` section (NYCHH + GroWrk) by peeling **three** stacked faults
|
|
178
252
|
on `syncTransferOrderFromNetsuite`: (1) `Undefined property $transferOrderStage` — api2 **omits a
|
|
179
253
|
null FK object** from a depth-2 GET, guarded `transferOrderStage`/`originLocation`/`destinationLocation`
|
|
@@ -40,6 +40,12 @@ This is a team directive. It applies to every session, both frameworks, every re
|
|
|
40
40
|
write, or spawn agents.
|
|
41
41
|
5. **Ordering is unchanged.** The team KB stays the **first** source, ahead of all other
|
|
42
42
|
research — that part was already correct and stays.
|
|
43
|
+
6. **Search the KB before reverse-engineering — mid-session, not just at kickoff.** Before
|
|
44
|
+
reverse-engineering any code, schema, or framework mechanism from the source or the live DB,
|
|
45
|
+
FIRST search the team KB (`node "<TEAM_REPO>/knowledge.js" search --q=…`). The KB is large and
|
|
46
|
+
usually already documents the mechanism — reaching for DB/code archaeology first is the mistake.
|
|
47
|
+
This extends rule 5 (KB is the first source) from kickoff to every point in a session where you
|
|
48
|
+
are about to derive how something works.
|
|
43
49
|
|
|
44
50
|
## How it is enforced
|
|
45
51
|
|
|
@@ -68,6 +74,12 @@ than the old subagent approach did. Mitigate by not re-reading absorbed docs and
|
|
|
68
74
|
only the needed section of a large spec — but priming in the main session is the requirement.
|
|
69
75
|
|
|
70
76
|
## Change history
|
|
77
|
+
- 2026-08-29 — Added rule 6: search the KB before reverse-engineering any code/schema/framework
|
|
78
|
+
mechanism, mid-session and not only at kickoff. Prompted by a NetSuite→TOGa Supply session that
|
|
79
|
+
reverse-engineered the entire 2.0 field-ACL model (AclFieldPermissions, Core.RecordFields,
|
|
80
|
+
EV-9/EV-12, the add-a-writable-field recipe, the cross-cluster hardcoded-literal caveat) from
|
|
81
|
+
DB/code before discovering it was fully documented in
|
|
82
|
+
`2.0/apps/_underscore/features/acl-permission-chain.md` all along. (jcardinal)
|
|
71
83
|
- 2026-08-13 — Standard recorded. Kickoff priming must run in the main session, synchronously,
|
|
72
84
|
and no subagent may start until priming is complete; released by the new
|
|
73
85
|
`knowledge.js kickoff-primed` sentinel. Retired the `context-primer` subagent from the
|
package/package.json
CHANGED