toga-ai 1.0.213 → 1.0.214

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, 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 |
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/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/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 |
@@ -15,9 +15,12 @@ files:
15
15
  - worker2/Worker/Netsuite/CashSale.php
16
16
  - worker2/Worker/Netsuite/CreditMemo.php
17
17
  - worker2/Worker/Netsuite/CashRefund.php
18
+ - worker2/Worker/Netsuite/JournalEntry.php
18
19
  - worker2/Worker/Netsuite/Opportunity.php
19
20
  - worker2/Worker/Netsuite/SalesOrder.php
21
+ - dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql
20
22
  - test/@dave/test_invoice_lifecycle.php
23
+ - test/@dave/test_je_lifecycle.php
21
24
  - test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
22
25
  - test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js
23
26
  related:
@@ -30,13 +33,14 @@ related:
30
33
  ## Summary
31
34
  Real-time importer that takes a NetSuite **sale** record and writes its lines into
32
35
  `Forecast.Sales` (the Forecast2 revenue table). Covers the four NetSuite sale record
33
- types: **invoice, cashSale, creditMemo, cashRefund**. It is the webhook-driven replacement
34
- for the SALES section of the legacy 5-minute pull cron
36
+ types: **invoice, cashSale, creditMemo, cashRefund**, plus **journalEntry** (GL
37
+ revenue/cost adjustments posted directly as journal entries — see *JournalEntry import*).
38
+ It is the webhook-driven replacement for the SALES section of the legacy 5-minute pull cron
35
39
  (`worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php`), and mirrors the
36
40
  already-shipped SalesOrder (open-orders) and Opportunity webhook handlers.
37
41
 
38
- The shared engine is `_Component_Forecast_SaleImport`; four thin worker2 handlers
39
- (`_Worker_Netsuite_{Invoice,CashSale,CreditMemo,CashRefund}`) just delegate to it.
42
+ The shared engine is `_Component_Forecast_SaleImport`; thin worker2 handlers
43
+ (`_Worker_Netsuite_{Invoice,CashSale,CreditMemo,CashRefund,JournalEntry}`) just delegate to it.
40
44
 
41
45
  **Critical, non-obvious facts (read before touching this):**
42
46
  - **Sign convention differs from the cron** — the raw REST record is all-positive, so the
@@ -50,7 +54,9 @@ The shared engine is `_Component_Forecast_SaleImport`; four thin worker2 handler
50
54
  |---|---|
51
55
  | `_underscore/Component/Forecast/SaleImport/SaleImport.php` | the engine (shared body for all four types) |
52
56
  | `_underscore/Component/Forecast/Db/Db.php` | shared NS-REST + sublist-pagination helpers |
53
- | `worker2/Worker/Netsuite/Invoice.php` etc. | four thin handlers; `post`/`put` → `sync`, `delete` → `removeAll` |
57
+ | `worker2/Worker/Netsuite/Invoice.php` etc. | four sale-type thin handlers; `post`/`put` → `sync`, `delete` → `removeAll` |
58
+ | `worker2/Worker/Netsuite/JournalEntry.php` | thin JE handler; `post`/`put` → `syncJournalEntry`, `delete` → `removeAllJournalEntry` |
59
+ | `test/@dave/test_je_lifecycle.php` | JE lifecycle harness (direct-engine create/update/delete) |
54
60
 
55
61
  Each handler is `abstract class _Worker_Netsuite_<Type> implements
56
62
  _Interface_Static_Webhook_Netsuite`. The router (`_Worker_NetSuite::Webhook`) PascalCases
@@ -117,9 +123,56 @@ uses) and then writes the line — instead of the cron's silent skip. Failure se
117
123
  the job for retry.
118
124
  - `itemGroup` items carry no line revenue and are skipped.
119
125
 
