toga-ai 1.0.698 → 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 +51 -0
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +57 -1
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +10 -1
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +142 -1
- package/knowledge/1.0/standards/backend-php.md +61 -4
- package/knowledge/1.0/standards/framework-rules.md +11 -1
- package/knowledge/2.0/standards/backend-php.md +15 -1
- package/knowledge/2.0/standards/framework-rules.md +28 -1
- package/knowledge/INDEX.md +4 -1
- package/knowledge/clients/aig/profile.md +3 -2
- package/knowledge/clients/canon/INDEX.md +5 -0
- package/knowledge/clients/canon/profile.md +34 -0
- package/knowledge/clients/endeavor-health/INDEX.md +5 -0
- package/knowledge/clients/endeavor-health/profile.md +32 -0
- package/knowledge/clients/spglobal/INDEX.md +5 -0
- package/knowledge/clients/spglobal/profile.md +32 -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
|
|
|
@@ -349,6 +356,35 @@ Used for fields SuiteQL doesn't expose on `transaction`: `shippingCost`, `taxTot
|
|
|
349
356
|
when `fetchItemFulfillmentById` returned zero lines — see
|
|
350
357
|
[NYCHH inventory-adjustment → item-fulfillment linking](../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md).
|
|
351
358
|
|
|
359
|
+
### `fetch*ById()` single-record helpers — the REST replacements for the SOAP `App_NetSuite::get*`
|
|
360
|
+
|
|
361
|
+
The togasupply importer fetches individual records by internal id through a family of `fetch*ById()`
|
|
362
|
+
helpers on `App_Api_Netsuite_Rest`, each assembling a SOAP-shaped shim from a SuiteQL header/lines
|
|
363
|
+
query plus (where needed) a REST record GET. As of **2026-08-29 the togasupply sync path uses these
|
|
364
|
+
exclusively — zero `App_NetSuite::` SOAP network calls remain** in `common_sync_togasupply.php` or the
|
|
365
|
+
`*FromNetsuite()` methods:
|
|
366
|
+
|
|
367
|
+
- **`fetchItemReceiptById`** — header + lines + serials (serials via SuiteQL
|
|
368
|
+
`inventoryassignment ⋈ inventorynumber`, grouped per product). Replaces SOAP
|
|
369
|
+
`getItemReceipt`/`getInventoryNumber*`/`getItemIsSerialized`.
|
|
370
|
+
- **`fetchPurchaseOrderById`** / **`fetchSalesOrderByIdFull`** — replace SOAP `getPurchaseOrder` /
|
|
371
|
+
`getSalesOrder`.
|
|
372
|
+
- **`fetchItemFlagsByIds`** — batch item serialized/fulfillable flags; replaces
|
|
373
|
+
`getItemSerializedAndFulfillable`.
|
|
374
|
+
- **`fetchInventoryAdjustmentById`** — replaces the `diagnose_hh` script's SOAP `getObject`.
|
|
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).
|
|
382
|
+
|
|
383
|
+
`listCustomers()` also gained a **`companyName`** field on its shim (the `netsuite_customers.php` cron
|
|
384
|
+
moved from `listAllCustomers` → `listCustomers`). REST-shim *shape* traps that bite these helpers (an
|
|
385
|
+
absent nested list crashes 1.0; `scriptId` case-sensitivity on `customFieldList`) are in
|
|
386
|
+
[`netsuite-suiteql-rest-shim.md`](netsuite-suiteql-rest-shim.md).
|
|
387
|
+
|
|
352
388
|
## `list*()` helpers — behavior & cost
|
|
353
389
|
|
|
354
390
|
`listSales()`, `listSalesOrders()`, `listOpportunities()` all:
|
|
@@ -418,6 +454,21 @@ numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
|
418
454
|
|
|
419
455
|
## Change history
|
|
420
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)
|
|
465
|
+
- 2026-08-29 — Documented the **`fetch*ById()` single-record helper family** (`fetchItemReceiptById`,
|
|
466
|
+
`fetchPurchaseOrderById`, `fetchSalesOrderByIdFull`, `fetchItemFlagsByIds`,
|
|
467
|
+
`fetchInventoryAdjustmentById`, `fetchInvoiceFile`) that replaced the SOAP `App_NetSuite::get*` calls,
|
|
468
|
+
and the `listCustomers` `companyName` addition — the togasupply sync path now makes **zero
|
|
469
|
+
`App_NetSuite::` SOAP network calls**. Added while root-causing the 5 stuck togasupply integrations as
|
|
470
|
+
REST-shim shape mismatches (see the
|
|
471
|
+
[per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md)). (jcardinal)
|
|
421
472
|
- 2026-08-29 — Recorded two NetSuite REST/SuiteQL facts from the NYCHH inventory-adjustment →
|
|
422
473
|
item-fulfillment **REST migration**: **REST record sublist endpoints
|
|
423
474
|
(`/record/v1/{type}/{id}/{sublist}`) reject `limit`/`offset` with a 400 `INVALID_PARAMETER`** (a
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-29
|
|
10
10
|
owners: [dfranks, jcardinal, kyalamarthi]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
@@ -112,6 +112,43 @@ Forecast2 tables already store.
|
|
|
112
112
|
TransferOrders). A custom field absent from the record reads as null downstream. See
|
|
113
113
|
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
114
114
|
|
|
115
|
+
- **⚠ `scriptId` in a `customFieldList` def is CASE-SENSITIVE on lookup — a case skew silently
|
|
116
|
+
returns the default (2026-08-29).** `custbody_stocking_order` was **tagged** in the shim with the
|
|
117
|
+
uppercase scriptId `CUSTBODY_STOCKING_ORDER` but **read** by the importer as lowercase
|
|
118
|
+
`custbody_stocking_order`, so the lookup never matched and always fell back to the default (unchecked)
|
|
119
|
+
— which mis-routed **every** NYCHH order to a Transfer Order. Fixed by lowercasing the scriptId in
|
|
120
|
+
**both** `buildSalesOrderShims()` and `fetchSalesOrderById()` defs; `END_CUSTOMER` is intentionally
|
|
121
|
+
left uppercase because it is resolved by internalId `3149`, not by scriptId. Nothing errors on a case
|
|
122
|
+
skew — the field just reads as its default — so verify the exact scriptId string on both the write and
|
|
123
|
+
the read side. See [NYCHH TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
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
|
+
|
|
139
|
+
- **A shim must produce the SOAP object's SHAPE, not just its scalar fields — an absent nested list
|
|
140
|
+
crashes 1.0 (2026-08-29).** `fetchCustomerById()`'s `addressbookList` was where the AIG lookup phase
|
|
141
|
+
crashed: a REST customer with no/odd address made the shim omit the nested `addressbook` structure the
|
|
142
|
+
old SOAP object always carried, and the downstream unguarded read raised a warning → `App_Error`
|
|
143
|
+
`exit()` (see the `getCustomFieldValue` warning-safety gotcha below — same class). It now builds
|
|
144
|
+
`addressbookList` via a `shimAddress()` helper that normalizes `country` to the `_unitedStates` enum
|
|
145
|
+
and returns an **empty `addressbook: []`** when there is no address, so the shape always matches;
|
|
146
|
+
two helpers were extracted for reuse — `countryToSoapEnum()` (country string → SOAP enum) and
|
|
147
|
+
`refString()`. This is the concrete "REST shim returns null/absent where SOAP had a value" failure
|
|
148
|
+
the [per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md) diagnoses via
|
|
149
|
+
`Logs.Issue`/`Logs.Event`. **When adding or editing a shim, reproduce the full SOAP shape (including
|
|
150
|
+
empty nested lists), not just the fields you happen to need.**
|
|
151
|
+
|
|
115
152
|
## Transaction status sourcing (1.0) — and the 2026.2 REST `status.id` change
|
|
116
153
|
|
|
117
154
|
1.0 reaches NetSuite over **two transports, and neither reads status off a REST record GET
|
|
@@ -192,6 +229,25 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
192
229
|
|
|
193
230
|
## Change history
|
|
194
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)
|
|
240
|
+
- 2026-08-29 — Recorded two shim-shape facts from root-causing the 5 stuck togasupply integrations
|
|
241
|
+
(which were **shim-shape mismatches, not dead SOAP** — see the
|
|
242
|
+
[per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md)): a
|
|
243
|
+
`customFieldList` **`scriptId` is case-sensitive on lookup** — `CUSTBODY_STOCKING_ORDER` (write) vs
|
|
244
|
+
`custbody_stocking_order` (read) silently returned the default and mis-routed every NYCHH order to a
|
|
245
|
+
Transfer Order (fixed by lowercasing in both `buildSalesOrderShims`/`fetchSalesOrderById`;
|
|
246
|
+
`END_CUSTOMER` stays uppercase, resolved by internalId 3149); and a shim must reproduce the SOAP
|
|
247
|
+
object's **full shape** — `fetchCustomerById()`'s `addressbookList` omitting the nested `addressbook`
|
|
248
|
+
crashed the AIG lookup phase (unguarded null read → warning → `exit()`), now built via `shimAddress()`
|
|
249
|
+
(country → `_unitedStates` enum, empty → `addressbook: []`) with extracted `countryToSoapEnum()` /
|
|
250
|
+
`refString()` helpers. (jcardinal)
|
|
195
251
|
- 2026-08-27 — **Traced the missing `tl.location` SELECT to its cause — a merge, not an omission — and
|
|
196
252
|
recorded what restoring it re-animates** (forecast2 open-orders, TRUE-79162). TRUE-79078 (31fadd77)
|
|
197
253
|
originally added `tl.location` + `BUILTIN.DF(tl.location)`; merge c3caa11a (2026-06-24) resolved in
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-29
|
|
10
10
|
owners: [jcardinal, mhammontree, bala]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -396,6 +396,15 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
396
396
|
|
|
397
397
|
## Change history
|
|
398
398
|
|
|
399
|
+
- 2026-08-29 — The `*FromNetsuite()` importer methods in this class were **completed onto REST** — the
|
|
400
|
+
item-receipt and inventory-adjustment paths dropped their last `App_NetSuite::` SOAP calls in favor of
|
|
401
|
+
`App_Api_Netsuite_Rest::fetch*ById()`, so the togasupply importer now makes **zero SOAP network
|
|
402
|
+
calls**. Also **corrects the 2026-08-27 entry below**: the private `isStockingOrder()` custom-field
|
|
403
|
+
read had a `scriptId` case-mismatch (`CUSTBODY_STOCKING_ORDER` vs `custbody_stocking_order`) that
|
|
404
|
+
silently defaulted and mis-routed every NYCHH order — now fixed. Method-level detail and the full
|
|
405
|
+
per-client fix set live on
|
|
406
|
+
[NetSuite → TOGa Supply per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)
|
|
407
|
+
(2026-08-29). (jcardinal)
|
|
399
408
|
- 2026-08-27 — `send()`'s throw sites were **reverted to plain `throw new Exception(...)`** (both
|
|
400
409
|
sites) as part of removing the sync engine's swallow-and-skip data-loss bug — the importer must
|
|
401
410
|
propagate a failed write, not catch it (see
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-29
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -531,8 +531,149 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
531
531
|
direct-FK-column change) on
|
|
532
532
|
[NYCHH inventory-adjustment → item-fulfillment linking](../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md).
|
|
533
533
|
|
|
534
|
+
- **⚠ 2026-08-29: the 5 stuck togasupply integrations were REST-shim SHAPE mismatches, NOT dead
|
|
535
|
+
SOAP — and how to tell them apart.** Five clients had a section frozen at `RUNNING` and the first
|
|
536
|
+
hypothesis was "SOAP is down." **SOAP was up.** The real cause was introduced by the earlier
|
|
537
|
+
SOAP→REST list-method conversion: a REST shim returns `null`/absent where the old SOAP object
|
|
538
|
+
carried a value, and in 1.0 **any unguarded read of that null raises a PHP warning that
|
|
539
|
+
`App_Error` escalates to `exit()`** (see the 1.0 backend-php standard *Any PHP warning terminates
|
|
540
|
+
the request*, and the [rest-shim warning-safety gotcha](../../library/features/netsuite-suiteql-rest-shim.md)).
|
|
541
|
+
The `exit()` mid-section leaves that record-type's `NETSUITE_EXECUTION_MODE_*` stuck at `RUNNING`
|
|
542
|
+
with the watermark frozen — i.e. **a shim-shape mismatch presents identically to a poison order.**
|
|
543
|
+
So a stuck section is never "SOAP is dead"; it is one specific record hitting one specific
|
|
544
|
+
unguarded shim field.
|
|
545
|
+
**Diagnosis playbook (durable):** query prod **`Logs.Issue` + `Logs.Event`** (the
|
|
546
|
+
`App_Error_Capture` tables, schema `Logs`, via the toga-db MCP) for the client's `clientId` — each
|
|
547
|
+
stuck section maps to an exact crash **file:line** in `common_sync_togasupply.php` or `toga2.php`.
|
|
548
|
+
The `RUNNING` mode + frozen watermark **is** the stuck signal (see the window/watermark gotchas
|
|
549
|
+
above); `Logs.Issue`/`Event` turns it into a line number, far faster than reading the cron blind.
|
|
550
|
+
**Per-client fixes shipped (all in `library`, `php -l` clean, and fail-loud — a genuinely
|
|
551
|
+
unresolvable case now throws rather than silently continuing):**
|
|
552
|
+
- **Item receipts — Canon + Endeavor Health** (`syncItemReceiptFromNetsuite`): migrated off SOAP to
|
|
553
|
+
new REST `fetchItemReceiptById` (header + lines + serials via SuiteQL
|
|
554
|
+
`inventoryassignment ⋈ inventorynumber`, serials grouped per product) + `fetchPurchaseOrderById`
|
|
555
|
+
+ `fetchItemFlagsByIds`; removed all `App_NetSuite::` SOAP calls (`getItemReceipt` /
|
|
556
|
+
`getPurchaseOrder` / `getItemIsSerialized` / `getInventoryNumber*`). Guards added;
|
|
557
|
+
**`RtnAuth` (return-authorization) item receipts are intentionally NOT imported** (explicit
|
|
558
|
+
documented skip), and an **unknown `createdFrom` type throws**.
|
|
559
|
+
- **Purchase orders** (`syncPurchaseOrderFromNetsuite`): a missing PO entity now **throws** (was an
|
|
560
|
+
unguarded crash); `getSalesOrder` → REST `fetchSalesOrderByIdFull`; a `2400` ACL-strip that used
|
|
561
|
+
to skip now **throws**. Threaded a new `array &$lookupWarehouseByNetsuiteLocationInternalId` param
|
|
562
|
+
from **both** callers — which also fixed a **null `arg13` `TypeError`** into
|
|
563
|
+
`syncSalesOrderFromNetsuite`.
|
|
564
|
+
- **Transfer orders — GroWrk `SALES_ORDERS`** (`syncTransferOrderFromNetsuite`): fixed an
|
|
565
|
+
all-catalog lookup that read `->uuid` off the array instead of `[0]`; an **unmapped NetSuite TO
|
|
566
|
+
status now throws** (was default-to-pending + log).
|
|
567
|
+
- **Item fulfillments — Elite (also Quad / S&P Global / Prudential-EDI)**
|
|
568
|
+
(`syncItemFulfillmentFromNetsuite`): the "Undefined index" on service/shipping lines with an
|
|
569
|
+
**empty `orderLine`** (`$lookupNsSalesOrderItemsByOrderLine['']`) is now guarded so it falls
|
|
570
|
+
through to the existing reconstruct path.
|
|
571
|
+
- **NYCHH stocking-order routing** — the `isStockingOrder` scriptId **case-mismatch** (full detail
|
|
572
|
+
in the [rest-shim doc](../../library/features/netsuite-suiteql-rest-shim.md)): the custom field was
|
|
573
|
+
tagged `CUSTBODY_STOCKING_ORDER` but looked up lowercase → always returned the default →
|
|
574
|
+
**every NYCHH order was mis-routed to a Transfer Order.** Fixed by lowercasing the scriptId in
|
|
575
|
+
both shim defs.
|
|
576
|
+
- **AIG lookup-phase crash** — `fetchCustomerById`'s `addressbookList` is now built via `shimAddress`
|
|
577
|
+
(country normalized to the `_unitedStates` enum, empty → `addressbook: []`); extracted
|
|
578
|
+
`countryToSoapEnum()` + `refString()` helpers, plus a belt-and-suspenders guard in
|
|
579
|
+
`common_sync_togasupply.php`.
|
|
580
|
+
|
|
581
|
+
- **SOAP→REST is now COMPLETE for the togasupply sync path (2026-08-29).** With the item-receipt and
|
|
582
|
+
(2026-08-29) inventory-adjustment migrations plus the new `fetch*ById` methods, **zero
|
|
583
|
+
`App_NetSuite::` (SOAP) network calls remain** anywhere in the `common_sync_togasupply.php` engine
|
|
584
|
+
or the `*FromNetsuite()` methods it calls — so the whole tier-wide-freeze class above is now driven
|
|
585
|
+
by REST-shim shape, not SOAP availability. New REST completeness pieces built in `library`:
|
|
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`,
|
|
590
|
+
the `diagnose_hh` script's `getObject` → `fetchInventoryAdjustmentById`, and
|
|
591
|
+
`worker/crons/sync/netsuite/netsuite_customers.php` `listAllCustomers` → `listCustomers` (with
|
|
592
|
+
`companyName` added to the `listCustomers` shim).
|
|
593
|
+
|
|
594
|
+
- **⚠ SALES_ORDERS line renumber is now a validate-before-mutate TWO-PHASE UPDATE (2026-08-29) —
|
|
595
|
+
supersedes the delete-recreate + pinned-skip approach.** `syncSalesOrderFromNetsuite`'s renumber no
|
|
596
|
+
longer DELETEs and re-creates lines (nor keeps un-deletable/invoiced rows at their old number — see
|
|
597
|
+
the 2026-08-17 pinned-skip gotcha above, now historical). It **parks** every drifting line at a
|
|
598
|
+
temporary `lineNumber` (base = `max(current, target lineNumbers) + headroom`, headroom const
|
|
599
|
+
**1000**), then **finalizes** each to its target — so `UNIQUE(salesOrderId, lineNumber)` is never
|
|
600
|
+
transiently violated and pinned FK-RESTRICT children (invoiced/fulfilled lines) renumber accurately
|
|
601
|
+
via `UPDATE` instead of being stranded at a stale number. It **throws BEFORE any PUT** if a kept or
|
|
602
|
+
undeletable row occupies a needed target, so a failure never leaves the order in a partial state.
|
|
603
|
+
Note `SalesOrderItems.lineNumber` is a **`smallint`**, so the parked base must stay within smallint
|
|
604
|
+
range for the client's line counts (headroom 1000 is safe at observed volumes).
|
|
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
|
+
|
|
534
643
|
## Change history
|
|
535
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)
|
|
659
|
+
- 2026-08-29 — **Root-caused the 5 stuck togasupply integrations as REST-shim SHAPE mismatches, not
|
|
660
|
+
dead SOAP, and fixed them.** SOAP was up; the earlier SOAP→REST conversion left shims returning
|
|
661
|
+
null/absent where SOAP had values, and a 1.0 unguarded read of that null → PHP warning →
|
|
662
|
+
`App_Error` `exit()` → the section frozen at `RUNNING`/watermark stuck (presents identically to a
|
|
663
|
+
poison order). Recorded the durable diagnosis playbook (map each stuck `clientId`/section to a crash
|
|
664
|
+
file:line via prod `Logs.Issue`/`Logs.Event`, the `App_Error_Capture` tables). Shipped per-client
|
|
665
|
+
`library` fixes, all fail-loud: item-receipts SOAP→REST (`fetchItemReceiptById` +
|
|
666
|
+
`fetchPurchaseOrderById` + `fetchItemFlagsByIds`, RtnAuth skip, unknown `createdFrom` throws — Canon
|
|
667
|
+
+ Endeavor Health); PO missing-entity throw + `arg13` warehouse-lookup TypeError fix +
|
|
668
|
+
`fetchSalesOrderByIdFull` + 2400-ACL throw; TransferOrder all-catalog `[0]`/`->uuid` fix + unmapped
|
|
669
|
+
status throw (GroWrk); IF empty-`orderLine` "Undefined index" guard (Elite/Quad/S&P/Prudential-EDI);
|
|
670
|
+
the `isStockingOrder` scriptId case-mismatch that mis-routed **every** NYCHH order to a Transfer
|
|
671
|
+
Order; and the AIG `fetchCustomerById` address-shim crash. Also **completed SOAP→REST for the whole
|
|
672
|
+
togasupply path** (zero `App_NetSuite::` network calls remain; new `fetchInvoiceFile` /
|
|
673
|
+
`fetchItemFlagsByIds` / `fetchInventoryAdjustmentById`, `listAllCustomers`→`listCustomers`+companyName),
|
|
674
|
+
and replaced the SALES_ORDERS renumber with a **validate-before-mutate two-phase UPDATE** (park at
|
|
675
|
+
temp lineNumbers, then finalize; throws before any PUT) superseding the delete-recreate + pinned-skip
|
|
676
|
+
approach. (jcardinal)
|
|
536
677
|
- 2026-08-29 — The inventory-adjustment sync (and the on-demand fulfillment / sales-order import it
|
|
537
678
|
triggers) in `syncInventoryAdjustmentFromNetsuite` was **migrated SOAP → REST/SuiteQL**, closing the
|
|
538
679
|
SOAP round-trip cost flagged 2026-08-28; the remaining error-log-and-swallow `try/catch` in the
|
|
@@ -5,13 +5,14 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-29
|
|
9
9
|
owners: [jcardinal, rgirish, mhammontree, ajean]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/library/architecture.md
|
|
13
13
|
- ../apps/library/features/email-queue-attachments.md
|
|
14
14
|
- ../apps/library/features/error-capture-1-0.md
|
|
15
|
+
- ../apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
15
16
|
- ../../2.0/standards/backend-php.md
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -507,9 +508,12 @@ Additional verified detail:
|
|
|
507
508
|
- **Diagnostic fingerprint:** an occurrence count that is an exact multiple of the job's
|
|
508
509
|
scheduled frequency (e.g. 20/hour for `*/3 * * * *`) means **one** poisoned item retried,
|
|
509
510
|
not many distinct failures.
|
|
510
|
-
- **In job code, prevention beats containment.** Guard optional data with
|
|
511
|
-
reading it
|
|
512
|
-
|
|
511
|
+
- **In job code, prevention beats containment for a warning.** Guard optional data with
|
|
512
|
+
`isset()` before reading it — you cannot wrap your way out of a notice. This is about the
|
|
513
|
+
uncatchable-warning mechanics above; it is **not** license to ack-and-continue past a genuine
|
|
514
|
+
(catchable) exception in a reconciliation loop. See *Fail-loud is the default for
|
|
515
|
+
import/sync/cron code* below before reaching for a `finally`/`catch (Throwable)` ack — that
|
|
516
|
+
pattern caused a real data-loss incident in this exact engine.
|
|
513
517
|
|
|
514
518
|
> **Existing guidance under review.** An earlier version of this section recommended
|
|
515
519
|
> wrapping fragile I/O in `try { ... } catch (\ErrorException $e)` relying on the GLOBAL
|
|
@@ -521,6 +525,59 @@ Additional verified detail:
|
|
|
521
525
|
> (~L112-123), `clients/prudential/profile.md` (~L75). Prefer the local-handler pattern
|
|
522
526
|
> from the canonical section above for anything new.
|
|
523
527
|
|
|
528
|
+
### Fail-loud is the default for import/sync/cron code — never catch-and-continue or log-and-skip a record
|
|
529
|
+
|
|
530
|
+
**This is the standing policy for reconciliation jobs; it supersedes the "consume/ack in a
|
|
531
|
+
`finally`" framing in the section above wherever the two would conflict.** For import/sync/cron
|
|
532
|
+
code that reconciles data from an external system into ours — the NetSuite → TOGa Supply sync
|
|
533
|
+
(`worker/crons/toga2/netsuite/`) is the canonical example — the rule is: **if anything prevents
|
|
534
|
+
the job from producing accurate data, let it throw.** The exception must propagate and abort the
|
|
535
|
+
run. Do not:
|
|
536
|
+
|
|
537
|
+
- Wrap a per-record dispatch in `try { … } catch (Throwable) { error_log(...) }` and move on to
|
|
538
|
+
the next record.
|
|
539
|
+
- Wrap a section/job in `try { … } finally { <advance the checkpoint> }` so the checkpoint still
|
|
540
|
+
moves even when the try body threw.
|
|
541
|
+
- Downgrade a hard failure to a log line and keep going "because most records succeeded."
|
|
542
|
+
|
|
543
|
+
**Why.** These jobs track progress with an execution-mode flag plus a watermark/cursor (e.g.
|
|
544
|
+
`NETSUITE_EXECUTION_MODE_*` / `NETSUITE_LAST_SYNC_DATETIME_*`). A thrown, unhandled exception
|
|
545
|
+
leaves that state at **RUNNING** with the watermark **not advanced** past the failure — visible
|
|
546
|
+
proof the run failed, the record still owes a retry, and a developer needs to look. Catching the
|
|
547
|
+
exception and continuing does the opposite: it drops the record, silently advances the checkpoint
|
|
548
|
+
past it (the loss becomes **permanent**, not retried), and can even reset the mode back to
|
|
549
|
+
**IDLE**, telling the next run "everything's fine" when it isn't. **Data accuracy is prioritized
|
|
550
|
+
over availability here — even for a failure that is not auto-retriable** (an ACL/permission
|
|
551
|
+
error, a malformed upstream record, a constraint violation). A run stuck at RUNNING that needs a
|
|
552
|
+
developer to reconcile it is the correct, intended outcome; a clean-looking IDLE run that quietly
|
|
553
|
+
dropped data is not.
|
|
554
|
+
|
|
555
|
+
**The one legitimate exception is a true message/inbox queue** — a loop reading items one at a
|
|
556
|
+
time from something like an IMAP inbox or an SQS queue, where an item that is never acknowledged
|
|
557
|
+
blocks every item behind it forever. There, and only there, consuming/acking the poison item so
|
|
558
|
+
the queue can move past it is defensible — and even then, **prefer surfacing** the failure
|
|
559
|
+
(dead-letter it, alert on it, record it against the item) over silently discarding it. A
|
|
560
|
+
reconciliation cron with its own per-client cursor is **not** this case: nothing blocks behind a
|
|
561
|
+
RUNNING watermark except that one client's own next run, which is exactly what the window
|
|
562
|
+
grow/shrink behavior is designed around.
|
|
563
|
+
|
|
564
|
+
**Real incident.** On 2026-08-20 a change wrapped every per-order dispatch across all six
|
|
565
|
+
NetSuite → TOGa Supply sections in `catch (Throwable) { error_log }` and wrapped each section in
|
|
566
|
+
`try { … } finally { finishModeIteration }`. It looked like an improvement — "don't let one bad
|
|
567
|
+
order jam the sync" — but it silently dropped any record whose write failed, advanced the
|
|
568
|
+
watermark past it, and reset the mode to IDLE, so a failed run looked identical to a successful
|
|
569
|
+
one. This was found and reverted 2026-08-27 (all per-order catches and section-level
|
|
570
|
+
`try/finally` removed — a failure now aborts the section with the mode left RUNNING), and a
|
|
571
|
+
related error-log-and-swallow path in the same engine's inventory-adjustment section was removed
|
|
572
|
+
again 2026-08-29 during its SOAP→REST migration. Full detail: [NetSuite → TOGa Supply Per-Client
|
|
573
|
+
Sync](../apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
574
|
+
|
|
575
|
+
This does not relax the "any PHP warning terminates the request" mechanics above, or the two
|
|
576
|
+
sanctioned local opt-outs for a **notice/warning** you must genuinely contain (a monitoring ping,
|
|
577
|
+
a defensively-guarded optional read). Those exist to survive an *uncatchable warning* without
|
|
578
|
+
crashing something that must not crash. This rule is about a **catchable exception** in a
|
|
579
|
+
reconciliation loop, where surviving it quietly is exactly the wrong behavior.
|
|
580
|
+
|
|
524
581
|
### Test mode silently disables queued email — non-prod cannot validate a queue bug
|
|
525
582
|
|
|
526
583
|
`App_Email::setQueueForSending()` (`library/app/email.php` ~L161) is gated on
|
|
@@ -5,12 +5,13 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-29
|
|
9
9
|
owners: [jcardinal, ajean]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/library/architecture.md
|
|
13
13
|
- ../apps/worker/architecture.md
|
|
14
|
+
- backend-php.md
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
# Framework 1.0 (App_) Rules
|
|
@@ -150,3 +151,12 @@ public function processOrder(): void {
|
|
|
150
151
|
$this->response->success($result);
|
|
151
152
|
}
|
|
152
153
|
```
|
|
154
|
+
|
|
155
|
+
**This pattern is scoped to a single request/response unit of work — it does not extend to
|
|
156
|
+
import/sync/cron code.** Above, catching stops the *whole* action and returns one error response;
|
|
157
|
+
nothing after the catch runs "successfully" on partial data. A batch/reconciliation job (a cron
|
|
158
|
+
looping over many external records, advancing a cursor/watermark) is a different shape: catching
|
|
159
|
+
per-record there and continuing to the next one is exactly the anti-pattern that caused a
|
|
160
|
+
data-loss incident in the NetSuite → TOGa Supply sync. For that class of code, see
|
|
161
|
+
`1.0/standards/backend-php.md` § *Fail-loud is the default for import/sync/cron code* — the job
|
|
162
|
+
must throw and abort, not catch-and-continue.
|
|
@@ -5,13 +5,15 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-29
|
|
9
9
|
owners: [jcardinal, mhammontree, dfranks, bala]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/_underscore/architecture.md
|
|
13
13
|
- ../apps/api2/architecture.md
|
|
14
14
|
- ../apps/worker2/architecture.md
|
|
15
|
+
- framework-rules.md
|
|
16
|
+
- ../../1.0/standards/backend-php.md
|
|
15
17
|
---
|
|
16
18
|
|
|
17
19
|
## General Principals
|
|
@@ -677,6 +679,14 @@ $c = 'Hello ' . $world;
|
|
|
677
679
|
### PHP Error Handling
|
|
678
680
|
|
|
679
681
|
* Always use exceptions to handle unexpected conditions.
|
|
682
|
+
* **Import/sync/cron code that reconciles external data must fail loud — never catch-and-continue
|
|
683
|
+
or log-and-skip a record.** A mid-loop failure must throw and abort the job, leaving its
|
|
684
|
+
checkpoint/watermark unmoved for retry/investigation, rather than being caught, logged, and
|
|
685
|
+
stepped over. See `2.0/standards/framework-rules.md` § *The above is for a per-entity job — a
|
|
686
|
+
reconciliation/import worker that loops over many records is different, and must fail loud* and
|
|
687
|
+
`1.0/standards/backend-php.md` § *Fail-loud is the default for import/sync/cron code* (the
|
|
688
|
+
policy this corrects, after a per-order catch-and-continue change caused a data-loss incident in
|
|
689
|
+
the NetSuite → TOGa Supply sync).
|
|
680
690
|
|
|
681
691
|
### Choosing an exception class is a ROUTING decision, not style
|
|
682
692
|
|
|
@@ -946,6 +956,10 @@ See: 2.0/apps/worker2/features/cross-account-aws-access.md
|
|
|
946
956
|
|
|
947
957
|
## Change history
|
|
948
958
|
|
|
959
|
+
- 2026-08-29 — Added a fail-loud cross-reference under **PHP Error Handling**: import/sync/cron
|
|
960
|
+
code that reconciles external data must never catch-and-continue or log-and-skip a record;
|
|
961
|
+
points to the full policy and the NetSuite → TOGa Supply sync data-loss incident recorded in
|
|
962
|
+
`framework-rules.md` and `1.0/standards/backend-php.md`. (jcardinal)
|
|
949
963
|
- 2026-08-25 — Clarified the exception-routing rule: `_Exception_Validation` is a valid 400 throw
|
|
950
964
|
site from a **scripted-API method** too, not only a pre/post interceptor or model op. Verified at
|
|
951
965
|
the outer `execute()`/`api()` catch in `api2/Controller/Index.php` (~L317) where
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-29
|
|
9
9
|
owners: [jcardinal, ajean, apeterson]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -14,6 +14,8 @@ related:
|
|
|
14
14
|
- ../apps/api2/architecture.md
|
|
15
15
|
- ../apps/dbchanges2/architecture.md
|
|
16
16
|
- ../apps/dbchanges2/features/surface-layer-schema.md
|
|
17
|
+
- backend-php.md
|
|
18
|
+
- ../../1.0/standards/backend-php.md
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
# Framework 2.0 (_underscore) Rules
|
|
@@ -298,6 +300,23 @@ returns nothing (or returns only when it succeeds) leaves a blind spot in the au
|
|
|
298
300
|
only for genuinely retriable conditions, where the queue's retry is the intended behaviour; a
|
|
299
301
|
handled non-retriable failure should return a result describing what happened rather than throwing.
|
|
300
302
|
|
|
303
|
+
### The above is for a per-entity job — a reconciliation/import worker that loops over many records is different, and must fail loud
|
|
304
|
+
|
|
305
|
+
`ProcessOrder(int $orderId)` above is scoped to **one** record: the job either succeeds or fails
|
|
306
|
+
as a whole, and "log and return a failure result" for a non-retriable error means *this job*
|
|
307
|
+
(this one order) is done — no other record is silently dropped alongside it. Do not generalize
|
|
308
|
+
that shape to a worker action that **internally loops over many records to reconcile external
|
|
309
|
+
data** (a NetSuite import, a bulk sync, anything advancing its own cursor/watermark across a
|
|
310
|
+
batch). For that shape, catching a per-record failure inside the loop and moving on to the next
|
|
311
|
+
record is the same anti-pattern documented in `1.0/standards/backend-php.md` § *Fail-loud is the
|
|
312
|
+
default for import/sync/cron code* — it silently drops the record and can leave the job's own
|
|
313
|
+
progress-tracking (checkpoint/cursor) looking healthy when it isn't. A batch/reconciliation worker
|
|
314
|
+
must let a mid-loop failure **throw and abort the job**, leaving its checkpoint unmoved so the
|
|
315
|
+
queue's retry (or a developer) picks up where it actually left off — never catch-per-record and
|
|
316
|
+
continue. Data accuracy outranks job "completion," even when the failure is not retriable (e.g. an
|
|
317
|
+
ACL error on one record) — a developer reconciling a stalled job is the intended outcome, not a
|
|
318
|
+
job that reports success having quietly skipped records.
|
|
319
|
+
|
|
301
320
|
## `c_`-prefixed custom columns are framework-dynamic
|
|
302
321
|
|
|
303
322
|
Columns prefixed `c_` are **custom/dynamic** columns that the `_underscore` model layer handles
|
|
@@ -317,6 +336,14 @@ and not how the convention works. Use a non-`c_` column when you *do* want the f
|
|
|
317
336
|
ACL-governed field treatment.
|
|
318
337
|
|
|
319
338
|
## Change history
|
|
339
|
+
- 2026-08-29 — Added a distinction under **Error handling in workers**: the documented
|
|
340
|
+
`ProcessOrder`-style per-entity pattern (catch non-retriable, log, return a failure result) does
|
|
341
|
+
**not** extend to a worker action that loops over many records to reconcile external data (a
|
|
342
|
+
NetSuite import, a bulk sync). That shape must fail loud — a mid-loop failure throws and aborts
|
|
343
|
+
the job so its checkpoint/cursor stays unmoved — matching the fail-loud policy corrected in
|
|
344
|
+
`1.0/standards/backend-php.md` after a per-order catch-and-continue change caused a silent
|
|
345
|
+
data-loss incident in the NetSuite → TOGa Supply sync (found 2026-08-20, reverted 2026-08-27,
|
|
346
|
+
final swallow removed 2026-08-29). (jcardinal)
|
|
320
347
|
- 2026-08-28 — Extended § Schema migrations with the production-state rules. Recorded that cluster
|
|
321
348
|
isolation is **physical** (verified: the prod Core reader exposes only Core/Forecast/
|
|
322
349
|
Forecast_Archive/Team, the client reader only `Client_*`), so a cross-database reference cannot
|
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)
|
|
@@ -46,9 +46,11 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
46
46
|
## Clients
|
|
47
47
|
|
|
48
48
|
- **AIG (Staples Protection Plan)** (`aig`) → [clients/aig/INDEX.md](clients/aig/INDEX.md)
|
|
49
|
+
- **Canon** (`canon`) → [clients/canon/INDEX.md](clients/canon/INDEX.md)
|
|
49
50
|
- **Compass Canada** (`compass-canada`) → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
|
|
50
51
|
- **Compass USA** (`compass-usa`) → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
|
|
51
52
|
- **Elite** (`elite`) → [clients/elite/INDEX.md](clients/elite/INDEX.md)
|
|
53
|
+
- **Endeavor Health** (`endeavor-health`) → [clients/endeavor-health/INDEX.md](clients/endeavor-health/INDEX.md)
|
|
52
54
|
- **Fordham University** (`fordham`) → [clients/fordham/INDEX.md](clients/fordham/INDEX.md)
|
|
53
55
|
- **GroWrk** (`growrk`) → [clients/growrk/INDEX.md](clients/growrk/INDEX.md)
|
|
54
56
|
- **Northwell Health** (`northwell`) → [clients/northwell/INDEX.md](clients/northwell/INDEX.md)
|
|
@@ -60,6 +62,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
60
62
|
- **Quad Graphics** (`quad`) → [clients/quad/INDEX.md](clients/quad/INDEX.md)
|
|
61
63
|
- **Rate** (`rate`) → [clients/rate/INDEX.md](clients/rate/INDEX.md)
|
|
62
64
|
- **Richmond University Medical Center** (`rumcsi`) → [clients/rumcsi/INDEX.md](clients/rumcsi/INDEX.md)
|
|
65
|
+
- **S&P Global** (`spglobal`) → [clients/spglobal/INDEX.md](clients/spglobal/INDEX.md)
|
|
63
66
|
- **Staples** (`staples`) → [clients/staples/INDEX.md](clients/staples/INDEX.md)
|
|
64
67
|
- **Tow Foundation** (`tow-foundation`) → [clients/tow-foundation/INDEX.md](clients/tow-foundation/INDEX.md)
|
|
65
68
|
- **TOGA Technology** (`true`) → [clients/true/INDEX.md](clients/true/INDEX.md)
|
|
@@ -6,13 +6,14 @@ apps:
|
|
|
6
6
|
- api2
|
|
7
7
|
- dbchanges2
|
|
8
8
|
- library
|
|
9
|
+
- worker
|
|
9
10
|
- worker2
|
|
10
11
|
project: API
|
|
11
12
|
client: aig
|
|
12
13
|
type: profile
|
|
13
14
|
status: active
|
|
14
|
-
updated: 2026-08-
|
|
15
|
-
owners: ["mhammontree"]
|
|
15
|
+
updated: 2026-08-29
|
|
16
|
+
owners: ["mhammontree", "jcardinal"]
|
|
16
17
|
files: []
|
|
17
18
|
related:
|
|
18
19
|
- clients/aig/features/entitlement-intake.md
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Client: Canon `canon`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [Canon](profile.md) | 2.0 | Canon is a **TOGa-Supply NetSuite integration client** on the shared 1.0 NetSuite → TOGa Supply per-client sync (`worker` + `library`). | |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Canon"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
apps:
|
|
5
|
+
- library
|
|
6
|
+
- worker
|
|
7
|
+
project: Worker
|
|
8
|
+
client: canon
|
|
9
|
+
type: profile
|
|
10
|
+
status: active
|
|
11
|
+
updated: 2026-08-29
|
|
12
|
+
owners: ["jcardinal"]
|
|
13
|
+
files: []
|
|
14
|
+
related:
|
|
15
|
+
- ../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Canon is a **TOGa-Supply NetSuite integration client** on the shared 1.0 NetSuite → TOGa Supply
|
|
21
|
+
per-client sync (`worker` + `library`). Its thin wrapper
|
|
22
|
+
`worker/crons/toga2/netsuite/sync_togasupply_canon.php` is the **canonical example** referenced by
|
|
23
|
+
the shared-engine doc — copy it when onboarding a new client. NetSuite transactions (sales orders,
|
|
24
|
+
POs, invoices, item receipts, item fulfillments, inventory adjustments) sync into Canon's 2.0
|
|
25
|
+
`Client_Canon` tenant through the TOGa2 API.
|
|
26
|
+
|
|
27
|
+
This profile was seeded during the 2026-08-29 root-cause work that fixed Canon's stuck
|
|
28
|
+
`ITEM_RECEIPTS` section — the item-receipt path was migrated SOAP → REST (`fetchItemReceiptById`);
|
|
29
|
+
full mechanics and the diagnosis playbook live in
|
|
30
|
+
[NetSuite → TOGa Supply per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
31
|
+
|
|
32
|
+
## Integration touchpoints
|
|
33
|
+
|
|
34
|
+
- **`worker` / `library` (1.0)** — the shared togasupply sync engine + `App_Api_Toga2` bridge.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Client: Endeavor Health `endeavor-health`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [Endeavor Health](profile.md) | 2.0 | Endeavor Health is a **TOGa-Supply NetSuite integration client** on the shared 1.0 NetSuite → TOGa Supply per-client sync (`worker` + `library`). | |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Endeavor Health"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
apps:
|
|
5
|
+
- library
|
|
6
|
+
- worker
|
|
7
|
+
project: Worker
|
|
8
|
+
client: endeavor-health
|
|
9
|
+
type: profile
|
|
10
|
+
status: active
|
|
11
|
+
updated: 2026-08-29
|
|
12
|
+
owners: ["jcardinal"]
|
|
13
|
+
files: []
|
|
14
|
+
related:
|
|
15
|
+
- ../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Endeavor Health is a **TOGa-Supply NetSuite integration client** on the shared 1.0 NetSuite → TOGa
|
|
21
|
+
Supply per-client sync (`worker` + `library`). NetSuite transactions sync into Endeavor's 2.0
|
|
22
|
+
`Client_<Id>` tenant through the TOGa2 API via a thin
|
|
23
|
+
`worker/crons/toga2/netsuite/sync_togasupply_*.php` wrapper.
|
|
24
|
+
|
|
25
|
+
This profile was seeded during the 2026-08-29 root-cause work that fixed Endeavor's stuck
|
|
26
|
+
`ITEM_RECEIPTS` section — the item-receipt path was migrated SOAP → REST (`fetchItemReceiptById`);
|
|
27
|
+
full mechanics and the diagnosis playbook live in
|
|
28
|
+
[NetSuite → TOGa Supply per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
29
|
+
|
|
30
|
+
## Integration touchpoints
|
|
31
|
+
|
|
32
|
+
- **`worker` / `library` (1.0)** — the shared togasupply sync engine + `App_Api_Toga2` bridge.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Client: S&P Global `spglobal`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [S&P Global](profile.md) | 2.0 | S&P Global is a **TOGa-Supply NetSuite integration client** on the shared 1.0 NetSuite → TOGa Supply per-client sync (`worker` + `library`). | |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "S&P Global"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
apps:
|
|
5
|
+
- library
|
|
6
|
+
- worker
|
|
7
|
+
project: Worker
|
|
8
|
+
client: spglobal
|
|
9
|
+
type: profile
|
|
10
|
+
status: active
|
|
11
|
+
updated: 2026-08-29
|
|
12
|
+
owners: ["jcardinal"]
|
|
13
|
+
files: []
|
|
14
|
+
related:
|
|
15
|
+
- ../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
S&P Global is a **TOGa-Supply NetSuite integration client** on the shared 1.0 NetSuite → TOGa Supply
|
|
21
|
+
per-client sync (`worker` + `library`). NetSuite transactions sync into its 2.0 `Client_<Id>` tenant
|
|
22
|
+
through the TOGa2 API via a thin `worker/crons/toga2/netsuite/sync_togasupply_*.php` wrapper.
|
|
23
|
+
|
|
24
|
+
This profile was seeded during the 2026-08-29 root-cause work on the stuck togasupply integrations:
|
|
25
|
+
S&P Global was one of the clients exposed to the item-fulfillment "Undefined index" on an empty
|
|
26
|
+
`orderLine` (`syncItemFulfillmentFromNetsuite`), fixed in shared code. Full mechanics and the
|
|
27
|
+
diagnosis playbook live in
|
|
28
|
+
[NetSuite → TOGa Supply per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
29
|
+
|
|
30
|
+
## Integration touchpoints
|
|
31
|
+
|
|
32
|
+
- **`worker` / `library` (1.0)** — the shared togasupply sync engine + `App_Api_Toga2` bridge.
|
package/package.json
CHANGED