toga-ai 1.0.112 → 1.0.113
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.
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
9
9
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
|
|
10
10
|
| [NetSuite → TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
11
|
+
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
11
12
|
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini |
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-17
|
|
10
|
+
owners: ["dfranks"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Netsuite/SalesOrder.php
|
|
13
|
+
- worker2/Worker/Netsuite.php
|
|
14
|
+
- test/@dave/probe_salesorder_rest_shape.php
|
|
15
|
+
- worker/crons/toga2/forecast2/import_open_orders.php
|
|
16
|
+
- worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
|
|
17
|
+
related:
|
|
18
|
+
- ./netsuite-opportunity-sync.md
|
|
19
|
+
- ../architecture.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). A NetSuite
|
|
25
|
+
`salesOrder` create/edit/delete arrives at `webhook.togahub.com/netsuite`, the
|
|
26
|
+
`_Worker_NetSuite::Webhook` router dispatches `Netsuite/SalesOrder/{POST,PUT,DELETE}`, and the
|
|
27
|
+
handler syncs the order's currently-**open** line items into `Forecast.OpenOrderItems`. It is the
|
|
28
|
+
real-time equivalent of the batch cron `worker/crons/toga2/forecast2/import_open_orders.php` (whose
|
|
29
|
+
per-record logic lives in `common_import_sales_from_netsuite.php`, OPEN ORDERS section). Mirrors the
|
|
30
|
+
[opportunity sync](./netsuite-opportunity-sync.md) — same shape, different table and gating.
|
|
31
|
+
|
|
32
|
+
`OpenOrderItems` holds the **unbilled remainder** of approved-but-not-fully-billed sales orders;
|
|
33
|
+
realized revenue (invoices/cash sales/credit memos/cash refunds) lives in `Forecast.Sales` and is a
|
|
34
|
+
separate build (see *Related work*).
|
|
35
|
+
|
|
36
|
+
## Key files / entry points
|
|
37
|
+
|
|
38
|
+
- `Worker/Netsuite/SalesOrder.php` — `_Worker_Netsuite_SalesOrder`. **One class, two directions:**
|
|
39
|
+
the new INBOUND import (`POST`/`PUT`/`DELETE`, REST → Forecast) was *merged into* the pre-existing
|
|
40
|
+
OUTBOUND push (`Create`/`Update`/`Sync`, Toga → NetSuite via SOAP). The action router forces the
|
|
41
|
+
import entry points to live here (`salesOrder` → `_Worker_Netsuite_SalesOrder`), so they coexist.
|
|
42
|
+
- `Worker/Netsuite.php` — the shared router (unchanged; no per-recordType edits needed).
|
|
43
|
+
- NetSuite access is **REST only**, via the `_underscore` client `_Component_Api_Netsuite`
|
|
44
|
+
(`RECORD_SALES_ORDER` + `?expandSubResources=true`) — never the SOAP `NetSuiteService`.
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
1. `POST`/`PUT` → `importOpenOrder($internalId)`; `DELETE` → `removeAll($internalId)`.
|
|
49
|
+
2. `importOpenOrder` GETs the sales order via REST, then gates on **open status** (`status->refName`
|
|
50
|
+
∈ `OPEN_STATUSES`: `Pending Fulfillment`, `Partially Fulfilled`, `Pending Billing/Partially
|
|
51
|
+
Fulfilled`, `Pending Billing`). A non-open **or missing** status → `removeAll` (the order carries
|
|
52
|
+
no open rows).
|
|
53
|
+
3. Per open line: skip lines with no NS item id, item-group items (`netsuiteTransactionType ==
|
|
54
|
+
'itemGroup'`), and excluded EWR items (`DO_NOT_IMPORT_OPEN_ORDER_ITEM_IDS`). Throw if the NS item
|
|
55
|
+
isn't in `Forecast.Items` (surfaces an item-sync gap rather than dropping revenue).
|
|
56
|
+
4. Open economics: `qtyOpen = quantity − quantityBilled`; `revenue = qtyOpen × rate`;
|
|
57
|
+
`profit = revenue − qtyOpen × unitCost` where `unitCost = costEstimate / quantity`
|
|
58
|
+
(divide-by-zero-safe). A line with `revenue == 0 && profit == 0` is dropped.
|
|
59
|
+
5. Lookups → local ids: `Customers`/`Employees` by `netsuiteInternalId` (miss → null);
|
|
60
|
+
`Classifications` create-on-miss, name = last ` : `-segment of `class->refName` (e.g.
|
|
61
|
+
"True Solutions : SaaS Reseller : CSP" → "CSP").
|
|
62
|
+
6. `syncOpenLines` upserts each open line keyed on `(netsuiteSalesOrderInternalId, lineNumber)`, then
|
|
63
|
+
deletes any row NetSuite no longer returns as open (orphans + now-closed lines). Commits
|
|
64
|
+
`DB_FORECAST` (lazy-transaction discipline).
|
|
65
|
+
|
|
66
|
+
## Data model
|
|
67
|
+
|
|
68
|
+
`Forecast.OpenOrderItems` — flat, denormalized **leaf** table (no header table, no FK children).
|
|
69
|
+
Unique key `(netsuiteSalesOrderInternalId, lineNumber)`. Columns written: `netsuiteSalesOrderInternalId`,
|
|
70
|
+
`dateOrder` (tranDate +12h), `orderNumber` (tranId), `customerId`, `salesRepEmployeeId`,
|
|
71
|
+
`classificationId`, `itemId` (nullable), `lineNumber`, `revenue`, `profit`. The `accountId` FK column
|
|
72
|
+
exists but is **never populated** (the legacy writer never set it — parity).
|
|
73
|
+
|
|
74
|
+
## Client variations
|
|
75
|
+
|
|
76
|
+
None — uniform (platform-wide Forecast2 sync).
|
|
77
|
+
|
|
78
|
+
## Gotchas / known issues
|
|
79
|
+
|
|
80
|
+
- **REST shape ≠ SOAP shape.** The cron reads the SOAP-shim shape; this handler reads the REST record
|
|
81
|
+
(`status->refName`, line `quantityBilled`, `class->refName`, `entity->id`, `salesRep->id`,
|
|
82
|
+
`shippingCost`). Verified against live orders via `test/@dave/probe_salesorder_rest_shape.php`.
|
|
83
|
+
- **Three deliberate departures from the legacy cron (all intentional):**
|
|
84
|
+
1. **No date-window gate.** The cron flips `isOrderOpen=false` for tranDate outside −365d/+90d
|
|
85
|
+
(`common_import_…:1753`) to bound its windowed scan. Irrelevant to a single-id webhook — dropped.
|
|
86
|
+
(Consequence: a future-dated open SO the cron excludes *would* sync via webhook.)
|
|
87
|
+
2. **No shipping line.** The cron appends a synthetic `SHIPPING` line for `OpenOrderItems` with a
|
|
88
|
+
**null** item id, which its own item-id guard then drops — so shipping has **never** persisted to
|
|
89
|
+
`OpenOrderItems` (confirmed: 0 line-0 rows in prod). We omit it, preserving that behavior. (NB:
|
|
90
|
+
the cron's *Sales* section is different — it uses real item id **13500**, so shipping *does*
|
|
91
|
+
persist in `Forecast.Sales`.)
|
|
92
|
+
3. **Zero-open cascade bug fixed.** The cron's `$isOrderOpen` is order-scoped and never reset
|
|
93
|
+
per-line, so the first zero-open line (`common_import_…:1848`) poisons every later line on the
|
|
94
|
+
order — under-reporting open revenue on partially-billed multi-line orders, order-dependent. The
|
|
95
|
+
webhook skips only the zero line and keeps the order's other open lines (correct per-line
|
|
96
|
+
semantic). `trueup_open_orders.php` has been masking this in prod data.
|
|
97
|
+
- **No `initialize()`.** It was removed: it only constructed the SOAP `NetSuiteService` (which *throws*
|
|
98
|
+
without `NS_HOST`/`NS_ENDPOINT` and builds a `SoapClient`), and the framework runs `initialize()`
|
|
99
|
+
before *every* action — keeping it would couple the REST import to SOAP config. The push methods
|
|
100
|
+
(`Create`/`Update`/`Sync`) self-construct `NetSuiteService`, so push is unaffected and the Forecast
|
|
101
|
+
DB is registered globally in `_.php`.
|
|
102
|
+
|
|
103
|
+
## Related work
|
|
104
|
+
|
|
105
|
+
`Forecast.Sales` (realized revenue) is fed by four other NS record types — `invoice`, `cashSale`,
|
|
106
|
+
`creditMemo`, `cashRefund` — each routing to its own worker (`_Worker_Netsuite_Invoice`, etc.) over a
|
|
107
|
+
planned shared `_Component_Forecast_SaleImport` engine (apply `$factor = −1` for credit memos / cash
|
|
108
|
+
refunds). The low-level Forecast SQL/lookup helpers (`sqlLiteral`, `buildInsert`, `buildAssignments`,
|
|
109
|
+
`lookupId`, `toSqlDate`, `fetchRecord`) are currently duplicated in `Opportunity.php` + `SalesOrder.php`
|
|
110
|
+
and are a candidate to extract into a shared `_Component_Forecast_Db` before the Sales build.
|
|
111
|
+
|
|
112
|
+
## Change history
|
|
113
|
+
|
|
114
|
+
- 2026-06-17 — Initial open-orders importer merged into `_Worker_Netsuite_SalesOrder` (TRUE-79142):
|
|
115
|
+
REST-only, no date-window gate, no shipping line, cascade bug fixed. (dfranks)
|
|
116
|
+
|
|
117
|
+
## Related docs
|
|
118
|
+
|
|
119
|
+
- [NetSuite → TOGA Opportunity Sync](./netsuite-opportunity-sync.md) — the sibling pattern this mirrors.
|
|
120
|
+
- [Worker (worker2) Architecture](../architecture.md)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
## 2.0 framework
|
|
16
16
|
|
|
17
17
|
- **_underscore** (_Underscore) _(framework core)_ — 7 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
18
|
-
- **worker2** (Worker) —
|
|
18
|
+
- **worker2** (Worker) — 8 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
19
19
|
- **api2** (API) — 3 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
20
20
|
- **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
21
21
|
- **toga2-supply** (TOGa Supply) — 2 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
package/package.json
CHANGED