126
+ ## JournalEntry import (the fifth transaction type)
127
+ JournalEntry extends the same engine to capture **GL revenue/cost adjustments posted
128
+ directly as journal entries**. It is **structurally different from the four sale types** — a
129
+ JE has **no item lines**; each line is a raw GL posting (account, debit/credit, entity,
130
+ location, memo). Handler `_Worker_Netsuite_JournalEntry`: `post`/`put` → `syncJournalEntry`,
131
+ `delete` → `removeAllJournalEntry`. The router PascalCases `journalEntry` → this class, so
132
+ **no router change** is needed (same as the sale types). The engine's
133
+ `syncJournalEntry`/`buildJournalEntryRows`/`importOneJournalEntry`/etc. reuse the existing
134
+ item-line upsert/reconcile machinery (`syncLines`/`guardedInsert`/`deleteRows`).
135
+
136
+ **Mapping model (durable design):**
137
+ - A JE line carries **no native item and no salesRep** (confirmed against the live account:
138
+ 0 JEs carry an item on a revenue/cost line). The fields the business rule needs **do not
139
+ exist in NetSuite yet** — they are stubbed behind constants `JE_LINE_ITEM_FIELD` /
140
+ `JE_LINE_SALESREP_FIELD` (currently **null**), so until the fields are added every kept
141
+ line groups under `(null, null)` into **one aggregate row**.
142
+ - Lines hitting a **revenue** or **cost** GL account are grouped by **(salesRep, item)**:
143
+ `revenue = Σ(credit − debit)` over revenue lines, `cost = Σ(debit − credit)` over cost
144
+ lines, `profit = revenue − cost`.
145
+ - **Account classification is done on the fly by NetSuite `accttype`** — SuiteQL
146
+ `SELECT id, accttype FROM account WHERE id IN (...)`, cached per run, bucketed via a tunable
147
+ `JE_ACCTTYPE_BUCKETS` map. It is **NOT** a hardcoded account-id list. PROVISIONAL default:
148
+ `Income → revenue`, `COGS → cost` (pending sales-team sign-off; open questions: whether
149
+ `OthIncome`/`Expense` count, and the Income-typed but "Cost"-named accounts 430/433).
150
+ - Group key uses `json_encode([salesRep, item])` (not a string-join) to stay collision-safe
151
+ once the fields go live — an empty-string separator would let `"5|" + null` collide.
152
+
153
+ **Reversal handling (a JE-only pattern):**
154
+ - Setting a reversal date makes NetSuite auto-create a **paired reversing JE**, but **only
155
+ one webhook fires** (for the original). The engine resolves the reversal from the original
156
+ via SuiteQL `transaction.reversal` (the reversal's id) and imports it too, keyed by its
157
+ **own** internalId, so each posting period nets correctly (the reversal carries opposite
158
+ debit/credit → the natural negation). Helper: `resolveJournalEntryReversal`.
159
+ - The reversal's REST `createdFrom` = the original entry, so its `Forecast.Sales` rows carry
160
+ `createdFromNetsuiteTransactionInternalId = original`. A **delete on the original therefore
161
+ cascades to the reversal rows locally** via `deleteRowsCreatedFrom` with **no NetSuite
162
+ call** — important because the record may already be gone on a delete event.
163
+ - The partner-reversal import is wrapped so a reversal **deleted in NetSuite before the
164
+ webhook fires** (`fetchRecord` 404) does **not** abort the original entry's
165
+ already-committed import (it is logged + skipped).
166
+
167
+ **Additive + dormant.** The enum add (`'journalEntry'` in `Sales.netsuiteTransactionType`)
168
+ is backward-compatible; with the prod JE enqueuer deployment **off**, no JE webhook fires so
169
+ the handler never runs. Safe to merge and deploy the NS enqueuer later. The
170
+ `'journalentry':'journalEntry'` entry was added to both enqueuer `RECORD_TYPE_MAP`s.
171
+
120
172
  ## Data model — Forecast.Sales schema facts
121
173
  Prod `Forecast.Sales` writable columns: `netsuiteTransactionType`
122
- (enum `'invoice','cashSale','creditMemo','cashRefund'`), `netsuiteTransactionInternalId`,
174
+ (enum `'invoice','cashSale','creditMemo','cashRefund','journalEntry'` — `journalEntry`
175
+ added by `dbchanges2/Forecast/2026-06-26a`), `netsuiteTransactionInternalId`,
123
176
  `tranDate`, `tranNumber`, `customerId`, `leadSource`, `salesRepEmployeeId`, `itemId`,
124
177
  `lineNumber`, `revenue`/`profit` `decimal(14,2)`, `createdFromNetsuiteTransactionInternalId`.
125
178
  - **No `amountDue` and no `dtPendingBilling` column** in prod (no staged dbchanges2 adds
@@ -143,7 +196,10 @@ Cash Refund**. The drainer (`customscript_ss_amq_drain`) is record-type-agnostic
143
196
  no per-type deployment.
144
197
 
145
198
  The enqueuer's `RECORD_TYPE_MAP` (lowercase NS `record.type` → camelCase `recordType` the
146
- worker2 router PascalCases) already maps the four sale types 1:1. Note: NetSuite emits item
199
+ worker2 router PascalCases) maps the four sale types 1:1, plus
200
+ `'journalentry':'journalEntry'` (added this session to both the prod and dev enqueuers); a
201
+ JE webhook therefore also requires this script deployed on the **Journal Entry** record
202
+ type. Note: NetSuite emits item
147
203
  events as specific **subtypes** (`inventoryItem`, `nonInventoryResaleItem`, `kitItem`,
148
204
  `itemGroup`, …) with no generic `item` type — collapsing those subtype VALUES to `'item'`
