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
|
|
34
|
-
|
|
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`;
|
|
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'`
|
|
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)
|
|
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