toga-ai 1.0.829 → 1.0.830
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/architecture.md +2 -1
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +5 -1
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +13 -1
- package/knowledge/clients/nychh/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- library/_.php
|
|
@@ -85,3 +85,4 @@ Consuming apps use **no Composer** — this library is the "vendor" directory. P
|
|
|
85
85
|
- **`debugVar()` can halt execution** (its `$stopExecution` arg) — remove all calls before merging.
|
|
86
86
|
- **app/ classes silently shadow library/ classes.** The autoloader checks both `__APPROOT__` and `__LIBROOT__`, so an app-level file with the same class path overrides the core one with no warning.
|
|
87
87
|
- **`__APPROOT__` resolution needs the `_` folder.** It is found by locating an ancestor folder named `_`; each app must have that `_` folder at its root or the constant won't resolve.
|
|
88
|
+
- **⚠ SECURITY — OPEN, SYSTEMIC, not fixed (2026-09-17): TLS peer verification is OFF by default for EVERY `App_ApiTransaction` call.** `library/app/apitransaction.php:363` computes `CURLOPT_SSL_VERIFYPEER` from `$sslVerifyPeer` (default `null`, line 77) as `(((is_null($sslVerifyPeer) && isHTTPS()) || !$sslVerifyPeer) ? false : true)` — which resolves to **`false` on HTTPS**. So every outbound HTTPS call through the shared transport (NetSuite OAuth token exchange, every REST read, the RESTlet call, and any other `App_Api_*` integration) runs with peer verification disabled — a MITM can capture the bearer token or inject responses. Pre-existing core defect, **not** caused by the NetSuite retry; affects all callers. Fix: `CURLOPT_SSL_VERIFYPEER = is_null($sslVerifyPeer) ? isHTTPS() : (bool)$sslVerifyPeer;` and have `rest.php` pass `setSSLVerifyPeer(true)` explicitly. No fix chosen yet. The paired token-logging defect (`send()` writes the bearer token to the request log) is on the [SuiteQL/REST shim doc](features/netsuite-suiteql-rest-shim.md).
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: [dfranks, jcardinal, kyalamarthi, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
@@ -39,6 +39,8 @@ Header SuiteQL → per-record detail → line SuiteQL, assembled into SOAP-shape
|
|
|
39
39
|
|
|
40
40
|
**Data model:** reads NetSuite `transaction`, `transactionline`, `previoustransactionlinelink` via SuiteQL and `/record/v1/{type}/{id}` via REST. Writes nothing. **Client variations:** none — uniform across clients (NetSuite is a single shared account).
|
|
41
41
|
|
|
42
|
+
**Transient-read retry in `send()`.** `send()` retries a transient failure before failing loud, but **only for read-only calls** — a `GET`, or a `POST` to `/query/v1/suiteql` (the read-only SuiteQL endpoint); a mutating write is never retried. This absorbs a single NetSuite blip in a large read burst (hundreds of reads per sync run) that would otherwise abort a whole sync section. Detail + scope in Gotchas.
|
|
43
|
+
|
|
42
44
|
### Transaction status sourcing (1.0) — and the 2026.2 REST `status.id` change
|
|
43
45
|
|
|
44
46
|
1.0 reaches NetSuite over **two transports, and neither reads status off a REST record GET body**:
|
|
@@ -85,6 +87,8 @@ Field semantics (verified on the live account):
|
|
|
85
87
|
|
|
86
88
|
**Consequence: two same-day records disagree about whether the SELECT is present, so verify the branch you are on** — `git grep -n "tl.location" library/app/api/netsuite/rest.php` — rather than trusting either doc. And note what restoring it *re-animates*: the leaf→root location rollup at `worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php:1636-1934`, which **auto-inserts and renames `Forecast.Locations` rows**, has been dead code the whole time `$line->location` was null. It stays dormant only because `import_open_orders.php` is `"active": 0` — re-enable that cron on a branch that selects `tl.location` and a dimension table starts being written by a code path nobody has exercised since June. See [open-orders sync](../../../../2.0/apps/worker2/features/netsuite-salesorder-open-orders-sync.md).
|
|
87
89
|
- **A merge resolved "in favour of `_production`" is a silent feature revert — re-grep for your own additions afterwards.** The c3caa11a case is the pattern: it removed two SELECT columns while leaving their consumers in place. It also broke `listLocations()`'s closing brace, and *that* half **was** repaired later (**5c1c194e**, "TRUE-80090: fixing syntax issue") — which is precisely what makes the whole breakage read as already-fixed to the next person who looks.
|
|
90
|
+
- **`send()` retries a TRANSIENT read failure before failing loud — READ-ONLY scope only (2026-09-17).** There is no per-record isolation in the sync engine, so ONE transient NetSuite blip in a large read burst used to abort the whole section and collapse its window (see [per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)). `App_Api_Netsuite_Rest::send()` now retries only when `method === 'GET'` **or** a `POST` to a route starting `/query/v1/suiteql` (the read-only SuiteQL endpoint) — **never a mutating write** (a POST/PUT that got no response could double-create). Transient set = HTTP status `0` (curl no-response/timeout; `App_ApiTransaction::execute` leaves `responseCode` null → cast 0), `429`, `502/503/504` — **NOT** bare `500`, not other `4xx`. 3 attempts total (1 + 2 retries); sleep 1s then 3s + `random_int(0,1)` jitter; one `error_log` per retry. After the last attempt the ORIGINAL throw-or-return runs unchanged, so **fail-loud is preserved** (cursor still cannot advance past an unimported record). Consts `TRANSIENT_RETRY_MAX_ATTEMPTS=3` / `TRANSIENT_RETRY_FIRST_DELAY_SECONDS=1` / `TRANSIENT_RETRY_SECOND_DELAY_SECONDS=3`. The retry wraps `execute` regardless of the throw flag, so it **also** covers the non-throwing GET reads (`send(...,false)`, e.g. `fetchItemFulfillmentById`'s header GET) where a transient blip would otherwise read as a false "record not found" and silently advance the cursor. Ships via a **worker** deploy (worker box clones library at deploy).
|
|
91
|
+
- **⚠ SECURITY — OPEN, not fixed (2026-09-17): `send()` writes the live `Authorization: Bearer <token>` to the API request log.** `library/app/apitransaction.php:393` (Toga2 logs) / `:403` (Toga1 logs) save `requestHeaders` when `$this->log && self::$isLoggingEnabled`. `authenticate()` calls `setLogging(false)` (`rest.php:187`) for exactly this reason; `send()` does **not**, so every logged call stores the bearer token (the transient-retry above multiplies it to up to 3 token writes per fully-failed call). Fix options — **redact the `Authorization` header in the logger** (fixes ALL `App_ApiTransaction` callers, keeps logging — preferred), or `setLogging(false)` in `send()`/`callRestlet()`. No fix chosen yet; record the LOCATION only, never a token value. A separate, systemic transport defect (TLS peer verification off for every `App_ApiTransaction` call) is tracked on [library architecture](../architecture.md).
|
|
88
92
|
- The shim must stay PHP 7.2-compatible (library is 7.2 prod): no arrow functions, typed props, `??=`, or `match`. Lint with `C:\xampp7\php\php.exe -l` before deploying.
|
|
89
93
|
- Field availability varies by NetSuite account **and** record type — probe the live account before assuming a column/relationship exists.
|
|
90
94
|
- **`App_NetSuite::getCustomFieldValue()` (`library/app/netsuite.php`) must be warning-safe.** 1.0 runs on PHP 7.2 in an unguarded path, so any PHP warning → `exit()` (kills the cron). It was hardened 2026-08-27 with `?? []` / `?? null` guards on the custom-field-list traversal. Keep that discipline when touching it.
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -101,6 +101,18 @@ its IDLE write. **There is no timeout, memory cap, or resource limit anywhere in
|
|
|
101
101
|
in prod* (see below), never *tune the window size down*. (Corollary: a window that has grown back to
|
|
102
102
|
the cap is positive evidence the section is completing.)
|
|
103
103
|
|
|
104
|
+
**One cause of a throw is now retried away: a single TRANSIENT NetSuite blip in a large read burst.**
|
|
105
|
+
Because there is no per-record isolation, draining a burst of hundreds of NetSuite reads per run means
|
|
106
|
+
ONE transient blip (empty HTTP status, `429`, `502/503/504`) at any read call site aborted the whole
|
|
107
|
+
section → RUNNING → window ÷3, even though nothing was wrong with the data. `App_Api_Netsuite_Rest::send()`
|
|
108
|
+
now retries transient **read-only** calls (GET / SuiteQL POST) before failing loud, so a lone blip no
|
|
109
|
+
longer collapses the window; a real poison record still throws and freezes as before. Scope + limits on
|
|
110
|
+
the [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md). Live example: NYCHH
|
|
111
|
+
ITEM_FULFILLMENTS was collapsing (mode `96000-RUNNING`, cursor frozen `2026-09-15 16:42:05`) draining a
|
|
112
|
+
~255-fulfillment burst — a different call site failed each run (`fetchItemFulfillmentById` /
|
|
113
|
+
`fetchItemFlagsByIds` / `fetchSalesOrderByIdFull`), which is the signature of a transient, not a poison
|
|
114
|
+
record.
|
|
115
|
+
|
|
104
116
|
**Where the error is: prod `Logs.Issue` / `Logs.Event`, NOT Sentry.** Read worker errors from the
|
|
105
117
|
production log DB by default — you do not need a developer to point you there. The base `Logs`
|
|
106
118
|
schema (prod-logs cluster) holds `Issue` (deduped, with `errorMessage` + `trace`) and `Event` (each
|
|
@@ -18,7 +18,7 @@ project: _Underscore
|
|
|
18
18
|
client: nychh
|
|
19
19
|
type: profile
|
|
20
20
|
status: active
|
|
21
|
-
updated: 2026-09-
|
|
21
|
+
updated: 2026-09-17
|
|
22
22
|
owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
|
|
23
23
|
files:
|
|
24
24
|
- dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
|
|
@@ -73,6 +73,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
|
|
|
73
73
|
- **Missing NetSuite records (item fulfillments + others) root-caused and fixed (2026-09-09).** Two separate bugs in the 1.0 NetSuite→TOGa Supply sync starved NYCHH imports, both deployed and verified in prod: (1) the resume cursor was **read on the wrong clock** (`strtotime()` in Chicago vs the Eastern-stored value), shifting the fetch window +1h and **silently skipping** any record in that 1-hour shadow — hit all 6 sections and all clients; and (2) the fulfillment `GET /sales-orders` ran at `depth=5` and **timed out at 900s**, freezing the section before the fulfillment row was written. Proven on IF `6138135` (Eastern 2025-09-04 17:26:58). Fixes + the standing "cursor is always Eastern" rule live in the [per-client sync feature doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
74
74
|
- **ITEM_RECEIPTS froze on a case-mismatched serial → duplicate-`Units` EV-10/1062, root-caused + fixed (2026-09-11, fix NOT deployed).** The receipts feed threw before its cursor PUT (freezing it and ITEM_FULFILLMENTS behind it) because `syncItemReceiptFromNetsuite` keyed its existing-`Units` PHP lookup on the DB's stored serial casing (`'na100'`) but read it with the uppercased serial (`'NA100'`); the missed row took the create path and collided on the case-insensitive `Units.itemId_serialNumber` unique key → MySQL 1062 / api2 EV-10. Proven on `Client_Nychh.Units` 23400-23409 (serials `'na100'..'na109'`, `c_netsuiteInternalInventoryAssignmentId` NULL). Library-only fix (`strtoupper()` on both the key and the reads). This is a **different** duplicate from the open sales-order `Items` duplicate below (658-BFZH). Mechanism + the EV-10 → `Logs_Nychh.Api` diagnosis traps: [per-client sync doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
75
75
|
- **ITEM_FULFILLMENTS freeze RESOLVED — five stacked reconcile bugs fixed, deployed + verified (2026-09-13).** The frozen section (prod: 25,766 unit links for 1,192 real units on one fulfillment) is cleared: (1) unit dedup now reads existing units **flat + fully paginated** instead of from a truncated nested GET; (2) an unmatched fulfillment line **fails loud** instead of creating a never-matchable NULL-parent orphan (self-heal deletes existing orphans); (3) the SO-items fetch is flat + paginated (api2 truncated the nested list at 25); (4) fulfillment→SO line matching prefers the stable `orderLineUniqueKey` over the drifting line number; (5) a part-number last-resort match for lines whose NetSuite link points at a TAX line. **Package tracking numbers now import to all three bridges (317/318/319)** — the shim fetches `/package?expandSubResources=true` (a bare sub-resource GET returned no fields), so one NetSuite tracking number fans to the header, item, and every serial unit. Engine + shim detail: [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md), [SuiteQL/REST shim](../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md).
|
|
76
|
+
- **ITEM_FULFILLMENTS collapsing on a transient NetSuite blip — fixed in the shared client, NOT deployed (2026-09-17).** The section was collapsing again (mode `96000-RUNNING`, cursor frozen `2026-09-15 16:42:05`) while draining a ~255-fulfillment burst; a **different** read call site failed each run — the signature of a transient blip, not a poison record. Root cause is engine-wide (no per-record isolation), fixed by a transient-read retry in `App_Api_Netsuite_Rest::send()`. After the **worker** deploy, reset `Client_Nychh.Parameters` `NETSUITE_EXECUTION_MODE_ITEM_FULFILLMENTS` to `864000-IDLE` (leave the cursor; it drains the ~261 waiting). Mechanism: [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md), [SuiteQL/REST shim](../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md). **⚠ Two OPEN security defects surfaced in the same layer** (not fixed): `send()` logs the live `Authorization: Bearer` token to the API request log, and TLS peer verification is off for every `App_ApiTransaction` call — see the shim + library architecture docs.
|
|
76
77
|
- **RESOLVED (2026-09-14): INVENTORY_ADJUSTMENTS destination wedge.** `POST /v2/transfer-orders` for **TO 277871** (SO 7102750) failed **EV-10 "destinationLocationId cannot be null"** every run because its customer **3997 "1896 Laurel Hill Management, LLC"** is not a NYCHH customer and has **no `Client_Nychh.Locations` row** (though it IS in `Customers`, id 19 — "maps to a Location" is the scope signal, not "is in Customers"). It reached NYCHH because the adjustment cascade scopes by the shared HQ warehouse then imports a fulfillment for a different customer. Fix: the importer now SKIPS out-of-scope transfer orders (loud log) instead of forcing a null destination; library deployed + verified. A follow-on regression from the added param froze SALES_ORDERS (a 3rd, direct cron call site, issue 754 "Too few arguments") — fixed; **redeploy worker**. See [NetSuite → TransferOrders import](./features/netsuite-transfer-order-import.md).
|
|
77
78
|
- **NYCHH sync scope + data-gap scale + backfill approach (2026-09-09).**
|
|
78
79
|
- **Scope = 22 NetSuite customers:** parent **28908** "NYC Health + Hospitals" + 21 hospital child customers (`24145, 29273, 29276, 31584, 31910-31916, 31918-31925, 32229, 33674`).
|
package/package.json
CHANGED