toga-ai 1.0.218 → 1.0.220
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.
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
8
8
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
9
9
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
10
|
-
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
10
|
+
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
11
11
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
|
|
12
12
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
13
13
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
@@ -11,6 +11,7 @@ owners: [dfranks]
|
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Forecast/SaleImport/SaleImport.php
|
|
13
13
|
- _underscore/Component/Forecast/Db/Db.php
|
|
14
|
+
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
14
15
|
- worker2/Worker/Netsuite/Invoice.php
|
|
15
16
|
- worker2/Worker/Netsuite/CashSale.php
|
|
16
17
|
- worker2/Worker/Netsuite/CreditMemo.php
|
|
@@ -24,6 +25,9 @@ files:
|
|
|
24
25
|
- test/@dave/test_creditmemo_lifecycle.php
|
|
25
26
|
- test/@dave/test_cashsale_lifecycle.php
|
|
26
27
|
- test/@dave/test_cashrefund_lifecycle.php
|
|
28
|
+
- test/@dave/test_fetchrecord_routes.php
|
|
29
|
+
- test/@dave/verify_je_classification.php
|
|
30
|
+
- test/@dave/probe_je_accounts.php
|
|
27
31
|
- test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
|
|
28
32
|
- test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js
|
|
29
33
|
related:
|
|
@@ -71,7 +75,7 @@ Engine public surface: `sync(int $internalId, string $type)` and
|
|
|
71
75
|
|
|
72
76
|
## How it works
|
|
73
77
|
Per-record flow in `sync`:
|
|
74
|
-
1. REST GET the record (`expandSubResources=true`) via `
|
|
78
|
+
1. REST GET the record (`expandSubResources=true`) via `_Component_Api_Netsuite::fetchRecord`.
|
|
75
79
|
2. **Per-type status gate** → if excluded, call `removeAll` and stop. Exclusions: invoice
|
|
76
80
|
`Voided`/`Rejected`; cashSale `Unapproved Payment`; creditMemo `Voided`; cashRefund has
|
|
77
81
|
no gate. **A cashSale created via REST lands in status `Deposited`** (NOT the excluded
|
|
@@ -88,14 +92,32 @@ Per-record flow in `sync`:
|
|
|
88
92
|
`removeAll(internalId)` deletes all rows for that transaction (used by delete events and by
|
|
89
93
|
the status gate).
|
|
90
94
|
|
|
91
|
-
### Shared helpers on `_Component_Forecast_Db`
|
|
92
|
-
-
|
|
93
|
-
|
|
95
|
+
### Shared NS helpers — `fetchRecord` lives on `_Component_Api_Netsuite`, the rest on `_Component_Forecast_Db`
|
|
96
|
+
- **`_Component_Api_Netsuite::fetchRecord(route, label)`** — NS REST GET + decode. **Moved
|
|
97
|
+
here** (beside `createRecord`) from `_Component_Forecast_Db` — it is a generic REST read,
|
|
98
|
+
not Forecast-specific, so it belongs with the other REST primitives. **Verify the class
|
|
99
|
+
before relying on it** (it was relocated this session). The **7 callers** were all
|
|
100
|
+
reprefixed to `_Component_Api_Netsuite::fetchRecord`: `SaleImport.php` ×2, `Opportunity.php`
|
|
101
|
+
×3, `SalesOrder.php` ×2 (static git-grep: zero stale `_Component_Forecast_Db::fetchRecord`
|
|
102
|
+
refs, all 7 resolve, lint clean; runtime-verified across all 5 SaleImport types +
|
|
103
|
+
opportunity/employee/salesOrder routes via `test_fetchrecord_routes.php`).
|
|
104
|
+
- **Still on `_Component_Forecast_Db`** (Forecast-specific): `collectItemLines(record)` +
|
|
105
|
+
`nextPageRoute(links)` (`rel=next` sublist pagination), `lookupId`, `toSqlDate`.
|
|
94
106
|
|
|
95
107
|
`Opportunity.php` and `SalesOrder.php` were refactored onto these shared versions; their
|
|
96
108
|
private `fetchRecord`/`collectAllItemLines`/`nextPageRoute` copies (and SalesOrder's
|
|
97
109
|
orphaned `MAX_SUBLIST_PAGES` const) were deleted.
|
|
98
110
|
|
|
111
|
+
> **Process gotcha — enumerate callers of these shared NS helpers with `git grep`, not the
|
|
112
|
+
> fuzzy/agent search.** Any move/rename of `_Component_Api_Netsuite::createRecord` /
|
|
113
|
+
> `fetchRecord` (or the other shared NS helpers) must build its caller inventory with
|
|
114
|
+
> `git grep -nF` **per repo** — the fuzzy search tool silently **missed**
|
|
115
|
+
> `worker2/Worker/Netsuite/SalesOrder.php:925` (`createRecord`, the SalesOrder SOAP→REST
|
|
116
|
+
> outbound push — a real **production** caller) on a first pass, producing a wrong "zero
|
|
117
|
+
> callers / test-only" conclusion and a broken refactor that had to be reverted. Search
|
|
118
|
+
> case-insensitively for `::method`, `function method`, and string/callable forms; verify
|
|
119
|
+
> **zero stale refs** after editing.
|
|
120
|
+
|
|
99
121
|
## Sign convention (the load-bearing decision)
|
|
100
122
|
The **raw REST record** returns each line `amount`/`costEstimate` **POSITIVE for all four
|
|
101
123
|
types** (verified by live probe 2026-06-24 against NS account 1095849: creditMemo lines
|
|
@@ -147,11 +169,25 @@ item-line upsert/reconcile machinery (`syncLines`/`guardedInsert`/`deleteRows`).
|
|
|
147
169
|
- Lines hitting a **revenue** or **cost** GL account are grouped by **(salesRep, item)**:
|
|
148
170
|
`revenue = Σ(credit − debit)` over revenue lines, `cost = Σ(debit − credit)` over cost
|
|
149
171
|
lines, `profit = revenue − cost`.
|
|
150
|
-
- **Account classification is
|
|
151
|
-
|
|
152
|
-
`
|
|
153
|
-
`
|
|
154
|
-
|
|
172
|
+
- **Account classification is an EXACT account-NUMBER allowlist** (sales-team-defined; this
|
|
173
|
+
*replaced* the old provisional `accttype`-bucket rule). Constants in
|
|
174
|
+
`_Component_Forecast_SaleImport`: `JE_REVENUE_ACCOUNT_NUMBERS = ['41100','41300','41500']`,
|
|
175
|
+
`JE_COST_ACCOUNT_NUMBERS = ['51100','51200']`. Any line whose account number is in neither
|
|
176
|
+
list is **ignored**. The helper `accountNumbers(accountIds)` (which **replaced**
|
|
177
|
+
`accountTypes()`) resolves each line account's `acctnumber` via SuiteQL
|
|
178
|
+
`SELECT id, acctnumber FROM account WHERE id IN (...)`, cached per run; matching is
|
|
179
|
+
**string membership of `acctnumber`**, not `accttype`.
|
|
180
|
+
- **Sub-accounts are EXCLUDED by design — exact match, never prefix.** This chart of accounts
|
|
181
|
+
has real traps that both a `accttype` rule AND a prefix match would mis-bucket: under **41300**
|
|
182
|
+
"Service Revenue" sit `41300.01` (Service Revenue-Agent, `accttype` Income), `41300.02`/`.03`
|
|
183
|
+
(Service Cost-Agent, `accttype` **Bank**), and `41300.04` (Service **Costs**-Agent, `accttype`
|
|
184
|
+
**Income** despite the cost name); **51200** has `51200.01` (COGS Service-Agent). Exact-number
|
|
185
|
+
matching sidesteps all of them.
|
|
186
|
+
- **Resolved account map** (number → internalId / name): `41100→245` Product Revenue,
|
|
187
|
+
`41300→339` Service Revenue, `41500→355` Pre-Sales Consulting; `51100→248` COGS Product,
|
|
188
|
+
`51200→249` COGS Service. Read-only resolver tool: `test/@dave/probe_je_accounts.php`.
|
|
189
|
+
- **Likely future one-line tweak:** if the Agent sub-accounts should count, add `41300.01` to
|
|
190
|
+
the revenue list and `51200.01` to the cost list.
|
|
155
191
|
- Group key uses `json_encode([salesRep, item])` (not a string-join) to stay collision-safe
|
|
156
192
|
once the fields go live — an empty-string separator would let `"5|" + null` collide.
|
|
157
193
|
|
|
@@ -218,28 +254,116 @@ events as specific **subtypes** (`inventoryItem`, `nonInventoryResaleItem`, `kit
|
|
|
218
254
|
would be needed only for a future real-time item webhook, **not** for this Sales importer
|
|
219
255
|
(which keeps items fresh via the hourly item pull cron plus the inline self-heal).
|
|
220
256
|
|
|
221
|
-
##
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
## E2E testing playbook + harness inventory (run this cold)
|
|
258
|
+
This section is the reusable procedure for verifying the NetSuite → `Forecast.Sales` path
|
|
259
|
+
end-to-end against a **local** Forecast mirror. It is written to be runnable in a future
|
|
260
|
+
session with no prior context.
|
|
261
|
+
|
|
262
|
+
### Harness inventory (`test/@dave/`, all PHP)
|
|
263
|
+
All harnesses boot worker2/_underscore by `chdir('C:/Users/dfranks/www/worker2'); require
|
|
264
|
+
'index.php';` and drive the live `_underscore` REST client (`_Component_Api_Netsuite`).
|
|
265
|
+
**Run them with xampp8 PHP 8.0:** `C:\xampp8\php\php.exe test/@dave/<harness>.php <action> …`
|
|
266
|
+
(local `_underscore` floor is PHP 8.0.0).
|
|
267
|
+
|
|
268
|
+
| Harness | Type | Actions |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `test_invoice_lifecycle.php` | invoice | `check`, `create`, `get <id>`, `sync <id>`, `update <id> <date>`, `delete <id>`, `recent` (plus `amq`/`deployments`/`locations` SuiteQL probes) |
|
|
271
|
+
| `test_creditmemo_lifecycle.php` | creditMemo | same per-type action set |
|
|
272
|
+
| `test_cashsale_lifecycle.php` | cashSale | same per-type action set |
|
|
273
|
+
| `test_cashrefund_lifecycle.php` | cashRefund | same per-type action set |
|
|
274
|
+
| `test_je_lifecycle.php` | journalEntry | per-type set **plus** `sync` (`syncJournalEntry`) and `remove` (`removeAllJournalEntry`, direct); `create` makes a **balanced reversing JE** |
|
|
275
|
+
| `test_fetchrecord_routes.php` | **read-only probe** | calls `_Component_Api_Netsuite::fetchRecord` against a real opportunity/employee/salesOrder id — proves that method per route, **no side effects** |
|
|
276
|
+
|
|
277
|
+
**Per-type actions explained:**
|
|
278
|
+
- `check` — boot + OAuth only, **no writes** (smoke test connectivity/creds).
|
|
279
|
+
- `create` — create a real record in NetSuite (tiny `$0`/`$0.01`).
|
|
280
|
+
- `get <id>` — REST GET the record.
|
|
281
|
+
- `sync <id>` — **DIRECT** `_Component_Forecast_SaleImport::sync()` (or `syncJournalEntry()`)
|
|
282
|
+
against NetSuite reads, **local, no webhook**. This is the local-only execution path.
|
|
283
|
+
- `update <id> <date>` — PATCH `tranDate` (a tracked field) to exercise edit→update.
|
|
284
|
+
- `delete <id>` — delete the record in NetSuite.
|
|
285
|
+
- `recent` — list recent records of that type.
|
|
286
|
+
|
|
287
|
+
Each harness carries its **own** local create helper that POSTs via `_ApiRequest` and parses
|
|
288
|
+
the `Location` header with a **correct `~`-delimited regex** — a deliberate workaround for the
|
|
289
|
+
live `_Component_Api_Netsuite::createRecord()` `#`-delimiter bug (still unfixed; see the
|
|
290
|
+
[REST client doc](./netsuite-rest-client.md)). The framework is **not** edited to test.
|
|
291
|
+
|
|
292
|
+
### The 6-step lifecycle pattern (every type)
|
|
293
|
+
1. **create** → a real NetSuite record.
|
|
294
|
+
2. **verify** the local `forecast.Sales` row(s) appeared.
|
|
295
|
+
3. **update** the `tranDate`.
|
|
296
|
+
4. **verify** the update landed (tracked-column change-detection).
|
|
297
|
+
5. **delete** the record.
|
|
298
|
+
6. **verify** the local row is gone (`removeAll`).
|
|
299
|
+
|
|
300
|
+
> **Scope of the update step (what it does and does NOT cover):** the update step changes
|
|
301
|
+
> **only `tranDate`** — a tracked column, so it forces a real change-detection UPDATE while
|
|
302
|
+
> the amount stays unchanged. It does **NOT** exercise updates to revenue / customer /
|
|
303
|
+
> salesRep / item, nor line add/remove. A future tester verifying those must extend the
|
|
304
|
+
> harness; the current update-step assertion only proves the tracked-column UPDATE path.
|
|
305
|
+
|
|
306
|
+
Poll the **local** Forecast DB between steps:
|
|
307
|
+
`C:\xampp8\mysql\bin\mysql.exe -u root` → db **`forecast`**, table **`Sales`** (filter on the
|
|
308
|
+
test internalId, or customerId `2` / itemId `25`).
|
|
309
|
+
|
|
310
|
+
### Two execution modes
|
|
311
|
+
- **(a) WEBHOOK path** — NS create → AMQ enqueuer → ngrok → local worker2 → engine. Requires:
|
|
312
|
+
the **dev** AMQ enqueuer **deployed on each record type under test** with its **AUDIENCE
|
|
313
|
+
including the REST/M2M integration user** (Execute-As-Role does **not** control triggering —
|
|
314
|
+
AUDIENCE does); the **prod** enqueuer left **UNDEPLOYED** for isolation; and **ngrok up** at
|
|
315
|
+
the enqueuer's `DEV_OVERRIDE` endpoint. Exercises the real inbound path.
|
|
316
|
+
- **(b) LOCAL-ONLY path** (use when deployments are off / you can't touch NetSuite scripts) —
|
|
317
|
+
skip the webhook entirely: call the harness **`sync`** action, which runs
|
|
318
|
+
`_Component_Forecast_SaleImport::sync()` / `syncJournalEntry()` **directly** against NetSuite
|
|
319
|
+
reads. Exercises the same import + `fetchRecord` with **no enqueuer/ngrok dependency**.
|
|
320
|
+
|
|
321
|
+
### Fixtures
|
|
322
|
+
- Customer **58** ("8 Test Company") → local `forecast.Customers` id **2**.
|
|
323
|
+
- Item **103741** ("Test Other Charge for Sale") → local `forecast.Items` id **25**.
|
|
324
|
+
- Location **5** ("Main") — required as a **header-level** field on creditMemo/cashSale/cashRefund
|
|
325
|
+
and **line-level** on invoice.
|
|
326
|
+
- **JE fixture:** subsidiary **1**; accounts **245** (Product Revenue, acctnumber **41100**),
|
|
327
|
+
**248** (COGS Product, **51100**), **215** (Clearing - CSS — ignored, neither revenue nor
|
|
328
|
+
cost); set a `reversalDate` so NetSuite auto-creates the reversal. Classification is now by
|
|
329
|
+
**account number** (allowlist), so any clearing/balance line lands outside both lists and is
|
|
330
|
+
ignored automatically.
|
|
331
|
+
- **Discriminating classification test — `test/@dave/verify_je_classification.php`:** posts one
|
|
332
|
+
balanced JE `+10/+20/+30` to `41100/41300/41500`, `−5/−7` to `51100/51200`, **plus `+100` to
|
|
333
|
+
the excluded sub-account `41300.01`**, plus a clearing balance line → local `Forecast.Sales`
|
|
334
|
+
aggregates to **revenue=60.00, profit=48.00** (NOT 160 — proving `41300.01` is excluded and the
|
|
335
|
+
exact-number rule is in force).
|
|
336
|
+
|
|
337
|
+
### Expected local revenue signs / row outcomes
|
|
338
|
+
- **invoice** → `+` (a **$0** invoice still writes **one** row, revenue `0.00` — the engine does
|
|
339
|
+
not skip a $0 line).
|
|
340
|
+
- **cashSale** → `+`; status must be **`Deposited`** (a REST-created cash sale lands Deposited,
|
|
341
|
+
**not** the excluded `Unapproved Payment`).
|
|
342
|
+
- **creditMemo** → `−`.
|
|
343
|
+
- **cashRefund** → `−`.
|
|
344
|
+
- **JE** → `revenue = Σ(credit − debit)` over revenue-acct lines, `cost = Σ(debit − credit)`
|
|
345
|
+
over cost-acct lines, `profit = revenue − cost`. The **reversal** lands as the negation keyed
|
|
346
|
+
by its **own** internalId with `createdFromNetsuiteTransactionInternalId = the original`.
|
|
347
|
+
|
|
348
|
+
### Timing / verification notes
|
|
349
|
+
- The create webhook can lag **~20s** before the row appears (read-after-write / processing) —
|
|
350
|
+
wait before asserting.
|
|
351
|
+
- On the debug webhook path there are **no** local app logs — confirm inbound arrival at the
|
|
352
|
+
ngrok inspector `http://127.0.0.1:4040/api/requests/http` (body is **base64**; decode before
|
|
353
|
+
grepping; buffer is ephemeral). See Gotchas.
|
|
354
|
+
|
|
355
|
+
### Prod hygiene (critical — tests write to the single prod NetSuite account)
|
|
356
|
+
All harnesses create **real** tiny (`$0`/`$0.01`) records in the **single prod NetSuite
|
|
357
|
+
account (1095849)**. The legacy 5-min pull cron
|
|
358
|
+
(`worker/crons/toga2/forecast2/import_sales.php`) can pull a briefly-alive test record into
|
|
359
|
+
**prod** `Forecast.Sales` **independent of the webhook code**. After each test batch:
|
|
360
|
+
1. Query the **prod reader** (`reader1.core.database.togahub.com`, `Forecast.Sales`) for the
|
|
361
|
+
test internalIds.
|
|
362
|
+
2. **Guarded-DELETE exact matches** (customerId `2` / itemId `25` / revenue `±0.01`) via the
|
|
363
|
+
**prod writer**.
|
|
364
|
+
Keep **create→delete windows short** so most records slip between cron runs; or pause
|
|
365
|
+
`import_sales` during a batch. (The daily discrepancy-fix also cleans stragglers since the
|
|
366
|
+
record is deleted in NetSuite.)
|
|
243
367
|
|
|
244
368
|
## Gotchas / known issues
|
|
245
369
|
- **Testing locally pollutes PROD unless you UNDEPLOY the prod AMQ enqueuer first.** Each sale
|
|
@@ -287,6 +411,29 @@ Actions: `check | create | get | update | delete | sync | recent | amq | deploym
|
|
|
287
411
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
288
412
|
|
|
289
413
|
## Change history
|
|
414
|
+
- 2026-06-26 — **Changed JE revenue/cost classification from `accttype` buckets to an EXACT
|
|
415
|
+
account-NUMBER allowlist** (sales-team-defined): `JE_REVENUE_ACCOUNT_NUMBERS =
|
|
416
|
+
{41100,41300,41500}`, `JE_COST_ACCOUNT_NUMBERS = {51100,51200}`; everything else ignored.
|
|
417
|
+
`accountNumbers()` (replaced `accountTypes()`) resolves each account's `acctnumber` via
|
|
418
|
+
SuiteQL, cached per run; match is exact-string, **never prefix** — sub-accounts are excluded
|
|
419
|
+
by design to dodge real chart-of-accounts traps (`41300.04` "Service Costs-Agent" is
|
|
420
|
+
`accttype` Income; `41300.02/.03` are `accttype` Bank). Resolved map: 41100→245, 41300→339,
|
|
421
|
+
41500→355, 51100→248, 51200→249. Verified with a discriminating balanced-JE test
|
|
422
|
+
(`test/@dave/verify_je_classification.php` → revenue=60.00/profit=48.00, proving `41300.01`
|
|
423
|
+
excluded); read-only resolver `test/@dave/probe_je_accounts.php`. Future one-liner: add
|
|
424
|
+
`41300.01`/`51200.01` if Agent sub-accounts should count. Also clarified the e2e harness
|
|
425
|
+
update step exercises **only `tranDate`** (a tracked column) — not revenue/customer/salesRep/
|
|
426
|
+
item or line add/remove. (dfranks)
|
|
427
|
+
- 2026-06-26 — **Documented the full e2e testing playbook + harness inventory to run cold**
|
|
428
|
+
(6-step create→verify→update→verify→delete→verify lifecycle; per-type harness action sets;
|
|
429
|
+
the two execution modes — WEBHOOK vs LOCAL-ONLY `sync` — fixtures, expected signs, prod
|
|
430
|
+
hygiene). Added the read-only `test/@dave/test_fetchrecord_routes.php` probe. Recorded that
|
|
431
|
+
**`fetchRecord` moved from `_Component_Forecast_Db` to `_Component_Api_Netsuite`** (beside
|
|
432
|
+
`createRecord`; 7 callers reprefixed — SaleImport ×2, Opportunity ×3, SalesOrder ×2;
|
|
433
|
+
verify-the-class-before-relying). Added the **`git grep` caller-enumeration gotcha**: build
|
|
434
|
+
caller inventories for the shared NS helpers with `git grep -nF` per repo, not the fuzzy
|
|
435
|
+
search, which silently missed the production `SalesOrder.php` `createRecord` caller and caused
|
|
436
|
+
a reverted refactor. (dfranks)
|
|
290
437
|
- 2026-06-26 — **Hardened + verified the full webhook path for all four sale types end-to-end**
|
|
291
438
|
(create→insert, edit→update, delete→removeAll) with sign conventions reconfirmed (invoice/cashSale
|
|
292
439
|
+, creditMemo/cashRefund −). Recorded durable gotchas: a UE enqueuer fires for a REST/M2M user only
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-26
|
|
10
10
|
owners: ["dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
@@ -64,6 +64,24 @@ through `_ApiRequest` directly (mirroring `send()`'s auth/endpoint/header setup)
|
|
|
64
64
|
> value carried in the thrown exception message (the lifecycle harness ships its own correct
|
|
65
65
|
> `~`-delimited parse to sidestep this without editing the framework). Confirmed live 2026-06-26.
|
|
66
66
|
|
|
67
|
+
### Read — `fetchRecord(string $route, string $label): array`
|
|
68
|
+
|
|
69
|
+
A generic NS REST **GET + decode** primitive. **Moved here from `_Component_Forecast_Db`**
|
|
70
|
+
(2026-06-26) so it sits beside `createRecord` with the other REST primitives — it is not
|
|
71
|
+
Forecast-specific. Its **7 callers** were reprefixed to `_Component_Api_Netsuite::fetchRecord`:
|
|
72
|
+
`SaleImport.php` ×2, `Opportunity.php` ×3, `SalesOrder.php` ×2 (static git-grep: zero stale
|
|
73
|
+
`_Component_Forecast_Db::fetchRecord` refs; runtime-verified across all 5 SaleImport types +
|
|
74
|
+
opportunity/employee/salesOrder routes via `test/@dave/test_fetchrecord_routes.php`).
|
|
75
|
+
**Verify the class before relying on it** — it was just relocated.
|
|
76
|
+
|
|
77
|
+
> **Enumerate callers of this class's shared helpers with `git grep`, not the fuzzy/agent
|
|
78
|
+
> search.** When moving/renaming `createRecord` / `fetchRecord`, build the caller inventory with
|
|
79
|
+
> `git grep -nF` **per repo** — the fuzzy search silently **missed**
|
|
80
|
+
> `worker2/Worker/Netsuite/SalesOrder.php:925` (`createRecord`, the SalesOrder SOAP→REST
|
|
81
|
+
> outbound push — a real **production** caller), producing a wrong "zero callers / test-only"
|
|
82
|
+
> conclusion and a broken refactor that had to be reverted. Search case-insensitively for
|
|
83
|
+
> `::method`, `function method`, and string/callable forms; confirm zero stale refs afterward.
|
|
84
|
+
|
|
67
85
|
### Update — reuse `send('PATCH', $route, $body)`
|
|
68
86
|
|
|
69
87
|
Updates do **not** need a new helper. A NetSuite record PATCH returns 204 with no body, and
|
|
@@ -103,6 +121,12 @@ doc.)
|
|
|
103
121
|
|
|
104
122
|
## Change history
|
|
105
123
|
|
|
124
|
+
- 2026-06-26 — **`fetchRecord` (generic NS REST GET + decode) moved onto this class** from
|
|
125
|
+
`_Component_Forecast_Db`, beside `createRecord`; 7 callers reprefixed (SaleImport ×2,
|
|
126
|
+
Opportunity ×3, SalesOrder ×2), git-grep clean + runtime-verified. Recorded the
|
|
127
|
+
caller-enumeration lesson: use `git grep -nF` per repo (not the fuzzy search, which missed the
|
|
128
|
+
production `SalesOrder.php` `createRecord` caller and caused a reverted refactor) when
|
|
129
|
+
moving/renaming these shared NS helpers. (dfranks)
|
|
106
130
|
- 2026-06-25 — **Recorded a live bug in `createRecord()`'s trailing-id regex.** The pattern
|
|
107
131
|
`'#/(\d+)(?:[?#]|$)#'` uses `#` as both the PCRE delimiter and a class member, so it always throws
|
|
108
132
|
`Unknown modifier ']'` — *after* the record is created, breaking the outbound create/push path.
|
package/package.json
CHANGED