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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +117 -0
- package/knowledge/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +111 -0
- package/knowledge/INDEX.md +2 -2
- package/package.json +1 -1
|
@@ -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/`).
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
8
|
-
- **worker** (Worker) —
|
|
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