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.
@@ -6,8 +6,8 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
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-14
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-06-26
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.392",
3
+ "version": "1.0.393",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",