toga-ai 1.0.699 → 1.0.700
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 +22 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +55 -2
- package/knowledge/INDEX.md +1 -1
- 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,20 @@ 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
|
+
|
|
125
139
|
- **A shim must produce the SOAP object's SHAPE, not just its scalar fields — an absent nested list
|
|
126
140
|
crashes 1.0 (2026-08-29).** `fetchCustomerById()`'s `addressbookList` was where the AIG lookup phase
|
|
127
141
|
crashed: a REST customer with no/odd address made the shim omit the nested `addressbook` structure the
|
|
@@ -215,6 +229,14 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
215
229
|
|
|
216
230
|
## Change history
|
|
217
231
|
|
|
232
|
+
- 2026-08-29 — Recorded `fetchPurchaseOrderById()`'s **missing `entity` SELECT** (deploy/verify
|
|
233
|
+
session): its header query omitted `entity`/`BUILTIN.DF(entity)` and the shim never set
|
|
234
|
+
`$shim->entity` (unlike `listPurchaseOrders`), so the PO vendor was always null and the
|
|
235
|
+
`syncPurchaseOrderFromNetsuite` fail-loud missing-entity guard threw every run — the actual cause of
|
|
236
|
+
the **Canon + Endeavor Health ITEM_RECEIPTS** freeze at `1-RUNNING` (watermarks ~8/~5 days). Fixed
|
|
237
|
+
additively (add `entity, BUILTIN.DF(entity) AS entity_name`; populate `$shim->entity` as
|
|
238
|
+
`{internalId,name}`); same "a field the shim consumes must be in the SELECT" class as `tl.location`.
|
|
239
|
+
Deployed ~12:52; both sections self-healed RUNNING→IDLE and resumed advancing. (jcardinal)
|
|
218
240
|
- 2026-08-29 — Recorded two shim-shape facts from root-causing the 5 stuck togasupply integrations
|
|
219
241
|
(which were **shim-shape mismatches, not dead SOAP** — see the
|
|
220
242
|
[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,59 @@ 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
|
+
|
|
604
643
|
## Change history
|
|
605
644
|
|
|
645
|
+
- 2026-08-29 — **Deploy/verify/stabilize session for the SOAP→REST conversion.** Root-caused the
|
|
646
|
+
**Canon + Endeavor Health ITEM_RECEIPTS** freeze: `fetchPurchaseOrderById` never populated the PO
|
|
647
|
+
vendor (`entity` missing from its SuiteQL SELECT), so `syncPurchaseOrderFromNetsuite`'s fail-loud
|
|
648
|
+
missing-entity guard threw every run — fixed in the shim (add `entity`/`BUILTIN.DF(entity)`, build
|
|
649
|
+
`$shim->entity`); deployed ~12:52, both sections self-healed. Migrated **invoice-PDF retrieval to a
|
|
650
|
+
RESTlet** (`/record/v1/file/{id}` 404s — no token-only REST path to File Cabinet binary exists), which
|
|
651
|
+
unblocked Endeavor's whole pipeline; new [File Cabinet content via
|
|
652
|
+
RESTlet](../../library/features/netsuite-filecabinet-restlet.md) doc. Recorded the **`1-RUNNING`
|
|
653
|
+
(window collapsed to 1s) = stuck** vs large-window `*-RUNNING` (healthy/mid-run or legit backfill)
|
|
654
|
+
diagnostic, the cross-schema `Parameters` sweep needing `CONVERT(... USING utf8mb4)`, and that
|
|
655
|
+
`Logs.Event` (not `Logs.Issue`) carries `clientId`. Logged two OPEN, unfixed items: **AIG** (clientId
|
|
656
|
+
32) LOOKUPS crash at `:463` (`Undefined property $companyName`, customer-shim gap that survived the
|
|
657
|
+
addressbook fix) and **Adyen** (clientId 48) setup crash at `:99` (stuck-IDLE, likely improperly
|
|
658
|
+
onboarded). (jcardinal)
|
|
606
659
|
- 2026-08-29 — **Root-caused the 5 stuck togasupply integrations as REST-shim SHAPE mismatches, not
|
|
607
660
|
dead SOAP, and fixed them.** SOAP was up; the earlier SOAP→REST conversion left shims returning
|
|
608
661
|
null/absent where SOAP had values, and a 1.0 unguarded read of that null → PHP warning →
|
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)
|
package/package.json
CHANGED