toga-ai 1.0.209 → 1.0.210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
7
7
  | [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 |
8
8
  | [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 |
9
- | [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/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php |
9
+ | [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/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, test/@dave/test_invoice_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
10
  | [_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 |
11
11
  | [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 |
12
12
  | [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 |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-25
9
+ updated: 2026-06-26
10
10
  owners: [dfranks]
11
11
  files:
12
12
  - _underscore/Component/Forecast/SaleImport/SaleImport.php
@@ -17,9 +17,14 @@ files:
17
17
  - worker2/Worker/Netsuite/CashRefund.php
18
18
  - worker2/Worker/Netsuite/Opportunity.php
19
19
  - worker2/Worker/Netsuite/SalesOrder.php
20
+ - test/@dave/test_invoice_lifecycle.php
21
+ - test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
22
+ - test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js
20
23
  related:
21
24
  - ../architecture.md
22
25
  - ../../worker2/architecture.md
26
+ - ../../worker2/features/netsuite-salesorder-open-orders-sync.md
27
+ - ./netsuite-rest-client.md
23
28
  ---
24
29
 
25
30
  ## Summary
@@ -144,7 +149,51 @@ events as specific **subtypes** (`inventoryItem`, `nonInventoryResaleItem`, `kit
144
149
  would be needed only for a future real-time item webhook, **not** for this Sales importer
145
150
  (which keeps items fresh via the hourly item pull cron plus the inline self-heal).
146
151
 
152
+ ## Local testing harness (`test/@dave/test_invoice_lifecycle.php`)
153
+ A CLI that boots worker2/_underscore (chdir to `worker2/` then `require index.php`) and drives a
154
+ **full NetSuite invoice lifecycle** through the live `_underscore` REST client
155
+ (`_Component_Api_Netsuite`) to exercise this importer end-to-end against a local Forecast mirror.
156
+ Actions: `check | create | get | update | delete | sync | recent | amq | deployments | locations`.
157
+
158
+ - **Fixture:** customer **58** ("8 Test Company") + item **103741** ("Test Other Charge for Sale",
159
+ maps to local `Forecast.Items` id 25), **rate 0** → a **$0 invoice that still yields one Sales
160
+ row** (the engine does **not** skip a $0 line — revenue `0.00` is written; same as the cron's
161
+ Sales section). Confirms create→insert, edit→update (tracked-column change-detection), and
162
+ delete→`removeAll` all land in local `Forecast.Sales`.
163
+ - It carries its **own** `createInvoiceRecord()` that POSTs via `_ApiRequest` and parses the
164
+ `Location` header with a **correct** regex (delimiter `~`) — a deliberate workaround for the live
165
+ `_Component_Api_Netsuite::createRecord()` `#`-delimiter bug (see the
166
+ [REST client doc](./netsuite-rest-client.md)); the framework was **not** edited this session.
167
+ - `amq` action SuiteQLs the AMQ custom record; `deployments` SuiteQLs `scriptdeployment`+`script`
168
+ to show which enqueuer fires per record type (used to find the dual-deployment trap below).
169
+
147
170
  ## Gotchas / known issues
171
+ - **Testing locally pollutes PROD unless you UNDEPLOY the prod AMQ enqueuer first.** Each sale
172
+ transaction type has **TWO** NetSuite UE enqueuer deployments: **"AMQ — Enqueuer"**
173
+ (`customscript_ue_amq_enqueue`, **RELEASED** → posts to **prod** `webhook.togahub.com`) and
174
+ **"AMQ development script"** (`customscript2619` → ngrok via `DEV_OVERRIDE`/`debugWrap`). A
175
+ **RELEASED** deployment fires for **all** actors **including the REST/M2M integration user**, so a
176
+ REST-driven test invoice hits **prod too**. Observed first run: test-invoice edits flowed to prod
177
+ and a $0 row was written to **prod** `Forecast.Sales` — by the **legacy 5-min pull cron**, NOT the
178
+ webhook (prod worker2 has no invoice handler deployed yet, so prod `Netsuite/Invoice/*` jobs
179
+ watchdog-timeout at 300s, `isSuccess=0`). **To isolate a local test, undeploy the prod enqueuer**
180
+ for the record types under test (invoice/cashSale/cashRefund/creditMemo/journalEntry); the second
181
+ full create→update→delete run was then cleanly isolated (zero prod `Logs.Webhook`/`Forecast.Sales`
182
+ for the test id). The **dev** enqueuer's `debugWrap` envelope
183
+ `{action,parameters:{payload,headers}}` is required because a local worker2 has no
184
+ `WebhookIngestion` Lambda to wrap the raw body.
185
+ - **No local app logs for debug-path webhooks — verify arrival via the ngrok inspector, and decode
186
+ the body.** The legacy `{action,parameters}` debug path writes **no** `WorkerJobs`/`Logs.Webhook`
187
+ locally. Confirm inbound arrival at the ngrok request inspector
188
+ `http://127.0.0.1:4040/api/requests/http` — but its **raw body is base64**, so decode the body
189
+ (after the `\r\n\r\n`) before grepping for `internalId`/`eventType`. The inspector buffer is
190
+ **ephemeral** (rotates/clears) — watch it live while triggering.
191
+ - **Sales create webhook races NetSuite REST read-after-write lag** — the handler GETs the invoice
192
+ before its line sublist materializes → **0 lines → 0 rows**. Same gotcha as the SalesOrder
193
+ open-orders sync (see that doc's read-after-write-lag gotcha); it self-heals on the next edit
194
+ webhook or the daily discrepancy-fix.
195
+ - **Local _underscore boots on PHP 8.0.30 (xampp8)** — the floor enforced in `_underscore.php` is
196
+ **8.0.0**, not 8.1.
148
197
  - **`_Component_*` classes must live under the `_underscore` framework root, never a project
149
198
  repo.** The autoloader rejects a `_Component_*`/`_Model_*` loaded from a project path
150
199
  (e.g. `./` under worker2) with "namespace … has not been defined" — at **class-load
@@ -154,6 +203,19 @@ would be needed only for a future real-time item webhook, **not** for this Sales
154
203
  - The cron's sign handling is not portable here — see Sign convention.
155
204
 
156
205
  ## Change history
206
+ - 2026-06-26 — **Verified the full create→update→delete webhook lifecycle end-to-end on a local box**
207
+ (ngrok tunnel → local worker2 → local `Forecast.Sales`), driving a real **$0** NetSuite invoice
208
+ through create→insert, edit→update (tracked-column change-detection), delete→`removeAll`; confirmed
209
+ a $0 line is **not** skipped (one revenue-`0.00` row). Built the reusable harness
210
+ `test/@dave/test_invoice_lifecycle.php` (boots worker2/_underscore, drives the lifecycle via
211
+ `_Component_Api_Netsuite`; `amq`/`deployments` SuiteQL actions). Recorded durable testing gotchas:
212
+ the **dual AMQ enqueuer** RELEASED-fires-for-the-REST-user prod-pollution trap (undeploy the prod
213
+ enqueuer to isolate; prod's $0 row came from the legacy 5-min pull cron, not a prod webhook —
214
+ prod has no invoice handler so its `Netsuite/Invoice/*` jobs watchdog-timeout `isSuccess=0`); no
215
+ local app logs on the debug `{action,parameters}` path (verify via the ngrok inspector at
216
+ `:4040`, body is base64, buffer ephemeral); the Sales create webhook shares the SalesOrder
217
+ read-after-write line-sublist race; and local _underscore's PHP floor is 8.0.0. No production code
218
+ change. (dfranks)
157
219
  - 2026-06-25 — **Verified `sync()` end-to-end across all four sale types** (prod NS reads, local Forecast
158
220
  writes; 40+ records spanning invoice/cashSale/creditMemo/cashRefund): revenue reconciled **to the penny**
159
221
  against an independent SuiteQL `SUM(-foreignamount)` oracle on every record; the uniform sign factor held
@@ -57,10 +57,12 @@ through `_ApiRequest` directly (mirroring `send()`'s auth/endpoint/header setup)
57
57
  > reads the class-`#` as a premature closing delimiter and the whole pattern **always throws
58
58
  > `preg_match(): Unknown modifier ']'`**. This fires *after* the record is already created, so the
59
59
  > create succeeds in NetSuite but `createRecord()` raises and the caller never gets the new id —
60
- > it breaks the entire outbound create/push path. **Fix direction:** change the delimiter so `#`
60
+ > it breaks the entire outbound create/push path **for any record type** (observed on `journalentry`
61
+ > and again on `invoice` create). **Fix direction:** change the delimiter so `#`
61
62
  > isn't both delimiter and class member, e.g. `~/(\d+)(?:[?#]|$)~`. Interim workaround used in
62
- > probes: recover the new id by regex-parsing the thrown exception message (the `Location` value
63
- > is in it). Confirmed live 2026-06-25.
63
+ > probes and in `test/@dave/test_invoice_lifecycle.php`: parse the new internalId off the `Location`
64
+ > value carried in the thrown exception message (the lifecycle harness ships its own correct
65
+ > `~`-delimited parse to sidestep this without editing the framework). Confirmed live 2026-06-26.
64
66
 
65
67
  ### Update — reuse `send('PATCH', $route, $body)`
66
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.209",
3
+ "version": "1.0.210",
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",