149
205
  would be needed only for a future real-time item webhook, **not** for this Sales importer
@@ -203,6 +259,20 @@ Actions: `check | create | get | update | delete | sync | recent | amq | deploym
203
259
  - The cron's sign handling is not portable here — see Sign convention.
204
260
 
205
261
  ## Change history
262
+ - 2026-06-26 — **Extended the importer to a fifth transaction type, `journalEntry`** (GL
263
+ revenue/cost adjustments). New thin handler `_Worker_Netsuite_JournalEntry`
264
+ (post/put→`syncJournalEntry`, delete→`removeAllJournalEntry`); engine gained
265
+ `TYPE_JOURNAL_ENTRY` + JE methods reusing the existing line upsert/reconcile machinery;
266
+ `'journalEntry'` added to the `Sales.netsuiteTransactionType` enum (dbchanges2 + local
267
+ ALTER) and `'journalentry'` to both enqueuer maps. Durable design recorded: JEs have **no
268
+ item lines** (lines are GL postings), grouped by (salesRep, item) with item/salesRep
269
+ **stubbed null** until the NetSuite fields exist; **on-the-fly accttype classification**
270
+ (`JE_ACCTTYPE_BUCKETS`, provisional Income→revenue / COGS→cost); and the **reversal
271
+ pattern** (resolve via `transaction.reversal`, import keyed by its own id, delete cascades
272
+ through `createdFrom` locally, 404-on-reversal tolerated). Verified e2e via the direct
273
+ engine against prod NetSuite with prod isolated (JE enqueuer undeployed → 0 prod rows);
274
+ built harness `test/@dave/test_je_lifecycle.php`. Additive + dormant until the NS enqueuer
275
+ deploys. (dfranks)
206
276
  - 2026-06-26 — **Verified the full create→update→delete webhook lifecycle end-to-end on a local box**
207
277
  (ngrok tunnel → local worker2 → local `Forecast.Sales`), driving a real **$0** NetSuite invoice
208
278
  through create→insert, edit→update (tracked-column change-detection), delete→`removeAll`; confirmed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.213",
3
+ "version": "1.0.214",
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",