toga-ai 1.0.198 → 1.0.200
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.
- package/knowledge/1.0/apps/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +56 -3
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +8 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +29 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-salesorder-open-orders-sync.md +12 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/rate/INDEX.md +1 -0
- package/knowledge/clients/rate/features/netsuite-cashsale-export.md +92 -0
- package/knowledge/clients/rate/profile.md +2 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
|
|
6
6
|
| [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
|
|
7
7
|
| [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php |
|
|
8
|
-
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/reconcile_netsuite_totals.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
8
|
+
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/reconcile_netsuite_totals.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
|
|
9
9
|
| [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
|
|
10
10
|
| [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
|
|
11
11
|
| [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
|
|
@@ -6,13 +6,14 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-25
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- test/@dave/reconcile_netsuite_totals.php
|
|
13
13
|
- test/@dave/analyze_netsuite_forecast_diff.php
|
|
14
14
|
- test/@dave/trueup_sales.php
|
|
15
15
|
- test/@dave/trueup_open_orders.php
|
|
16
|
+
- test/@dave/loop_trueup_open_orders.php
|
|
16
17
|
- test/@dave/trueup_opportunities.php
|
|
17
18
|
- test/@dave/probe_sales_gap_direct.php
|
|
18
19
|
- test/@dave/probe_missing_oo_timing.php
|
|
@@ -21,6 +22,9 @@ files:
|
|
|
21
22
|
- test/@dave/probe_profit_invoices.php
|
|
22
23
|
- test/@dave/probe_profit_gap.php
|
|
23
24
|
- worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
|
|
25
|
+
- worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php
|
|
26
|
+
- worker/crons/toga2/forecast2/import_open_orders.php
|
|
27
|
+
- worker/schedules/cron.worker.infrastructure.json
|
|
24
28
|
related:
|
|
25
29
|
- ../architecture.md
|
|
26
30
|
- ../../library/features/netsuite-suiteql-rest-shim.md
|
|
@@ -49,8 +53,29 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
49
53
|
NS_ONLY (missing from FC), FC_ONLY (stale/extra), DRIFT (value differs). Read-only.
|
|
50
54
|
- `trueup_sales.php --from --to [--chunk-days N] [--prod] [--dry-run]` — makes `Forecast.Sales`
|
|
51
55
|
match NetSuite for a tranDate range (insert/update/delete per line).
|
|
52
|
-
- `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose]
|
|
53
|
-
`Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
|
|
56
|
+
- `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose] [--quiet] [--by-lastmodified]`
|
|
57
|
+
— same for `Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
|
|
58
|
+
- **`--by-lastmodified`** windows **both** passes on `lastmodifieddate` instead of `tranDate`:
|
|
59
|
+
Step 1 (open-SO reconcile) and Step 3b (stale cleanup). `lastmodifieddate` is a TIMESTAMP →
|
|
60
|
+
bounded `TO_TIMESTAMP(from 00:00:00)..(to 23:59:59)` in the NetSuite **ET** session tz. Under this
|
|
61
|
+
mode Step 3b **inverts the lookup**: it asks NS for SOs **modified in the window** that are no longer
|
|
62
|
+
open (`status NOT IN B/D/E/F`) via id-only paged SuiteQL (**no per-id GET, no per-row request** —
|
|
63
|
+
cost scales with SOs modified, not with local row count), then deletes the local rows for any still
|
|
64
|
+
held. This catches an order **billed in the window whose `tranDate` predates it** — exactly the
|
|
65
|
+
invoice-transform billing case a `tranDate` window misses (see the open-orders sync doc).
|
|
66
|
+
- **`--quiet`** suppresses all progress (`out()` no-op) and prints the Step 4 results block **only**
|
|
67
|
+
when `insert+update+delete > 0` (a no-change pass prints nothing); transient NS failures + fatals
|
|
68
|
+
still go to stderr.
|
|
69
|
+
- **`--verbose`** was trimmed — removed per-line MATCH/EXCLUDE/NOOP and the always-on per-SO summary;
|
|
70
|
+
the per-SO header now prints lazily, once, only when the order has an INSERT/UPDATE/DELETE.
|
|
71
|
+
- `loop_trueup_open_orders.php [--prod] [--dry-run] [--verbose] [--quiet] [--sleep=N] [--days-back=N]`
|
|
72
|
+
— continuous runner that recomputes a rolling `[today−N .. today]` window each pass and re-invokes
|
|
73
|
+
`trueup_open_orders.php --by-lastmodified` until Ctrl-C. Uses `PHP_BINARY` + `passthru` with
|
|
74
|
+
`escapeshellarg` over a **fixed flag allowlist** (`--prod`/`--dry-run`/`--verbose`/`--quiet`); survives
|
|
75
|
+
a failed pass. With `--quiet` it also suppresses its own per-pass banner + the sleeping line, so the
|
|
76
|
+
only output is a trueup Step 4 block when rows actually change. **This is the stopgap for the real-time
|
|
77
|
+
open-order removal gap** (an SO billed via invoice-transform fires no webhook → `removeAll` never runs —
|
|
78
|
+
see the open-orders sync doc).
|
|
54
79
|
- `trueup_opportunities.php --from --to [--prod] [--dry-run]` — same for `Forecast.Opportunities`
|
|
55
80
|
by tranDate range. Pure SuiteQL (no per-id GET), so it's fast: a 2026-YTD prod run reconciled
|
|
56
81
|
~4,000 opps in ~13s to a $0 delta.
|
|
@@ -206,11 +231,39 @@ None — Forecast2 is a single shared dataset.
|
|
|
206
231
|
revenue. `trueup_sales.php` repairs them: it compares against NS by trandate, detects the sign
|
|
207
232
|
mismatch, and delete+re-inserts with the correct sign. See the shim/reference docs for why a
|
|
208
233
|
uniform `-foreignamount` is correct and an extra per-type `$factor` double-negates.
|
|
234
|
+
- **`OpenOrderItems.dtPendingBilling` does not exist in prod and NO prod code writes it.** A `SHOW
|
|
235
|
+
COLUMNS` on the core2 reader confirms the column is **present locally, absent in prod** — the
|
|
236
|
+
TRUE-79081 migration was never deployed. `periodic_forecast_discrepancy_fix_open_orders.php` **intentionally
|
|
237
|
+
does not write it** (explicit guard comments; writing it would raise MySQL 1054) and worker2's
|
|
238
|
+
`_Worker_Netsuite_SalesOrder` never references it. So **nothing currently sets
|
|
239
|
+
`OpenOrderItems.dtPendingBilling` in prod.** (This **corrects** the earlier note that it was "fixed in
|
|
240
|
+
both writers" on 2026-06-15 — the writers were since changed to **skip** it pending the prod migration.)
|
|
241
|
+
- **Of the legacy open-orders crons, only the discrepancy-fix is active — and it CLEANS billed orders,
|
|
242
|
+
it does not resurrect them.** `import_open_orders.php` is **`"active": 0` (DISABLED)** in
|
|
243
|
+
`worker/schedules/cron.worker.infrastructure.json`, but its flag `SHOULD_SYNC_OPEN_ORDERS = true` is
|
|
244
|
+
still set — a **latent re-enable footgun** (flipping `active` back on would resume the cron with its
|
|
245
|
+
known cascade/date-window quirks). The **only active legacy writer** of `OpenOrderItems` is
|
|
246
|
+
`periodic_forecast_discrepancy_fix_open_orders.php` (`active:1`, daily 3:00 AM): it INSERTs only for
|
|
247
|
+
currently-**open** SOs (status B/D/E/F) and its **Step-3b stale-cleanup DELETEs** rows whose SO is no
|
|
248
|
+
longer open — so it **removes** billed/closed orders rather than resurrecting them. It is therefore
|
|
249
|
+
**not** a resurrection risk against the webhook `removeAll` path; it is in fact the current backstop
|
|
250
|
+
deleter for orders the webhook never sees billed (the invoice-transform gap).
|
|
209
251
|
- These tools live in `test/@dave/` (developer tooling), but `trueup_open_orders` has been run
|
|
210
252
|
against production. The `Defaults`/checkpoint mechanics of the scheduled sync are separate.
|
|
211
253
|
|
|
212
254
|
## Change history
|
|
213
255
|
|
|
256
|
+
- 2026-06-25 — **`trueup_open_orders` gained `--by-lastmodified` / `--quiet` / leaner `--verbose`; added
|
|
257
|
+
`loop_trueup_open_orders.php` as the stopgap for the real-time billed-order removal gap.**
|
|
258
|
+
`--by-lastmodified` windows both Step 1 and Step 3b on `lastmodifieddate` (ET TIMESTAMP bounds) and
|
|
259
|
+
inverts Step 3b to an id-only paged SuiteQL "modified-and-no-longer-open" lookup (no per-id GET), so it
|
|
260
|
+
catches an order billed in-window whose `tranDate` predates it. `--quiet` prints only a non-zero Step 4
|
|
261
|
+
block. `loop_trueup_open_orders.php` is a continuous rolling-window runner (PHP_BINARY + passthru over a
|
|
262
|
+
fixed flag allowlist). Also recorded that **`OpenOrderItems.dtPendingBilling` is local-schema-only and no
|
|
263
|
+
prod code writes it** (corrects the earlier "fixed in both writers" note — both writers now skip it), and
|
|
264
|
+
that of the legacy open-orders crons **only `periodic_forecast_discrepancy_fix_open_orders.php` is active**
|
|
265
|
+
and it cleans up billed/closed orders rather than resurrecting them (while `import_open_orders.php` is
|
|
266
|
+
`active:0` but still flags `SHOULD_SYNC_OPEN_ORDERS=true` — a latent re-enable footgun). (dfranks)
|
|
214
267
|
- 2026-06-24 — **Sales audit correctness pass.** Fixed a phantom Sales delta: `reconcile`'s NS Sales
|
|
215
268
|
SUM/diff now applies the sync's status exclusions (Unapproved Payment / Voided / Rejected) so it can't
|
|
216
269
|
count rows the sync never stores (closed revenue delta to $0). Added a Sales per-transaction presence
|
|
@@ -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-25
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Forecast/SaleImport/SaleImport.php
|
|
@@ -154,4 +154,11 @@ would be needed only for a future real-time item webhook, **not** for this Sales
|
|
|
154
154
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
155
155
|
|
|
156
156
|
## Change history
|
|
157
|
+
- 2026-06-25 — **Verified `sync()` end-to-end across all four sale types** (prod NS reads, local Forecast
|
|
158
|
+
writes; 40+ records spanning invoice/cashSale/creditMemo/cashRefund): revenue reconciled **to the penny**
|
|
159
|
+
against an independent SuiteQL `SUM(-foreignamount)` oracle on every record; the uniform sign factor held
|
|
160
|
+
(invoice/cashSale positive, creditMemo/cashRefund negative); kit handling correct (itemGroup line and the
|
|
161
|
+
no-item-id summary line dropped, the real component line kept); tax-reversal credit memos correctly net to
|
|
162
|
+
$0 line revenue; the guarded `INSERT … WHERE NOT EXISTS` proved idempotent (re-sync → 0 duplicates); and
|
|
163
|
+
item self-heal worked for old records. No code change — confirms the existing behavior. (dfranks)
|
|
157
164
|
- 2026-06-24 — Built the Forecast.Sales real-time webhook importer (shared `_Component_Forecast_SaleImport` engine + four thin worker2 handlers), replacing the legacy cron SALES section; decided the uniform-factor sign convention against the raw REST record; deduped `fetchRecord`/sublist pagination onto `_Component_Forecast_Db`. (dfranks)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-25
|
|
10
10
|
owners: ["dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite.php
|
|
@@ -321,6 +321,25 @@ None — platform-wide Forecast sync.
|
|
|
321
321
|
HTTP call so a concurrent drainer pass skips it (`Sending` ∉ sendable), narrowing the double-send
|
|
322
322
|
window to the load→claim gap. `findSendableIds` must list every sendable status
|
|
323
323
|
(`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently matches nothing (`candidates:0`).
|
|
324
|
+
- **The enqueuer is generic over all record types but RELEASED only on Opportunity + Sales Order.**
|
|
325
|
+
`ue_api_msg_queue_enqueue` (`customscript_ue_amq_enqueue`) is generic via `RECORD_TYPE_MAP`, fires
|
|
326
|
+
`afterSubmit` for create/edit/xedit/delete, has **no execution-context filter**, and runs with
|
|
327
|
+
`DEV_OVERRIDE.enabled=false` in prod (posts to `webhook.togahub.com/netsuite`). Probe-confirmed it is
|
|
328
|
+
**`Released` on only two record types**: Opportunity (`customdeploy1`) and Sales Order
|
|
329
|
+
(`customdeploy2`). So Invoice/CashSale/CreditMemo/CashRefund (the Forecast.Sales types) are **not yet
|
|
330
|
+
enqueued via webhook** — which is why a billing invoice fires no webhook (see the open-orders doc's
|
|
331
|
+
invoice-transform gap). Adding a real-time sale-import webhook requires releasing this same enqueuer on
|
|
332
|
+
those four types.
|
|
333
|
+
- **`lib_amq_queue.js` is a LIBRARY module (no `@NScriptType`) → it has NO deployment of its own, and its
|
|
334
|
+
`N/log` output surfaces in the CALLING script's execution log.** Its `transmit()` wraps `https.request`
|
|
335
|
+
in a try, but the surrounding `record.load`/claim and `markFailure`'s `submitFields` are **outside** that
|
|
336
|
+
try — so a throw there escapes to `ss_amq_drain`'s per-row catch. A live drainer error
|
|
337
|
+
`{"queueId":40529,"error":"UNEXPECTED_ERROR: An unexpected SuiteScript error has occurred"}` (a TypeError,
|
|
338
|
+
observed 2026-06-25 1:10am) originates in this transmit path and shows up under **`ss_amq_drain`'s**
|
|
339
|
+
execution log (not a library log of its own). **Durable error detail is also persisted on the API Message
|
|
340
|
+
Queue custom record** fields `custrecordcustrecord_amq_errormsg` / `_resp_status` / `_resp_body`
|
|
341
|
+
(doubled-prefix ids — see the doubled-id gotcha), which are **SuiteQL-queryable** for after-the-fact
|
|
342
|
+
diagnosis even though the calling-script log rolls off.
|
|
324
343
|
|
|
325
344
|
## CU→NS direction (future — not yet built): reverse mapping
|
|
326
345
|
|
|
@@ -345,6 +364,15 @@ same **skip-if-unchanged** compare on the extracted values, and **actor-identity
|
|
|
345
364
|
trigger a CU→NS write. The NS→CU change-detection above is the complementary backstop, not a substitute.
|
|
346
365
|
|
|
347
366
|
## Change history
|
|
367
|
+
- 2026-06-25 — **Characterized the AMQ enqueuer scope + a live drainer TypeError.** The enqueuer is
|
|
368
|
+
generic over all record types via `RECORD_TYPE_MAP` (no context filter, prod `DEV_OVERRIDE.enabled=false`)
|
|
369
|
+
but probe-confirmed `Released` on **only Opportunity (`customdeploy1`) + Sales Order (`customdeploy2`)** —
|
|
370
|
+
so the four Forecast.Sales record types are not yet webhook-enqueued. Recorded that `lib_amq_queue.js` is a
|
|
371
|
+
**library module** (no `@NScriptType`/own deployment) whose `N/log` surfaces in the **calling** script's
|
|
372
|
+
(`ss_amq_drain`) log, and that an `UNEXPECTED_ERROR` (TypeError, queueId 40529, 2026-06-25 1:10am)
|
|
373
|
+
originates in its `transmit()` path where `record.load`/claim + `markFailure.submitFields` sit **outside**
|
|
374
|
+
the `https.request` try. Durable error detail also persists on the queue record's
|
|
375
|
+
`custrecordcustrecord_amq_errormsg`/`_resp_status`/`_resp_body` fields (SuiteQL-queryable). (dfranks)
|
|
348
376
|
- 2026-06-24 — **Default presales-lead assignee fallback.** `buildCustomFields()` now wires the
|
|
349
377
|
previously-unused `DEFAULT_PRESALES_LEAD_USER_ID = '87374309'` (Cory Martin, comartin@togatech.com —
|
|
350
378
|
verified active via ClickUp `GET /team`) into the else-branch when `resolvePresalesUserId()` returns
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-25
|
|
10
10
|
owners: ["dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/SalesOrder.php
|
|
@@ -133,6 +133,7 @@ None — uniform (platform-wide Forecast2 sync).
|
|
|
133
133
|
|
|
134
134
|
## Gotchas / known issues
|
|
135
135
|
|
|
136
|
+
- **Billing an SO via invoice-transform flips it to "Billed" WITHOUT firing the SalesOrder afterSubmit UE → no edit/PUT webhook → `removeAll()` never runs → the OpenOrderItems rows linger (this is the real cause of the "billed order not removed" problem — NOT a removeAll-didn't-persist bug).** When a NetSuite Sales Order is billed by **creating an Invoice from it**, the SO's `status` transitions to `Billed` as a **side-effect of the invoice transform** — this does **not** trigger the SalesOrder `afterSubmit` User Event, so the AMQ enqueuer (`ue_api_msg_queue_enqueue`, deployed per-record-type) **never emits an edit/PUT webhook for the SO**. Consequently `_Worker_Netsuite_SalesOrder` is never invoked for the billing change, `importOpenOrder`'s status gate (which *would* call `removeAll` on a non-open status) never runs, and the order's `OpenOrderItems` rows survive until the nightly discrepancy-fix cron deletes them. **Confirmed live 2026-06-24/25 on SO 7190415** (tranId 280387): only a `create` webhook delivery exists in `Logs.Webhook` (2026-06-24 23:48, 16 rows inserted) — **NO** edit/delete delivery at billing time (~09:41 UTC 2026-06-25); `Core.WorkerJobs` shows only `Netsuite/Webhook` + `Netsuite/SalesOrder/post` (no put/delete); `Logs.Api` has only the original NS GET + a 200 `OPEN_ORDER_IMPORT` breadcrumb — **no 204 removeAll breadcrumb**. The invoice (7190621) that billed it **also produced no webhook** — its 16 `Forecast.Sales` rows came from the **legacy 5-min pull cron**, not the webhook. **This corrects the earlier suspicion that "removeAll fired but DB didn't persist"** — both `removeAll` paths *do* commit `DB_FORECAST`; `removeAll` was simply **never called**. **Diagnostic ladder for a lingering billed order:** `Logs.Webhook` (was an edit/delete even delivered? — here, no) → `Core.WorkerJobs` (was a put/delete processed? — no) → `Logs.Api` `source='OPEN_ORDER_IMPORT'` (any 204 removeAll breadcrumb? — no). **Fix direction (not yet implemented):** the SalesOrder-side enqueuer must also fire on the invoice-transform status change — either deploy the AMQ enqueuer on **Invoice** so the bill event drives an SO re-sync, or have the SalesOrder handler re-evaluate the SO when its child invoice arrives; until then the **stopgap is the continuous `loop_trueup_open_orders.php --by-lastmodified` runner** (see the reconciliation doc), which windows on `lastmodifieddate` so it catches an order billed today whose `tranDate` predates the window. The `removeAll` path in `SalesOrder.php` was re-confirmed correct in this session.
|
|
136
137
|
- **Wrong FK column name → MySQL 1054 that masquerades as "order missing".** `OpenOrderItems`'s
|
|
137
138
|
sales-order key is **`netsuiteSalesOrderInternalId`**. Do **NOT** query it with
|
|
138
139
|
`netsuiteTransactionInternalId` — that is the **`Forecast.Sales`** column. On `OpenOrderItems` it
|
|
@@ -296,6 +297,16 @@ test fixture (it surfaced the stale SO 7181316 above).
|
|
|
296
297
|
|
|
297
298
|
## Change history
|
|
298
299
|
|
|
300
|
+
- 2026-06-25 — **Root-caused "billed SO not removed from OpenOrderItems in real time" to a missing
|
|
301
|
+
webhook, NOT a removeAll-didn't-persist bug.** Billing an SO via invoice-transform flips it to
|
|
302
|
+
`Billed` as a side-effect that does **not** fire the SalesOrder `afterSubmit` UE → the AMQ enqueuer
|
|
303
|
+
emits no edit/PUT webhook → `_Worker_Netsuite_SalesOrder` never runs → `removeAll()` is never called
|
|
304
|
+
(both removeAll paths confirmed to commit `DB_FORECAST` correctly — the call simply never happens).
|
|
305
|
+
Confirmed live on SO 7190415: only a `create` delivery in `Logs.Webhook`, no put/delete in
|
|
306
|
+
`Core.WorkerJobs`, no 204 removeAll breadcrumb in `Logs.Api`; the billing invoice produced no webhook
|
|
307
|
+
either (its Sales rows came from the legacy pull cron). Recorded the `Logs.Webhook`→`WorkerJobs`→
|
|
308
|
+
`OPEN_ORDER_IMPORT`-breadcrumb diagnostic ladder and the stopgap (`loop_trueup_open_orders.php
|
|
309
|
+
--by-lastmodified`). (dfranks)
|
|
299
310
|
- 2026-06-24 — **Outbound push (`CreateNetSuite`/`UpdateNetSuite`/`Sync`) converted SOAP →
|
|
300
311
|
REST-only** via `_Component_Api_Netsuite`: item + customer lookups now SuiteQL
|
|
301
312
|
(`resolveNetSuiteCustomerId()` throws on 0/>1 name matches), `buildNetSuiteOrder()` emits a REST
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
18
|
+
- **_underscore** (_Underscore) _(framework core)_ — 15 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
19
|
- **worker2** (Worker) — 15 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
20
|
- **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Rate Monthly Reconciliation Report](features/monthly-reconciliation-report.md) | 1.0 | A monthly cron that emails an Excel reconciliation report covering all Rate subscription sales orders and their linked PayPal payments for the prior calendar mo | worker/crons/notifications/reports/rate/send_monthly_rate_purchases_report.php, worker/schedules/cron.worker.notification.json |
|
|
6
|
+
| [Rate SalesOrder → NetSuite CashSale Export (postPost)](features/netsuite-cashsale-export.md) | 2.0 | Rate sells home-warranty / home-tech-support products. | _underscore/Model/Rate/SalesOrder.php, _underscore/Model/Rate/Item.php |
|
|
6
7
|
| [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
|
|
7
8
|
| [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
|
|
8
9
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Rate SalesOrder → NetSuite CashSale Export (postPost)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: rate
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-25
|
|
10
|
+
owners: ["rgirish"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Rate/SalesOrder.php
|
|
13
|
+
- _underscore/Model/Rate/Item.php
|
|
14
|
+
related:
|
|
15
|
+
- 2.0/apps/_underscore/features/netsuite-rest-client.md
|
|
16
|
+
- clients/rate/profile.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
Rate sells home-warranty / home-tech-support products. When a Rate SalesOrder is created,
|
|
22
|
+
the `_Model_Rate_SalesOrder::postPost` interceptor builds and writes a NetSuite **CashSale**
|
|
23
|
+
(not a SalesOrder). This doc covers two pieces of that export added 2026-06-25: passing the
|
|
24
|
+
order's ship-to (property) address onto the CashSale, and resolving each line item's NetSuite
|
|
25
|
+
item internalId from a dedicated column with a partNumber fallback.
|
|
26
|
+
|
|
27
|
+
This is Rate's override of the shared NetSuite write path documented in
|
|
28
|
+
`2.0/apps/_underscore/features/netsuite-rest-client.md`.
|
|
29
|
+
|
|
30
|
+
## Key files / entry points
|
|
31
|
+
|
|
32
|
+
| File | Role |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `_underscore/Model/Rate/SalesOrder.php` | `postPost` interceptor — builds the NetSuite CashSale (address + line items) |
|
|
35
|
+
| `_underscore/Model/Rate/Item.php` | `_Model_Rate_Item` — declares `c_netsuiteInternalItemId` so the column is API-exposed and UI-settable |
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
### Ship-to address → CashSale `shippingAddress`
|
|
40
|
+
The `postPost` SQL query was expanded to `LEFT JOIN Addresses` (via
|
|
41
|
+
`SalesOrders.shipToAddressId`) and `States`, selecting `line1`, `line2`, `city`, the
|
|
42
|
+
state code, and `zip`. The address is captured from the first result row and set as a
|
|
43
|
+
top-level `shippingAddress` on the CashSale — an `Address` object with `override=true`
|
|
44
|
+
and `country='_unitedStates'`.
|
|
45
|
+
|
|
46
|
+
This fires for **every** order that has a `shipToAddressId` set — it is **not** restricted
|
|
47
|
+
to the Whole Home Warranty SKU (1429124), because `shippingAddress` is a top-level field on
|
|
48
|
+
the CashSale, not a per-line concern. (The business driver was Whole Home Warranty orders
|
|
49
|
+
needing the property address in NetSuite so ops / account teams can determine regional
|
|
50
|
+
pricing, but the field applies to all orders.)
|
|
51
|
+
|
|
52
|
+
### Line item NetSuite internalId resolution
|
|
53
|
+
Each line item's NetSuite `RecordRef` internalId is now resolved as:
|
|
54
|
+
|
|
55
|
+
1. `Items.c_netsuiteInternalItemId` when populated, else
|
|
56
|
+
2. the hard-coded `partNumber` integer (1429122 / 1429123 / 1429124) as a fallback.
|
|
57
|
+
|
|
58
|
+
The query was updated to `SELECT Items.c_netsuiteInternalItemId` alongside `partNumber`.
|
|
59
|
+
The fallback exists because ops has not yet populated `c_netsuiteInternalItemId`.
|
|
60
|
+
|
|
61
|
+
### `c_netsuiteInternalItemId` field declaration
|
|
62
|
+
`Client_Rate.Items` has a `c_netsuiteInternalItemId INT` column that was not declared in the
|
|
63
|
+
ORM. It is now declared as `FIELD_INTEGER` on `_Model_Rate_Item`, so the column is exposed
|
|
64
|
+
through the API and can be set via the UI.
|
|
65
|
+
|
|
66
|
+
## Production data state (as of 2026-06-25)
|
|
67
|
+
|
|
68
|
+
- **All 39 Rate SalesOrders in `Client_Rate.SalesOrders` have `shipToAddressId = NULL`.**
|
|
69
|
+
The new code path only populates `shippingAddress` once the frontend starts setting
|
|
70
|
+
`shipToAddressId` on order creation. For the historical backfill export, the address was
|
|
71
|
+
pulled from the contact's `primaryContactAddressId → ContactAddresses → Addresses` join
|
|
72
|
+
instead — a one-off, not the going-forward path.
|
|
73
|
+
- **`Items.c_netsuiteInternalItemId` is NULL for all three Rate SKUs** (partNumbers
|
|
74
|
+
1429122, 1429123, 1429124). Until ops populates it, the partNumber-integer fallback is the
|
|
75
|
+
active internalId path.
|
|
76
|
+
|
|
77
|
+
## Gotchas / known issues
|
|
78
|
+
|
|
79
|
+
- **`shippingAddress` only flows when `shipToAddressId` is set.** No live order has it set
|
|
80
|
+
yet (all 39 are NULL), so the new address path is effectively dormant until the frontend
|
|
81
|
+
populates the field on creation. Don't assume the CashSale carries an address from current
|
|
82
|
+
prod data.
|
|
83
|
+
- **internalId fallback is partNumber, not the NS internalId.** Until ops backfills
|
|
84
|
+
`c_netsuiteInternalItemId`, CashSale lines reference the partNumber integer as the NS item
|
|
85
|
+
internalId. These happen to coincide for Rate today, but the clean path is the column.
|
|
86
|
+
- **Historical backfill used a different address source** (contact primary address), not
|
|
87
|
+
`shipToAddressId` — do not confuse the one-off backfill join with the going-forward order
|
|
88
|
+
path.
|
|
89
|
+
|
|
90
|
+
## Change history
|
|
91
|
+
|
|
92
|
+
- 2026-06-25 — Added ship-to address (`LEFT JOIN Addresses`/`States`) to the CashSale as a top-level `shippingAddress` (override=true, US); resolved line-item NS internalId from `c_netsuiteInternalItemId` with partNumber fallback; declared `c_netsuiteInternalItemId` on `_Model_Rate_Item`. Confirmed all 39 prod orders have NULL `shipToAddressId` and all 3 SKUs have NULL `c_netsuiteInternalItemId` (rgirish)
|
|
@@ -10,12 +10,13 @@ project: SAML SSO Gateway
|
|
|
10
10
|
client: rate
|
|
11
11
|
type: profile
|
|
12
12
|
status: active
|
|
13
|
-
updated: 2026-06-
|
|
13
|
+
updated: 2026-06-25
|
|
14
14
|
owners: ["rgirish", "bala"]
|
|
15
15
|
files: []
|
|
16
16
|
related:
|
|
17
17
|
- clients/rate/features/saml-sso.md
|
|
18
18
|
- clients/rate/features/monthly-reconciliation-report.md
|
|
19
|
+
- clients/rate/features/netsuite-cashsale-export.md
|
|
19
20
|
---
|
|
20
21
|
|
|
21
22
|
## Summary
|
package/package.json
CHANGED