toga-ai 1.0.61 → 1.0.63
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-api-reference.md +321 -0
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +3 -0
- package/knowledge/1.0/apps/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +33 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/mits-po-to-so-item-linking.md +21 -0
- package/package.json +1 -1
|
@@ -4,4 +4,5 @@
|
|
|
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 API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem |
|
|
7
8
|
| [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,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NetSuite SuiteQL/REST API Reference
|
|
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
|
+
- library/ssl/netsuite_ec_key.pem
|
|
14
|
+
related:
|
|
15
|
+
- netsuite-suiteql-rest-shim.md
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../worker/features/forecast2-netsuite-reconciliation.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
General working reference for the Agilant NetSuite integration: how to authenticate, how
|
|
23
|
+
SuiteQL behaves, and the confirmed schema of the tables/columns/codes we query. The client class
|
|
24
|
+
is `App_Api_Netsuite_Rest` (`library/app/api/netsuite/rest.php`), split out of the old
|
|
25
|
+
`netsuite.php` ~2026-05-15. SOAP is deprecated — **production code must use REST/SuiteQL**; SOAP
|
|
26
|
+
is acceptable only for ad-hoc troubleshooting.
|
|
27
|
+
|
|
28
|
+
This doc is the **broad map** (mechanics, schemas, codes, performance). For the deep field
|
|
29
|
+
*semantics* that silently corrupt totals (sign conventions, `tl.id`==line, ShipItem NULL cost,
|
|
30
|
+
`iscogs` COGS lines, `createdFrom` non-determinism), see the companion
|
|
31
|
+
`netsuite-suiteql-rest-shim.md`.
|
|
32
|
+
|
|
33
|
+
## Authentication
|
|
34
|
+
|
|
35
|
+
- **REST OAuth 2.0 / JWT client-credentials.** The client assertion is signed with an EC private
|
|
36
|
+
key at `library/ssl/netsuite_ec_key.pem`. Token lifetime is **3600s**; the adapter auto-refreshes
|
|
37
|
+
~60s before expiry (`TOKEN_EXPIRATION_SAFETY_SECONDS`).
|
|
38
|
+
- The `certificateId` (the JWT `kid`) comes from the NetSuite integration record. The **worker**
|
|
39
|
+
config (`worker/config.*.ini` `[netsuite]` section) holds the real value; `worker2/Config/` ships
|
|
40
|
+
a placeholder `CERTIFICATE_ID_HERE` that `initialize()` explicitly rejects.
|
|
41
|
+
- `readConfig()` supports two framework contexts: 1.0 via `App_Registry::get('config')['netsuite']`,
|
|
42
|
+
and 2.0 via `_Config::netsuite()`.
|
|
43
|
+
|
|
44
|
+
## Bootstrapping the client from a standalone script
|
|
45
|
+
|
|
46
|
+
To use the client outside a normal app entry point (e.g. a `test/@dave` CLI):
|
|
47
|
+
|
|
48
|
+
```php
|
|
49
|
+
$_SERVER['SCRIPT_FILENAME'] = realpath(__DIR__ . '/../../worker') . DIRECTORY_SEPARATOR . '_approot_anchor.php';
|
|
50
|
+
chdir(__DIR__ . '/../../worker'); // REQUIRED — OAuth cert / certificateId resolves relative to worker/
|
|
51
|
+
require_once('_.php');
|
|
52
|
+
App_Framework_Worker::initialize();
|
|
53
|
+
App_Api_Netsuite_Rest::authenticate(); // acquire OAuth2 token once
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **`chdir` into `worker/` is mandatory** — without it the OAuth `certificateId`/cert path won't
|
|
57
|
+
resolve and `authenticate()` throws.
|
|
58
|
+
- In our usage the client is effectively **read-only**: only `GET` (record fetch) and `POST`
|
|
59
|
+
(SuiteQL) are exercised. `send()` accepts any verb (incl. PATCH/PUT) but we don't write through it.
|
|
60
|
+
|
|
61
|
+
### The `send()` logging trap (dev laptop)
|
|
62
|
+
|
|
63
|
+
`App_Api_Netsuite_Rest::send()` auto-authenticates but does **not** call `setLogging(false)`. From a
|
|
64
|
+
dev laptop the API-log DB write then fails and the framework error handler reports a **misleading
|
|
65
|
+
"Could not connect to database 'Vision'"** that masks the real result. Workaround: `authenticate()`
|
|
66
|
+
once, then issue your own `App_ApiTransaction` with logging off:
|
|
67
|
+
|
|
68
|
+
```php
|
|
69
|
+
$url = rtrim(App_Api_Netsuite_Rest::$endpoint, '/') . App_Api_Netsuite_Rest::API_BASE_PATH
|
|
70
|
+
. '/query/v1/suiteql?limit=1000&offset=0';
|
|
71
|
+
$req = new App_ApiTransaction('POST', $url, ['q' => $sql], App_ApiTransaction::ENCODE_JSON);
|
|
72
|
+
$req->setLogging(false);
|
|
73
|
+
$req->addHeader('Authorization', 'Bearer ' . App_Api_Netsuite_Rest::$accessToken);
|
|
74
|
+
$req->addHeader('Accept', 'application/json');
|
|
75
|
+
$req->addHeader('Content-Type', 'application/json');
|
|
76
|
+
$req->addHeader('Prefer', 'transient'); // required for SuiteQL
|
|
77
|
+
$resp = $req->execute(false);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
In production (worker/EB) `send()` is fine — the Logs DB is reachable there.
|
|
81
|
+
|
|
82
|
+
## SuiteQL
|
|
83
|
+
|
|
84
|
+
- **Endpoint:** `POST {base}/query/v1/suiteql?limit=1000&offset=N`, body `{"q": "<sql>"}`.
|
|
85
|
+
- **`Prefer: transient` header is required** (avoids server-side cursor overhead / errors).
|
|
86
|
+
- **Pagination:** max `limit=1000`; the response carries `items[]` and a boolean **`hasMore`**. Loop
|
|
87
|
+
`offset += 1000` until `hasMore` is false. The private `suiteqlListAll()` helper in `rest.php` does
|
|
88
|
+
exactly this.
|
|
89
|
+
- **Session timezone is `America/New_York`** — `CURRENT_DATE`/`CURRENT_TIMESTAMP`, `TO_TIMESTAMP()`,
|
|
90
|
+
and `lastmodifieddate` all resolve in ET (−04:00/−05:00), distinct from the account's "Pacific"
|
|
91
|
+
setting. Confirmed empirically: `SYSDATE` is −07:00 but the date columns are ET. **Build window
|
|
92
|
+
bounds in ET.**
|
|
93
|
+
- **Date vs timestamp filters:** `trandate` is a **DATE** column — use
|
|
94
|
+
`TO_DATE('YYYY-MM-DD', 'YYYY-MM-DD')`. `lastmodifieddate` is a **TIMESTAMP** — use
|
|
95
|
+
`TO_TIMESTAMP('YYYY-MM-DD HH:MM:SS', 'YYYY-MM-DD HH24:MI:SS')`. `TO_DATE` rejects impossible
|
|
96
|
+
calendar dates (e.g. `2026-06-31`) with an opaque HTTP 400 — validate dates before building SQL.
|
|
97
|
+
- **Oracle-flavored functions work:** `TO_DATE`, `TO_TIMESTAMP`, `TO_CHAR(trandate,'YYYY')` /
|
|
98
|
+
`'YYYY-MM'` for bucketing, `NVL(...)`, and `BUILTIN.DF(field)` for a reference field's display
|
|
99
|
+
value (e.g. `BUILTIN.DF(entity)` → customer name, `BUILTIN.DF(status)` → "Pending Fulfillment") —
|
|
100
|
+
a cheap alternative to per-id GETs for display strings.
|
|
101
|
+
- **`ORDER BY` inside a `GROUP BY` query is rejected** with `Invalid or unsupported search`. Sort in
|
|
102
|
+
PHP after fetching all pages.
|
|
103
|
+
- **Correlated subqueries are supported** (used in `listSales()` for `orderLine` — see the
|
|
104
|
+
`previousTransactionLineLink` section).
|
|
105
|
+
|
|
106
|
+
### Debugging: the framework swallows errors as an HTML page
|
|
107
|
+
|
|
108
|
+
A failing query (or any PHP error) often renders the framework's HTML "Oops" error page with a
|
|
109
|
+
Sentry Error ID instead of a usable message. To see the real cause from a CLI script:
|
|
110
|
+
|
|
111
|
+
- check `$req->responseCode` and print the raw body on a non-2xx, and/or
|
|
112
|
+
- register a shutdown handler: `register_shutdown_function(function () { print_r(error_get_last()); });`
|
|
113
|
+
|
|
114
|
+
`execute()` may return an already-decoded object **or** a string — normalize with
|
|
115
|
+
`json_decode(json_encode($resp), true)` and always check `responseCode` is 2xx first.
|
|
116
|
+
|
|
117
|
+
## `transaction` table
|
|
118
|
+
|
|
119
|
+
### Type codes (`transaction.type`)
|
|
120
|
+
|
|
121
|
+
| Code | Meaning |
|
|
122
|
+
|------|---------|
|
|
123
|
+
| `SalesOrd` | Sales Order |
|
|
124
|
+
| `CustInvc` | Invoice |
|
|
125
|
+
| `CashSale` | Cash Sale |
|
|
126
|
+
| `CashRfnd` | Cash Refund |
|
|
127
|
+
| `CustCred` | Credit Memo |
|
|
128
|
+
| `Opprtnty` | Opportunity |
|
|
129
|
+
| `PurchOrd` | Purchase Order |
|
|
130
|
+
| `ItemRcpt` | Item Receipt |
|
|
131
|
+
| `ItemShip` | Item Fulfillment |
|
|
132
|
+
| `InvAdjst` | Inventory Adjustment |
|
|
133
|
+
|
|
134
|
+
### Status codes (`transaction.status` letter)
|
|
135
|
+
|
|
136
|
+
**Sales Orders:** `A` Pending Approval · `B` Pending Fulfillment · `C` Cancelled ·
|
|
137
|
+
`D` Partially Fulfilled · `E` Pending Billing/Partially Fulfilled · `F` Pending Billing ·
|
|
138
|
+
`G` Billed · `H` Closed. **"Open" sales orders = `B`, `D`, `E`, `F`.** Note `listSalesOrders()`
|
|
139
|
+
returns **all** statuses for a window (no server-side status filter) — callers must filter.
|
|
140
|
+
|
|
141
|
+
**Invoices:** `A` Open · `B` Paid In Full.
|
|
142
|
+
**Credit Memos:** `A` Open · `B` Fully Applied.
|
|
143
|
+
**Cash Sales:** `A` Unapproved Payment · `C` Deposited.
|
|
144
|
+
**Cash Refunds:** `Y` (the only status; empty string in SOAP).
|
|
145
|
+
|
|
146
|
+
### Header columns present (✓ confirmed in SuiteQL)
|
|
147
|
+
|
|
148
|
+
`id`, `tranid`, `trandate`, `lastmodifieddate`, `type`, `status`, `entity`, `employee`,
|
|
149
|
+
`leadsource`, `terms`, `memo`, `otherrefnum`, `foreigntotal`/`total` (both return the same value),
|
|
150
|
+
`foreignamountunpaid` (AR balance — invoices only; NULL for SO/credit memo/cash sale), `duedate`,
|
|
151
|
+
`shipmethod`, `opportunity`, `shippingaddress` (numeric id → join `transactionShippingAddress`),
|
|
152
|
+
and custom fields (`CUSTBODY_END_CUSTOMER`, `CUSTBODY_STOCKING_ORDER`, …).
|
|
153
|
+
|
|
154
|
+
### Header fields NOT in SuiteQL (✗ — require a per-id REST GET)
|
|
155
|
+
|
|
156
|
+
`shippingCost`, `taxTotal`, `subTotal`, `createdFrom` (also derivable from
|
|
157
|
+
`previousTransactionLineLink`, but non-deterministically — see shim doc), `trackingNumbers`,
|
|
158
|
+
`linkedTrackingNumbers`, and the **structured** `shippingAddress` object (SuiteQL gives only the id).
|
|
159
|
+
|
|
160
|
+
## `transactionline` table
|
|
161
|
+
|
|
162
|
+
### Key columns (confirmed present)
|
|
163
|
+
|
|
164
|
+
`transaction`, `linesequencenumber`, `uniquekey`, `id`, `item`, `itemtype`, `mainline`, `quantity`,
|
|
165
|
+
`foreignamount`, `rate`, `costestimate`, `class`, `memo`, `quantitybilled`, `quantitycommitted`,
|
|
166
|
+
`quantitypicked`, `quantityshiprecv`, `createdfrom`, `entity` (line-level customer), `location`,
|
|
167
|
+
`subsidiary`.
|
|
168
|
+
|
|
169
|
+
- **`mainline = 'F'`** selects detail (non-summary) lines; `mainline = 'T'` is the header row.
|
|
170
|
+
- **`tl.orderline` does NOT exist** — HTTP 400 `Field 'orderline' for record 'transactionLine' was
|
|
171
|
+
not found.` The originating SO line lives in `previousTransactionLineLink`.
|
|
172
|
+
- **Synthetic-line sentinels:** `tl.item` returns **negative ids** for synthetic rows (e.g. `-8` =
|
|
173
|
+
TaxGroup "-Not Taxable-"). Filter `tl.item > 0`, and exclude `itemtype NOT IN ('ShipItem',
|
|
174
|
+
'TaxItem','TaxGroup','Discount','EndGroup','BeginGroup','Subtotal','Description','Markup',
|
|
175
|
+
'Payment')`.
|
|
176
|
+
- Sign conventions and the `tl.id`==`line` rule are detailed in `netsuite-suiteql-rest-shim.md`.
|
|
177
|
+
|
|
178
|
+
### Line filtering for revenue queries
|
|
179
|
+
|
|
180
|
+
- `tl.mainline = 'F'` and `tl.item > 0` plus the itemtype denylist above.
|
|
181
|
+
- For a **product vs shipping** split, also exclude `'ShipItem'`/`'Discount'` from product and sum
|
|
182
|
+
`ShipItem` separately.
|
|
183
|
+
- **Open-order (in-flight) product revenue:** `((-tl.quantity) - NVL(tl.quantitybilled,0)) * tl.rate`;
|
|
184
|
+
shipping term `SUM(-tl.foreignamount)` over `tl.itemtype = 'ShipItem'`.
|
|
185
|
+
|
|
186
|
+
## `previousTransactionLineLink` table
|
|
187
|
+
|
|
188
|
+
The bridge between a child line (e.g. invoice) and its originating parent line (e.g. SO).
|
|
189
|
+
|
|
190
|
+
| Column | Meaning |
|
|
191
|
+
|--------|---------|
|
|
192
|
+
| `nextdoc` / `nextline` / `nexttype` | Child transaction id / its `linesequencenumber` / type (e.g. `CustInvc`) |
|
|
193
|
+
| `previousdoc` / `previousline` / `previoustype` | Parent id / its `linesequencenumber` (= SOAP `orderLine`) / type (e.g. `SalesOrd`) |
|
|
194
|
+
| `linktype` | e.g. `OrdBill`, `OrdRvCom`, `OppClose` |
|
|
195
|
+
|
|
196
|
+
**SOAP `orderLine` for an invoice line:**
|
|
197
|
+
|
|
198
|
+
```sql
|
|
199
|
+
SELECT MIN(potl.previousline)
|
|
200
|
+
FROM previoustransactionlinelink potl
|
|
201
|
+
WHERE potl.nextdoc = <invoice_id>
|
|
202
|
+
AND potl.nextline = <linesequencenumber>
|
|
203
|
+
AND potl.previoustype = 'SalesOrd'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Filter `previoustype = 'SalesOrd'` to exclude the `OppClose` → Opportunity link; `MIN()` collapses
|
|
207
|
+
the multiple link types (`OrdBill`, `OrdRvCom`) that reference the same line. For `createdFrom`, the
|
|
208
|
+
REST record GET is authoritative — the mainline link is ambiguous (see shim doc).
|
|
209
|
+
|
|
210
|
+
## `transactionShippingAddress` table
|
|
211
|
+
|
|
212
|
+
`transaction.shippingaddress` is a numeric id → join here **on `nkey`** (`WHERE id = ...` does NOT
|
|
213
|
+
work; the pk is `nkey`). Columns: `addr1`, `addr2`, `addressee`, `attention`, `city`, `state`
|
|
214
|
+
(two-letter), `country` (two-letter), `zip`, `addrtext` (formatted full-address blob).
|
|
215
|
+
|
|
216
|
+
## `item` table
|
|
217
|
+
|
|
218
|
+
`itemtype` (+ `subtype` / `isserialitem`) → REST shim `_recordType` / SOAP class:
|
|
219
|
+
|
|
220
|
+
| `itemtype` + qualifier | `_recordType` |
|
|
221
|
+
|------------------------|---------------|
|
|
222
|
+
| `InvtPart` + `isserialitem=T` | `serializedInventoryItem` |
|
|
223
|
+
| `InvtPart` + `isserialitem=F` | `inventoryItem` |
|
|
224
|
+
| `NonInvtPart` + `Sale` / `Resale` / `Purchase` | `nonInventorySaleItem` / `nonInventoryResaleItem` / `nonInventoryPurchaseItem` |
|
|
225
|
+
| `OthCharge` + `Sale` / `Resale` | `otherChargeSaleItem` / `otherChargeResaleItem` |
|
|
226
|
+
| `Service` + `Sale` / `Resale` | `serviceSaleItem` / `serviceResaleItem` |
|
|
227
|
+
| `Discount` / `Kit` / `Group` / `Description` | `discountItem` / `kitItem` / `itemGroup` / `descriptionItem` |
|
|
228
|
+
| `Expense` | **skip** — internal; no SOAP equivalent |
|
|
229
|
+
|
|
230
|
+
`item.description` = SOAP `salesDescription`; `item.purchasedescription` = SOAP
|
|
231
|
+
`purchaseDescription`. There is **no** SuiteQL `salesdescription` column.
|
|
232
|
+
|
|
233
|
+
## `customer` table
|
|
234
|
+
|
|
235
|
+
- `BUILTIN.DF(custentity10)` = display name of the **Consolidated Customer** custom field (CF id
|
|
236
|
+
4968, script id `custentity10`); SOAP `customFieldList` CF internalId 4968 maps to this.
|
|
237
|
+
- `entityid` and `companyname` are split in SuiteQL; SOAP concatenated them as
|
|
238
|
+
`"<entityid> <companyname>"`. When they're equal, return the value once (don't duplicate).
|
|
239
|
+
|
|
240
|
+
## Custom fields
|
|
241
|
+
|
|
242
|
+
| NS internalId | Script id | Description |
|
|
243
|
+
|---------------|-----------|-------------|
|
|
244
|
+
| 3149 | `CUSTBODY_END_CUSTOMER` | End Customer (on transaction) |
|
|
245
|
+
| 7097 | `CUSTBODY_STOCKING_ORDER` | Stocking Order flag |
|
|
246
|
+
| 4968 | `custentity10` | Consolidated Customer (on customer record) |
|
|
247
|
+
| 6840 | `custbody_forecast_category` | Forecast Category (on opportunity) |
|
|
248
|
+
| 6841 | `custbody_sales_stage` | Sales Stage (on opportunity) |
|
|
249
|
+
| 3942 | `custbody_ctc_account` (`CF_ACCOUNT`) | Account |
|
|
250
|
+
| 6579 | `custbody_ctc_business_unit` (`CF_BUSINESS_UNIT`) | Business Unit |
|
|
251
|
+
|
|
252
|
+
## REST per-id record GET
|
|
253
|
+
|
|
254
|
+
Used for fields SuiteQL doesn't expose on `transaction`: `shippingCost`, `taxTotal`, `subTotal`,
|
|
255
|
+
`amountRemaining` (SuiteQL `foreignamountunpaid` is the equivalent), `trackingNumbers` /
|
|
256
|
+
`linkedTrackingNumbers`, the structured `shippingAddress`, the `createdFrom` reference, and the
|
|
257
|
+
**stable `line` number** on the record sublist (`line` ↔ `lineUniqueKey`).
|
|
258
|
+
|
|
259
|
+
- **Stable line numbers — the critical one.** The REST record's `item.items[*].line` (via
|
|
260
|
+
`lineUniqueKey`) is the **authoritative** source for `Forecast.Sales.lineNumber`. SuiteQL
|
|
261
|
+
`linesequencenumber` can diverge from REST `line` for SOAP-era records (confirmed empirically),
|
|
262
|
+
so keying on `linesequencenumber` risks duplicate Sales rows that can't be matched or deleted.
|
|
263
|
+
**`listSales()` keeps the per-id GET for this reason — do not eliminate it.**
|
|
264
|
+
|
|
265
|
+
## `list*()` helpers — behavior & cost
|
|
266
|
+
|
|
267
|
+
`listSales()`, `listSalesOrders()`, `listOpportunities()` all:
|
|
268
|
+
|
|
269
|
+
1. **Filter on `lastmodifieddate`, NOT `trandate`.** A record dated long ago but edited recently IS
|
|
270
|
+
returned; a recently-dated but unmodified record is NOT. (Trips up any local lookup that windows
|
|
271
|
+
on transaction date.)
|
|
272
|
+
2. Fetch headers via paginated SuiteQL, then — for sales / sales-orders — do a **per-id REST GET per
|
|
273
|
+
header** for the GET-only fields above, then fetch lines in `id` chunks of 500.
|
|
274
|
+
|
|
275
|
+
**Cost implication:** runtime scales with the number of records *modified* in the window (open +
|
|
276
|
+
closed) because of the per-id GET, not with the number you care about — a wide window is expensive.
|
|
277
|
+
`listOpportunities()` has no per-id GET, so it runs in minutes regardless of window.
|
|
278
|
+
|
|
279
|
+
### Measured volumes / latency (Agilant account, 2024–2026)
|
|
280
|
+
|
|
281
|
+
| Metric | Value |
|
|
282
|
+
|--------|-------|
|
|
283
|
+
| Heaviest month (Aug 2024) | 14,795 transactions / 69,802 item lines |
|
|
284
|
+
| Typical heavy month | ~4,000–6,000 transactions |
|
|
285
|
+
| SuiteQL header page (1000 rows) | ~1.5s |
|
|
286
|
+
| SuiteQL line chunk (500 txns, 1000 rows) | ~1.4s |
|
|
287
|
+
| SuiteQL link chunk (500 txns, 1000 rows) | ~0.5s |
|
|
288
|
+
| Per-id REST GET (one record) | ~100–150ms |
|
|
289
|
+
| `listSales()` — typical month (5k txns) | ~1.5–2h (GET-bound) |
|
|
290
|
+
| `listSales()` — Aug 2024 (14.8k txns) | ~4–6h (GET-bound) |
|
|
291
|
+
| `listOpportunities()` — same window | minutes (no GET) |
|
|
292
|
+
|
|
293
|
+
SalesOrd per-id GET counts by `lastmodifieddate` window: ~16,600 (90d) / ~61,900 (365d) / ~145,700
|
|
294
|
+
(3yr) — vs only ~13,500 currently-open SOs, i.e. a 3-year window does ~10× the work of the target
|
|
295
|
+
set. Fields that *could* move to the header query (eliminating most GETs): `total`,
|
|
296
|
+
`foreignamountunpaid`, `dueDate`, `terms`, `shipMethod`, `opportunity`, `shippingAddress`. Fields
|
|
297
|
+
that **cannot**: `shippingCost`, `taxTotal`, `subTotal`, `trackingNumbers`, and the stable `line`
|
|
298
|
+
numbers. Budget hours for multi-year runs and launch them under `nohup`/`tmux`.
|
|
299
|
+
|
|
300
|
+
## Gotchas / known issues
|
|
301
|
+
|
|
302
|
+
- **PHP 7.2 prod constraint.** `library`/`worker` run PHP 7.2 (Amazon Linux 1 EB). Banned syntax:
|
|
303
|
+
arrow functions (`fn() =>`), typed properties (`public int $x`), `??=`, `match()`, numeric
|
|
304
|
+
separators (`1_000`). Lint with `C:\xampp7\php\php.exe -l` before deploying — `C:\xampp8\php`
|
|
305
|
+
(PHP 8.0) is for running probes on the laptop only, **not** for compat checking.
|
|
306
|
+
- Field/relationship availability varies by NetSuite account **and** record type — probe the live
|
|
307
|
+
account before assuming a column exists.
|
|
308
|
+
|
|
309
|
+
## Change history
|
|
310
|
+
|
|
311
|
+
- 2026-06-11 — Initial reference captured from live-probe notes (TRUE-79183): SuiteQL mechanics,
|
|
312
|
+
ET session timezone, transaction/transactionline/previousTransactionLineLink/shippingAddress/item
|
|
313
|
+
schemas, type & status codes, custom-field map, REST GET-only fields, `list*()` cost model and
|
|
314
|
+
measured volumes. (dfranks)
|
|
315
|
+
|
|
316
|
+
## Related docs
|
|
317
|
+
|
|
318
|
+
- `netsuite-suiteql-rest-shim.md` — deep field semantics (signs, `tl.id`==line, `iscogs`, ShipItem
|
|
319
|
+
cost, `createdFrom`) that sit on top of these mechanics.
|
|
320
|
+
- `../../worker/features/forecast2-netsuite-reconciliation.md` — the audit/trueup tooling that
|
|
321
|
+
consumes this API.
|
|
@@ -11,6 +11,7 @@ owners: [dfranks]
|
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
13
13
|
related:
|
|
14
|
+
- netsuite-suiteql-api-reference.md
|
|
14
15
|
- ../architecture.md
|
|
15
16
|
- ../../worker/features/forecast2-netsuite-reconciliation.md
|
|
16
17
|
---
|
|
@@ -113,5 +114,7 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
113
114
|
|
|
114
115
|
## Related docs
|
|
115
116
|
|
|
117
|
+
- `netsuite-suiteql-api-reference.md` — the broad API reference (auth, SuiteQL mechanics, table
|
|
118
|
+
schemas, type/status codes, custom-field map, performance) these semantics sit on top of.
|
|
116
119
|
- `../architecture.md` — Library core architecture.
|
|
117
120
|
- `../../worker/features/forecast2-netsuite-reconciliation.md` — the tooling that applies these.
|
|
@@ -3,4 +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 |
|
|
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, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
@@ -13,10 +13,13 @@ files:
|
|
|
13
13
|
- test/@dave/analyze_netsuite_forecast_diff.php
|
|
14
14
|
- test/@dave/trueup_sales.php
|
|
15
15
|
- test/@dave/trueup_open_orders.php
|
|
16
|
+
- test/@dave/trueup_opportunities.php
|
|
17
|
+
- test/@dave/probe_sales_gap_direct.php
|
|
16
18
|
- worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
|
|
17
19
|
related:
|
|
18
20
|
- ../architecture.md
|
|
19
21
|
- ../../library/features/netsuite-suiteql-rest-shim.md
|
|
22
|
+
- ../../library/features/netsuite-suiteql-api-reference.md
|
|
20
23
|
---
|
|
21
24
|
|
|
22
25
|
## Summary
|
|
@@ -38,6 +41,11 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
38
41
|
match NetSuite for a tranDate range (insert/update/delete per line).
|
|
39
42
|
- `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose]` — same for
|
|
40
43
|
`Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
|
|
44
|
+
- `trueup_opportunities.php --from --to [--prod] [--dry-run]` — same for `Forecast.Opportunities`
|
|
45
|
+
by tranDate range. Pure SuiteQL (no per-id GET), so it's fast: a 2026-YTD prod run reconciled
|
|
46
|
+
~4,000 opps in ~13s to a $0 delta.
|
|
47
|
+
- `probe_sales_gap_direct.php` / `probe_open_orders_gap.php` — read-only NS-vs-FC2 revenue
|
|
48
|
+
breakdowns by type+month using **direct prod mysqli** (demonstrate the sandbox-dev bypass below).
|
|
41
49
|
|
|
42
50
|
## How it works
|
|
43
51
|
|
|
@@ -92,11 +100,36 @@ None — Forecast2 is a single shared dataset.
|
|
|
92
100
|
- **Always dry-run prod first** and confirm the change counts are real drift, not artifacts —
|
|
93
101
|
e.g. 2023-01-03 showed 240 "updates" that were entirely createdFrom backfill + profit float-dust
|
|
94
102
|
(zero real drift); after the fixes it correctly reports `unchanged=240`.
|
|
103
|
+
- **`App_Database` routes *reads* to sandbox-dev on dev laptops.** On the laptop,
|
|
104
|
+
`App_Database::query('db_forecast2')` resolves (via `config.dev-*.ini`) to the **sandbox-dev**
|
|
105
|
+
database, not prod — so any probe using the framework DB layer shows zero rows for
|
|
106
|
+
`Forecast.Sales` even though prod has data. Read probes must bypass the ini and connect directly
|
|
107
|
+
to the **core2 reader** (`reader1.core.database.togahub.com`, creds in `CLAUDE.md` /
|
|
108
|
+
`worker/config.worker.ini`). `reconcile_netsuite_totals.php` and `probe_sales_gap_direct.php`
|
|
109
|
+
already do this. (This is the read-side mirror of the `--prod` write override above.)
|
|
110
|
+
- **`reconcile`'s Open Orders figure is a live snapshot**, not a tranDate-bounded query: it sums
|
|
111
|
+
**all currently-open SOs** regardless of date. Comparing that against the full `OpenOrderItems`
|
|
112
|
+
table shows a large apparent gap driven by records that will never sync, **not** by drift:
|
|
113
|
+
- **Future-dated SOs** (trandate 2027–2029) — NetSuite data-entry errors (wrong year). The
|
|
114
|
+
backfill window doesn't extend into the future, so they never sync. Flag to whoever owns
|
|
115
|
+
NetSuite data; not a code bug.
|
|
116
|
+
- **Pre-2024 legacy open SOs** — ordered before the sync window.
|
|
117
|
+
When a user asks about a date range (e.g. 2026 YTD), **filter the open-orders comparison to that
|
|
118
|
+
trandate window manually** — within the normal window (2024→present) the delta is rounding-only.
|
|
119
|
+
- **Legacy credit-memo wrong-sign rows.** Some `Forecast.Sales` credit-memo/cash-refund rows were
|
|
120
|
+
stored with **flipped signs** by a prior sync version (positive instead of negative), inflating
|
|
121
|
+
revenue. `trueup_sales.php` repairs them: it compares against NS by trandate, detects the sign
|
|
122
|
+
mismatch, and delete+re-inserts with the correct sign. See the shim/reference docs for why a
|
|
123
|
+
uniform `-foreignamount` is correct and an extra per-type `$factor` double-negates.
|
|
95
124
|
- These tools live in `test/@dave/` (developer tooling), but `trueup_open_orders` has been run
|
|
96
125
|
against production. The `Defaults`/checkpoint mechanics of the scheduled sync are separate.
|
|
97
126
|
|
|
98
127
|
## Change history
|
|
99
128
|
|
|
129
|
+
- 2026-06-11 — Documented the `App_Database` sandbox-dev read pitfall (bypass with direct core2
|
|
130
|
+
mysqli), `reconcile`'s open-orders live-snapshot semantics (future-dated + pre-2024 legacy SOs
|
|
131
|
+
explain apparent gaps), the legacy credit-memo wrong-sign repair, and added `trueup_opportunities`
|
|
132
|
+
+ the gap-probe tools. (dfranks)
|
|
100
133
|
- 2026-06-11 — `trueup_sales` rewritten to bulk SuiteQL (`fetchSalesBulk`, ~300× faster);
|
|
101
134
|
`amountDue` + `createdFrom` dropped; float-dust fixed; `--prod` flag added. `reconcile`/`analyze`
|
|
102
135
|
profit queries `NVL(costestimate,0)` — established the $663K gap was an audit artifact and the
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 4 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
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
|
|
@@ -71,7 +71,28 @@ USA and Canada share the parent handler unchanged.
|
|
|
71
71
|
number onto the line). Example seen on SA132739: UPS `1Z8696XA0193326215` actually shipped
|
|
72
72
|
`C40QYUC-1` (SO line 4) but was attached to SO line 1 (`1000555`).
|
|
73
73
|
|
|
74
|
+
- **Over-fulfillment (fulfilled qty > ordered) is the other visible symptom of the same
|
|
75
|
+
scramble.** When a spuriously-linked PO item ships, its ASN/PO-driven ItemFulfillment walks
|
|
76
|
+
the bad bridge link and creates a **phantom `ItemFulfillmentItem` on the wrong SO line**, so
|
|
77
|
+
that line shows more fulfilled than ordered (the same shipment is also fulfilled correctly on
|
|
78
|
+
its real line). Cleanup per order:
|
|
79
|
+
- **Spurious bridge rows:** delete where the SO item's part ≠ the PO item's part, scoped to
|
|
80
|
+
the one `salesOrderId` (`soi.itemId <> vi.itemId` via `VendorItems`). Idempotent.
|
|
81
|
+
- **Phantom IFI:** the duplicate on the over-fulfilled line is the IFI with **no
|
|
82
|
+
`ItemFulfillmentItemUnits`, no `ItemFulfillmentItems_TrackingNumbers`, and nothing
|
|
83
|
+
mirroring it** (`upstreamItemFulfillmentItemId`). The legitimate line fulfillment is the
|
|
84
|
+
sibling IFI that has a real `Unit` and/or a downstream mirror (often in an auto-numbered
|
|
85
|
+
`F#####` IF rather than the SO-named IF). Delete the phantom guarded by
|
|
86
|
+
`id + salesOrderItemId + itemFulfillmentId + quantity`.
|
|
87
|
+
- These are pre-fix orders only (POs created before the 2026-06-11 guard). Cleanup is
|
|
88
|
+
one-time data, written as a dated `dbchanges2/Client_Compass/` file per order.
|
|
89
|
+
- ⚠ Not every over-fulfilled Compass line is this bug — high multipliers (5×–20×) seen on
|
|
90
|
+
other orders (e.g. SA114091 25→125) do not fit the split-PO 2× pattern and are a separate,
|
|
91
|
+
uninvestigated cause.
|
|
92
|
+
|
|
74
93
|
## Change history
|
|
94
|
+
- 2026-06-11 — Documented the over-fulfillment symptom and the phantom-IFI cleanup predicate;
|
|
95
|
+
remediated SA132657 and SA132641 (one dated dbchanges2 file each). (jcardinal)
|
|
75
96
|
- 2026-06-11 — Guarded `postPost` SO↔PO item linking to MR orders only; SA links come solely
|
|
76
97
|
from `prePost`'s `createdFromSalesOrderItem` remap. Fixes spurious cross-part links. (jcardinal)
|
|
77
98
|
|
package/package.json
CHANGED