toga-ai 1.0.392 → 1.0.393
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-api-reference.md +14 -2
- package/knowledge/1.0/apps/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +36 -2
- package/knowledge/2.0/apps/_underscore/features/netsuite-rest-client.md +43 -2
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["dfranks"]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: ["dfranks", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
13
13
|
- library/ssl/netsuite_ec_key.pem
|
|
@@ -343,6 +343,13 @@ numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
|
343
343
|
arrow functions (`fn() =>`), typed properties (`public int $x`), `??=`, `match()`, numeric
|
|
344
344
|
separators (`1_000`). Lint with `C:\xampp7\php\php.exe -l` before deploying — `C:\xampp8\php`
|
|
345
345
|
(PHP 8.0) is for running probes on the laptop only, **not** for compat checking.
|
|
346
|
+
- **⚠ Blast radius: a SINGLE PHP 7.4+ token here is a FULL outage, not a localized bug.** On
|
|
347
|
+
2026-06-10 one `static fn(...)` left in `library/app/api/netsuite/rest.php` **parse-errored
|
|
348
|
+
`App_Api_Netsuite_Rest` on class load** — since the parse failure happens at require time, it
|
|
349
|
+
took down **every** consumer of the class at once: **all** 5-minute REST sync crons crashed and
|
|
350
|
+
stayed down until the token was reverted. There is no partial degradation — a banned token
|
|
351
|
+
anywhere in this file crashes the whole NetSuite REST tier. Lint before every deploy; treat a
|
|
352
|
+
7.4+ token in `rest.php` as a production-down defect, not a style nit.
|
|
346
353
|
- Field/relationship availability varies by NetSuite account **and** record type — probe the live
|
|
347
354
|
account before assuming a column exists.
|
|
348
355
|
- **`IS NOT NULL` on a custom `transactionline` column inside an aggregate query is a ~15× planner
|
|
@@ -364,6 +371,11 @@ numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
|
364
371
|
|
|
365
372
|
## Change history
|
|
366
373
|
|
|
374
|
+
- 2026-07-21 — **Recorded the "one PHP 7.4+ token = total outage" blast-radius lesson** on the PHP
|
|
375
|
+
7.2 compat gotcha (folded in from a retired project-local CLAUDE.md). On 2026-06-10 a single
|
|
376
|
+
`static fn(...)` in `rest.php` parse-errored `App_Api_Netsuite_Rest` on class load and crashed ALL
|
|
377
|
+
5-minute REST sync crons until reverted — a banned token here is production-down, not localized.
|
|
378
|
+
(jcardinal)
|
|
367
379
|
- 2026-07-07 — **Added the `systemnote` field-change forensics section and qualified the custom-column
|
|
368
380
|
perf rule** (Forecast2 SALES profit-drift forensics). `systemnote` filtered by `field`+`"date"` is
|
|
369
381
|
~1s vs ~279s by `recordid` alone; it carries `context` (`UIF`), `name` (acting user), `role`,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
|
|
6
6
|
| [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
|
|
7
7
|
| [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php |
|
|
8
|
-
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
|
|
8
|
+
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/reconcile_drift_2023plus.php, test/@dave/probe_invoice_gap_2026.php, test/@dave/probe_creditmemo_gap_detail.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
|
|
9
9
|
| [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
|
|
10
10
|
| [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
|
|
11
11
|
| [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: [dfranks]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: [dfranks, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- test/@dave/checker.php
|
|
13
13
|
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
@@ -16,6 +16,9 @@ files:
|
|
|
16
16
|
- test/@dave/fixer.php
|
|
17
17
|
- test/@dave/analyze_netsuite_forecast_diff.php
|
|
18
18
|
- test/@dave/trueup_sales.php
|
|
19
|
+
- test/@dave/reconcile_drift_2023plus.php
|
|
20
|
+
- test/@dave/probe_invoice_gap_2026.php
|
|
21
|
+
- test/@dave/probe_creditmemo_gap_detail.php
|
|
19
22
|
- test/@dave/trueup_open_orders.php
|
|
20
23
|
- test/@dave/loop_trueup_open_orders.php
|
|
21
24
|
- test/@dave/trueup_opportunities.php
|
|
@@ -98,6 +101,15 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
98
101
|
NS_ONLY (missing from FC), FC_ONLY (stale/extra), DRIFT (value differs). Read-only.
|
|
99
102
|
- `trueup_sales.php --from --to [--chunk-days N] [--prod] [--dry-run]` — makes `Forecast.Sales`
|
|
100
103
|
match NetSuite for a tranDate range (insert/update/delete per line).
|
|
104
|
+
- `reconcile_drift_2023plus.php` — **transaction-level** (NOT line-level) reconciler for a
|
|
105
|
+
**known, small, discrepant set** of transactions. Distinct from the line-level trueup tools: for
|
|
106
|
+
**only the flagged transactions** it **DELETEs all lines then re-INSERTs them from REST**, writes
|
|
107
|
+
a **pre-image CSV backup** first, and is **dry-run by default** (`--commit` to write). Built to
|
|
108
|
+
close the SOAP-era double-line bug (below): it took the 2023 gap from **+$2.27M to +$31.8K**.
|
|
109
|
+
**Decision rule — which reconciler:** a **known small discrepant set** → `reconcile_drift_2023plus`
|
|
110
|
+
(transaction-level delete+reinsert); an **unknown full-window** drift → `trueup_sales`
|
|
111
|
+
(line-level insert/update/delete). **Diagnostic ladder to identify the set:**
|
|
112
|
+
`probe_sales_gap_direct` → `probe_invoice_gap_2026` → `probe_creditmemo_gap_detail`.
|
|
101
113
|
- `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose] [--quiet] [--by-lastmodified]`
|
|
102
114
|
— same for `Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
|
|
103
115
|
- **`--by-lastmodified`** windows **both** passes on `lastmodifieddate` instead of `tranDate`:
|
|
@@ -303,6 +315,17 @@ None — Forecast2 is a single shared dataset.
|
|
|
303
315
|
|
|
304
316
|
## Gotchas / known issues
|
|
305
317
|
|
|
318
|
+
- **SOAP-era double-line bug (2023) — lines inserted TWICE under consecutive-but-different line
|
|
319
|
+
numbers, evading the dup check.** The SOAP-era importer **ran twice for some invoice batches** and
|
|
320
|
+
inserted each invoice's lines **twice** under consecutive-but-*different* `lineNumber`s — so the
|
|
321
|
+
`(txnId, lineNumber)` duplicate check never caught it (the second copy had a different line
|
|
322
|
+
number). **Fingerprint:** Forecast revenue is **exactly 2× NetSuite**, **transaction counts
|
|
323
|
+
match** (it's line duplication, not extra transactions), and **FC line rows > NS line count**.
|
|
324
|
+
Affected **3 months in 2023** (transaction dates **May 1 / Jul 2 / Dec 7**). Fixed with
|
|
325
|
+
`reconcile_drift_2023plus.php` (transaction-level delete-all-lines-then-reinsert-from-REST), which
|
|
326
|
+
closed the 2023 gap from **+$2.27M to +$31.8K**. When a window shows exact-2× revenue with matching
|
|
327
|
+
txn counts and more FC lines than NS, suspect this pattern and use the transaction-level reconciler,
|
|
328
|
+
not the line-level trueup.
|
|
306
329
|
- **CROSS-TYPE CONTAMINATION: once JEs share `Forecast.Sales`, any FC Sales read missing a type scope
|
|
307
330
|
treats JE rows as rogue sales.** Discovered live (TRUE-79862): the SALES fix **deleted 74 JE rows
|
|
308
331
|
(8 JEs) as "stale sales"** because `findSalesDiscrepancies`'s FC query had **no type filter**, so JE
|
|
@@ -549,6 +572,17 @@ None — Forecast2 is a single shared dataset.
|
|
|
549
572
|
|
|
550
573
|
## Change history
|
|
551
574
|
|
|
575
|
+
- 2026-07-21 — **Documented `reconcile_drift_2023plus.php` + the SOAP-era double-line bug (2023)**
|
|
576
|
+
(folded in from a retired project-local CLAUDE.md). The SOAP-era importer ran twice for some
|
|
577
|
+
invoice batches and inserted each invoice's lines twice under consecutive-but-different line
|
|
578
|
+
numbers, evading the `(txnId, lineNumber)` dup check — fingerprint = FC revenue exactly 2× NS,
|
|
579
|
+
matching txn counts, FC line rows > NS lines; affected 3 months in 2023 (txn dates May 1 / Jul 2 /
|
|
580
|
+
Dec 7). `reconcile_drift_2023plus.php` reconciles a **known small set** at the **transaction level**
|
|
581
|
+
(DELETE all lines then re-INSERT from REST for only flagged txns; pre-image CSV backup; dry-run by
|
|
582
|
+
default, `--commit` to write), closing the 2023 gap from +$2.27M to +$31.8K. Recorded the decision
|
|
583
|
+
rule (known small set → `reconcile_drift_2023plus`; unknown full window → `trueup_sales`) and the
|
|
584
|
+
diagnostic ladder (`probe_sales_gap_direct` → `probe_invoice_gap_2026` →
|
|
585
|
+
`probe_creditmemo_gap_detail`). (jcardinal)
|
|
552
586
|
- 2026-07-14 — **`fixer.php` now resolves `customerId` on JE lines, mirroring the importer +
|
|
553
587
|
recorded the manual all-time backfill** (TRUE-80129, dfranks). `fixJournalEntries()` (the
|
|
554
588
|
hand-maintained mirror of `buildJournalEntryRows`) previously hard-coded `customerId = NULL`;
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["dfranks"]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: ["dfranks", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
13
13
|
related:
|
|
@@ -31,6 +31,40 @@ SuiteQL escaping rule — that callers in other repos depend on.
|
|
|
31
31
|
> (`\api\_Component_Api_Netsuite`) for its own use. Editing the api2 copy has **no effect** on the
|
|
32
32
|
> worker2/webhook path — change the `_underscore` one.
|
|
33
33
|
|
|
34
|
+
## Authentication & config
|
|
35
|
+
|
|
36
|
+
- **OAuth 2.0 certificate-signed JWT client assertion.** The adapter authenticates by signing a
|
|
37
|
+
JWT client assertion with a certificate registered on the NetSuite integration record — no
|
|
38
|
+
username/password, no stored bearer. Signing algorithm is **ES256 preferred** (EC private key),
|
|
39
|
+
with **RS256 as fallback**. The access token is **cached and auto-refreshed ~60s before expiry**,
|
|
40
|
+
so callers never manage token lifecycle.
|
|
41
|
+
- **All credentials come from the `[netsuite]` ini section — nothing is hardcoded.** Keys:
|
|
42
|
+
- `host` — the NetSuite REST host.
|
|
43
|
+
- `clientId` — the integration record's client id.
|
|
44
|
+
- `certificateId` — the integration record's certificate id (the JWT **`kid`**).
|
|
45
|
+
- `privateKeyPath` — filesystem path to the signing private key.
|
|
46
|
+
The per-developer block lives in `worker2/Config/dev-<name>-laptop.ini`. **Document only the key
|
|
47
|
+
names/locations — never paste the values.** (See the 1.0 `netsuite-suiteql-api-reference.md`
|
|
48
|
+
*Authentication* section for the sibling `App_Api_Netsuite_Rest` client, which reads the same
|
|
49
|
+
`[netsuite]` section via `_Config::netsuite()` in the 2.0 context.)
|
|
50
|
+
|
|
51
|
+
### Public API surface
|
|
52
|
+
|
|
53
|
+
- **`send($method, $route, $payload, $headers, $throwExceptionsOnError)`** — the general REST call.
|
|
54
|
+
Verb + route + JSON payload + optional extra headers; `$throwExceptionsOnError` toggles whether a
|
|
55
|
+
non-2xx raises or is returned. (Record writes go through `createRecord`/`send('PATCH', …)` above;
|
|
56
|
+
reads through `fetchRecord`/`send('GET', …)`.)
|
|
57
|
+
- **`RECORD_*` constant catalog** — canonical route constants so callers never hardcode record
|
|
58
|
+
paths: `SALES_ORDER`, `INVOICE`, `CASH_SALE`, `CASH_REFUND`, `CREDIT_MEMO`, `OPPORTUNITY`, `ITEM`,
|
|
59
|
+
`INVENTORY_ITEM`, `CUSTOMER`, `EMPLOYEE`, `VENDOR`, `PURCHASE_ORDER`.
|
|
60
|
+
- **`QUERY_SUITEQL`** — the SuiteQL query route; **`METADATA_CATALOG`** — the metadata/catalog route
|
|
61
|
+
(record & field metadata discovery).
|
|
62
|
+
- **`?expandSubResources=true`** — append to a record GET to inline sub-resources (sublists) in one
|
|
63
|
+
call rather than issuing follow-up requests.
|
|
64
|
+
- **Response normalization rule:** the underlying transport may hand back an already-decoded object
|
|
65
|
+
**or** a raw JSON string, so normalize every response with `is_string($x) ? json_decode($x) : $x`
|
|
66
|
+
before consuming it.
|
|
67
|
+
|
|
34
68
|
## Record writes — REST has no SOAP `baseRef->internalId`
|
|
35
69
|
|
|
36
70
|
### Create — `createRecord(string $route, array $payload): string`
|
|
@@ -121,6 +155,13 @@ doc.)
|
|
|
121
155
|
|
|
122
156
|
## Change history
|
|
123
157
|
|
|
158
|
+
- 2026-07-21 — **Documented the auth mechanism, config-key locations, `send()` signature, and the
|
|
159
|
+
`RECORD_*`/query/metadata constant catalog** (folded in from a retired project-local
|
|
160
|
+
`worker2/netsuite/CLAUDE.md`). Auth = OAuth 2.0 certificate-signed JWT client assertion
|
|
161
|
+
(ES256 preferred / RS256 fallback), token cached + refreshed ~60s before expiry; all credentials
|
|
162
|
+
from the `[netsuite]` ini section (`host`/`clientId`/`certificateId`=`kid`/`privateKeyPath`),
|
|
163
|
+
per-dev block in `worker2/Config/dev-<name>-laptop.ini`. Recorded `?expandSubResources=true` and
|
|
164
|
+
the `is_string($x) ? json_decode($x) : $x` response-normalization rule. (jcardinal)
|
|
124
165
|
- 2026-06-26 — **`fetchRecord` (generic NS REST GET + decode) moved onto this class** from
|
|
125
166
|
`_Component_Forecast_Db`, beside `createRecord`; 7 callers reprefixed (SaleImport ×2,
|
|
126
167
|
Opportunity ×3, SalesOrder ×2), git-grep clean + runtime-verified. Recorded the
|
package/package.json
CHANGED