toga-ai 1.0.57 → 1.0.58

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.
@@ -4,3 +4,4 @@
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
6
  | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
7
+ | [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 |
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: NetSuite SuiteQL/REST Shim — Field Semantics
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-11
10
+ owners: [dfranks]
11
+ files:
12
+ - library/app/api/netsuite/rest.php
13
+ related:
14
+ - ../architecture.md
15
+ - ../../worker/features/forecast2-netsuite-reconciliation.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP
21
+ toolkit. Its `list*()` methods return **shim objects whose PHP class/shape matches the old
22
+ SOAP records**, so existing cron code keeps working. This doc captures the **non-obvious
23
+ SuiteQL/REST field semantics** discovered while building and reconciling the Forecast2 sync —
24
+ the things that silently produce wrong totals or duplicate rows if you don't know them.
25
+
26
+ Production NetSuite code must use REST (SuiteQL/REST), not SOAP — SOAP is being phased out and
27
+ is OK only for ad-hoc troubleshooting.
28
+
29
+ ## Key files / entry points
30
+
31
+ - `library/app/api/netsuite/rest.php`
32
+ - `listSales($from, $to, $entityIds, $dateField)` — Invoice/CashSale/CashRfnd/CustCred.
33
+ - `listSalesOrders($from, $to, $entityIds)` — SalesOrd.
34
+ - Both do a SuiteQL header query, then (historically) a **per-id REST GET** per record for
35
+ fields SuiteQL doesn't expose on `transaction`, then a chunked `transactionline` query.
36
+
37
+ ## How it works
38
+
39
+ Header SuiteQL → per-record detail → line SuiteQL, assembled into SOAP-shaped shims. Sign and
40
+ line-number conventions below are applied so the shim matches what SOAP returned and what the
41
+ Forecast2 tables already store.
42
+
43
+ ## Field semantics / gotchas (verified on the live account)
44
+
45
+ - **Line number = `transactionline.id`, NOT `linesequencenumber`.** `tl.id` equals the REST
46
+ `item[].line` value (the stable SOAP-era line key); `linesequencenumber` is physical order and
47
+ re-maps/collides when SOAP-era records are edited. Verified: sales orders 106 orders / 344
48
+ lines (2019–2029, incl. edited orders with line-id gaps), invoices + cash sales 62/62.
49
+ `listSales` historically resolved line via `uniquekey → REST lineUniqueKey → line`; `tl.id`
50
+ gives the same answer in bulk without the per-id GET.
51
+
52
+ - **ShipItem lines have `costestimate = NULL`** (100% of 42K+ lines, 2025–26). In any profit
53
+ expression `SUM(-foreignamount + costestimate)`, a NULL cost makes the whole term NULL and SQL
54
+ **drops the row from the SUM** — so shipping *revenue* still counts but shipping *profit* does
55
+ not, silently understating NS profit. Always `NVL(tl.costestimate, 0)`. (This caused a fake
56
+ $663K "gap" in the audit tools — see the reconciliation doc.)
57
+
58
+ - **Cash refunds & credit memos carry extra COGS/inventory lines.** SuiteQL `transactionline`
59
+ with `mainline='F' AND item>0` returns, per item, the customer line **plus** a `CUSTOMERRETURN`
60
+ and an `ASSET` posting (all `item>0`), which REST's `item[].items` sublist omits. They are
61
+ distinguished by **`tl.iscogs = 'T'`** (and `accountinglinetype` IN `CUSTOMERRETURN`/`ASSET`).
62
+ Filter **`tl.iscogs = 'F'`** to get exactly the customer-facing lines. Invoices/cash sales do
63
+ not have this (0 extra lines). Without the filter, bulk imports duplicate refund/memo lines.
64
+
65
+ - **Sign conventions are per-type and self-consistent.** SuiteQL returns
66
+ `foreignamount`/`quantity`/`costestimate` **negative for invoices & cash sales, positive for
67
+ credit memos & cash refunds**. A uniform `-foreignamount` therefore yields **positive** revenue
68
+ for invoices and **negative** for reversals — the agreed Forecast2 convention. Do NOT also apply
69
+ a per-type factor (it double-negates reversals back to positive). The shim flips
70
+ `amount = -foreignamount`, `costEstimate = -costestimate`.
71
+
72
+ - **`createdFrom` is not a usable SuiteQL column.** `transaction.createdfrom` is **always NULL**
73
+ in SuiteQL (26/26). It is recoverable from `previoustransactionlinelink.previousdoc` excluding
74
+ `previoustype = 'Opprtnty'` (an invoice links to both its SO and its opportunity; REST's
75
+ `createdFrom` is the SO). **But this is non-deterministic for multi-link transactions** (credit
76
+ memos return several non-Opprtnty `previousdoc` rows in arbitrary order) — two runs can pick
77
+ different values. REST `createdFrom` (per-id GET) is the only deterministic source. Treat the
78
+ link table as best-effort, not authoritative.
79
+
80
+ - **AR open balance = `transaction.foreignamountunpaid`** (== REST `amountRemaining`, 12/12 on
81
+ open invoices). It is **NULL on fully-paid invoices and on cash sales/refunds** — wrap in
82
+ `NVL(...,0)` for a "settled = 0.00" convention. Valid balance columns are **`foreignamountunpaid`,
83
+ `foreignamountpaid`, `foreigntotal`** only; `amountremaining` and `amountunpaid` do **not exist**
84
+ and 400 the query.
85
+
86
+ - **SO originating line** lives in `previoustransactionlinelink` (correlated subquery on
87
+ `previoustype='SalesOrd'`), because `transactionline` has **no `orderline` column**.
88
+
89
+ - **TO_DATE rejects impossible dates** (e.g. `2026-06-31`) with an opaque HTTP 400 — validate
90
+ calendar dates before building SuiteQL.
91
+
92
+ ## Data model
93
+
94
+ Reads NetSuite `transaction`, `transactionline`, `previoustransactionlinelink` via SuiteQL and
95
+ `/record/v1/{type}/{id}` via REST. Writes nothing.
96
+
97
+ ## Client variations
98
+
99
+ None — uniform across clients (NetSuite is a single shared account).
100
+
101
+ ## Gotchas / known issues
102
+
103
+ - The shim must stay PHP 7.2-compatible (library is 7.2 prod): no arrow functions, typed props,
104
+ `??=`, or `match`. Lint with `C:\xampp7\php\php.exe -l` before deploying.
105
+ - Field availability varies by NetSuite account **and** record type — probe the live account
106
+ before assuming a column/relationship exists.
107
+
108
+ ## Change history
109
+
110
+ - 2026-06-11 — Documented bulk SuiteQL field semantics (tl.id==line, ShipItem NULL cost, iscogs
111
+ COGS filter, createdFrom non-determinism, foreignamountunpaid, sign conventions) surfaced while
112
+ moving the Forecast2 trueup tools off per-id REST GETs. (dfranks)
113
+
114
+ ## Related docs
115
+
116
+ - `../architecture.md` — Library core architecture.
117
+ - `../../worker/features/forecast2-netsuite-reconciliation.md` — the tooling that applies these.
@@ -3,3 +3,4 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
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
+ | [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/reconcile_netsuite_totals.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-11
10
+ owners: [dfranks]
11
+ files:
12
+ - test/@dave/reconcile_netsuite_totals.php
13
+ - test/@dave/analyze_netsuite_forecast_diff.php
14
+ - test/@dave/trueup_sales.php
15
+ - test/@dave/trueup_open_orders.php
16
+ - worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
17
+ related:
18
+ - ../architecture.md
19
+ - ../../library/features/netsuite-suiteql-rest-shim.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and
25
+ NetSuite. The 5-min importer (`common_import_sales_from_netsuite.php`) filters on
26
+ `lastmodifieddate` and the nightly discrepancy-fix only looks back 30 days, so older rows whose
27
+ NetSuite values change (e.g. cost re-valuation) are never re-pulled. These tools close that gap
28
+ by reconciling a chosen tranDate range directly against NetSuite.
29
+
30
+ ## Key files / entry points
31
+
32
+ - `reconcile_netsuite_totals.php [from] [to]` — category grand totals NS vs Forecast2 (Sales,
33
+ Sales Profit, Open Orders, Opportunities) with deltas. Read-only. Connects explicitly to the
34
+ **prod core2 reader**; NetSuite via SuiteQL SUMs.
35
+ - `analyze_netsuite_forecast_diff.php [from] [to]` — decomposes the delta **per transaction** into
36
+ NS_ONLY (missing from FC), FC_ONLY (stale/extra), DRIFT (value differs). Read-only.
37
+ - `trueup_sales.php --from --to [--chunk-days N] [--prod] [--dry-run]` — makes `Forecast.Sales`
38
+ match NetSuite for a tranDate range (insert/update/delete per line).
39
+ - `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose]` — same for
40
+ `Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
41
+
42
+ ## How it works
43
+
44
+ - **Bulk SuiteQL (not per-id GET).** Both trueup tools were rewritten to fetch detail via chunked
45
+ `WHERE transaction IN (...)` SuiteQL using `tl.id` as the line number (see the shim doc for why
46
+ that's valid), replacing the ~1.5s-per-record REST GET. Result: a day that took ~48 min now
47
+ runs in seconds (parity verified — `trueup_sales` matched the old `listSales` path 1813/1813
48
+ records, 3675/3675 line cells on a sample day). `trueup_open_orders` builds its header+lines
49
+ per chunk; `trueup_sales` does it in a local `fetchSalesBulk()` that returns the same shim shape
50
+ `App_Api_Netsuite_Rest::listSales()` produced, so the reconciliation loop is unchanged.
51
+ - **`--prod` targeting (self-contained).** trueup writes via `App_Database(...,'db_forecast2')`.
52
+ On dev laptops that registry config points at **localhost XAMPP**; `--prod` overrides it in
53
+ memory at runtime by reading `[database_forecast2]` from `worker/config.worker.ini` (the core2
54
+ **writer**). `App_Database::registerDatabaseConnect` reads the registry config lazily on first
55
+ query, so the override also covers the keepalive reconnect. **Do not edit any `config.*.ini`** to
56
+ target prod — that mutates shared dev state and silently leaves the laptop pointed at the writer.
57
+ - **Chunking + keepalive.** trueup_sales chunks by `--chunk-days` (default 7) and commits per
58
+ chunk so a crash resumes. Both ping/reconnect `db_forecast2` before DB work (Aurora drops idle
59
+ links during long NS calls).
60
+
61
+ ## Data model
62
+
63
+ `Forecast.Sales`, `Forecast.OpenOrderItems` on the **core2** cluster
64
+ (reader `reader1.core.database.togahub.com`, writer `writer.core.database.togahub.com`). Source
65
+ of truth is NetSuite; FC is made to match.
66
+
67
+ ## Client variations
68
+
69
+ None — Forecast2 is a single shared dataset.
70
+
71
+ ## Gotchas / known issues
72
+
73
+ - **NVL the cost in NS profit SUMs.** `reconcile`/`analyze` compute NS profit as
74
+ `SUM(-foreignamount + NVL(costestimate,0))`. Without `NVL`, ShipItem lines (NULL cost) drop from
75
+ the SUM and NS profit is understated, producing a **fake FC-over-NS "gap"** ($663K across 2025).
76
+ After the fix, the real residual is the opposite sign and small: **~$87K NS-over-FC**,
77
+ concentrated **Dec 2025–Apr 2026**, driven by NetSuite re-valuing `costestimate` on older
78
+ invoices that no `lastmodifieddate`-based sync re-pulls. (Revenue reconciles to the penny; only
79
+ profit drifts.)
80
+ - **Compare money at 2 decimals.** DB columns store 2dp but PHP `revenue - cost` carries float
81
+ dust (`313.6` vs `313.60000001`); raw `!=` produced thousands of phantom UPDATEs that re-wrote
82
+ identical values (1,839 on one open-orders run). Both tools now compare `round((float)$x, 2)`.
83
+ - **`trueup_sales` does not manage `createdFrom` or `amountDue`.**
84
+ - `createdFrom` — non-deterministic from the link table (see shim doc); the REST-based importer
85
+ owns it. trueup preserves it on UPDATE, leaves NULL on INSERT.
86
+ - `amountDue` (`Forecast.Sales`) and `dtPendingBilling` (`OpenOrderItems`) are **not yet in
87
+ prod** — those columns exist only in local dev (TRUE-78923 / TRUE-79081 migrations are
88
+ local-only). Both trueup tools have those fields **removed** so they run against prod. ⚠ The
89
+ prod migrations (`dbchanges2/Forecast/2026-06-09 *.sql`) must be applied **before** this
90
+ session's importer/discrepancy-fix changes (which write those columns) deploy, or production
91
+ crashes with `Unknown column`.
92
+ - **Always dry-run prod first** and confirm the change counts are real drift, not artifacts —
93
+ e.g. 2023-01-03 showed 240 "updates" that were entirely createdFrom backfill + profit float-dust
94
+ (zero real drift); after the fixes it correctly reports `unchanged=240`.
95
+ - These tools live in `test/@dave/` (developer tooling), but `trueup_open_orders` has been run
96
+ against production. The `Defaults`/checkpoint mechanics of the scheduled sync are separate.
97
+
98
+ ## Change history
99
+
100
+ - 2026-06-11 — `trueup_sales` rewritten to bulk SuiteQL (`fetchSalesBulk`, ~300× faster);
101
+ `amountDue` + `createdFrom` dropped; float-dust fixed; `--prod` flag added. `reconcile`/`analyze`
102
+ profit queries `NVL(costestimate,0)` — established the $663K gap was an audit artifact and the
103
+ real drift is ~$87K NS-over-FC in Dec 2025–Apr 2026. (dfranks)
104
+ - 2026-06-10 — `trueup_open_orders` bulk SuiteQL refactor + `--prod`; phantom float-dust UPDATEs
105
+ eliminated; Aurora idle-drop reconnect. (dfranks)
106
+
107
+ ## Related docs
108
+
109
+ - `../../library/features/netsuite-suiteql-rest-shim.md` — the SuiteQL/REST field semantics these
110
+ tools rely on.
111
+ - `../architecture.md` — Worker architecture (forecast2 sync lives in `crons/toga2/forecast2/`).
@@ -4,8 +4,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 2 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 2 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 3 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
+ - **worker** (Worker) — 3 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
 
10
10
  ## 2.0 framework
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.57",
3
+ "version": "1.0.58",
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",