toga-ai 1.0.328 → 1.0.330
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 +49 -5
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +71 -20
- package/knowledge/sessions/2026-07-14-bdr-build-tcox.md +78 -0
- 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/checker.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/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.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, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
|
|
10
10
|
| [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
|
|
11
11
|
| [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 |
|
|
@@ -6,11 +6,11 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- test/@dave/checker.php
|
|
13
|
-
-
|
|
13
|
+
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
14
14
|
- test/@dave/looper.php
|
|
15
15
|
- test/@dave/reconcile_netsuite_totals.php
|
|
16
16
|
- test/@dave/fixer.php
|
|
@@ -223,8 +223,8 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
223
223
|
`41100/41300/41500`, cost `51100/51200` (exact-number, never prefix — see the sale-import doc for the
|
|
224
224
|
sub-account traps). `revenue = Σ(−foreignamount)` over revenue-acct lines; `profit = Σ(−foreignamount)`
|
|
225
225
|
over **all** allowlisted lines (cost lines net profit down via debit-positive `foreignamount`). Group by
|
|
226
|
-
**(local salesRep, item)** → one Sales row per group; synthetic `lineNumber` from
|
|
227
|
-
`ksort(json_encode([salesRepId,itemId]))` then `1..N` — **fixer MUST key identically** or its upsert
|
|
226
|
+
**(local salesRep, item, customer)** → one Sales row per group; synthetic `lineNumber` from
|
|
227
|
+
`ksort(json_encode([salesRepId,itemId,customerId]))` then `1..N` — **fixer MUST key identically** or its upsert
|
|
228
228
|
dupes against the webhook importer's rows. JE `foreignamount`s are **cent-precision**, so a per-JE
|
|
229
229
|
sum-then-round equals the importer's per-salesRep round-then-sum **exactly** — this is what keeps
|
|
230
230
|
checker's "in sync" == fixer's "nothing to fix" for JEs (the compute-identically invariant applied to JEs).
|
|
@@ -249,6 +249,35 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
249
249
|
correctly re-derives "no raw rep" from NetSuite. One-time cleanup =
|
|
250
250
|
`fixer.php --commit --prod --category sales` over the JE window, then `checker.php` to confirm
|
|
251
251
|
(in current prod this was a no-op — 0 no-rep JE rows — so the importer filter is preventative).
|
|
252
|
+
- **`fixer.php` now resolves `customerId` on JE lines, mirroring the importer (TRUE-80129,
|
|
253
|
+
dfranks 2026-07-14).** Both `fixJournalEntries()` (the hand-maintained MIRROR of
|
|
254
|
+
`buildJournalEntryRows` — same account allowlist, same group key, same `ksort` ordering,
|
|
255
|
+
which must stay in lockstep or its upsert dupes against the webhook importer's rows) used to
|
|
256
|
+
hard-code `customerId = NULL` in its UPDATE and INSERT. The fix:
|
|
257
|
+
- Added a `Customers` lookup (reusing the exact pattern `fixOpportunities()` already uses in
|
|
258
|
+
the same file).
|
|
259
|
+
- Added **`tl.entity AS customer`** to the bulk-line SuiteQL SELECT. `tl.entity` is a
|
|
260
|
+
**NATIVE** column, so it needs **no** `> 0` / `IS NOT NULL` perf-trap workaround (unlike the
|
|
261
|
+
`custcol_*` salesRep/item columns — see the custom-column planner gotcha below); the comment
|
|
262
|
+
in-file notes native columns carry no such predicate risk.
|
|
263
|
+
- Extended the group key to `(salesRep, item, customer)` (`json_encode([$salesRepId, $itemId,
|
|
264
|
+
$customerId])`) to key **identically** to the importer.
|
|
265
|
+
- Added `customerId` to the **read-back SELECT + change-detection compare** (a deliberate guard
|
|
266
|
+
against the known "new column dropped from change-detection" footgun class), plus the UPDATE
|
|
267
|
+
and INSERT.
|
|
268
|
+
- New constant `JE_CUSTOMER_LINE_FIELD = 'entity'`. Non-Customer entities (vendor/employee
|
|
269
|
+
refs) resolve to null naturally — NetSuite internal ids are one global sequence across entity
|
|
270
|
+
subtypes, so a non-customer id simply misses `Customers`.
|
|
271
|
+
- **Backfilling `customerId` onto existing JE rows is a MANUAL, all-time developer step — not
|
|
272
|
+
part of the code PR (TRUE-80129 decision).** Because the checker (`journalEntryNsTotals()`)
|
|
273
|
+
only detects **revenue/profit** drift per JE, a JE row with a correct total but a missing
|
|
274
|
+
`customerId` is **not** flagged discrepant, so a normal checker/fixer pass will never
|
|
275
|
+
repopulate it — shipping the code fix alone does **not** backfill history. The chosen backfill
|
|
276
|
+
(run once, manually, by the developer after deploy): **DELETE all**
|
|
277
|
+
`netsuiteTransactionType='journalEntry'` rows from `Sales` (**no date-window restriction**),
|
|
278
|
+
then run **`fixer.php --commit --prod --category sales`** over full JE history so
|
|
279
|
+
`fixJournalEntries()` takes the INSERT branch for every group and repopulates every JE row
|
|
280
|
+
including `customerId`. Explicitly **out of scope for the code PR**.
|
|
252
281
|
- **JE item dimension is NULL today; here is how to enable it later.** `itemId` is null on JE Sales rows
|
|
253
282
|
because the JE import path reads item **only** via the configurable `JE_LINE_ITEM_FIELD` (currently null)
|
|
254
283
|
— it does **not** read native `transactionline.item`, so even a populated native `tl.item` would NOT flow
|
|
@@ -256,7 +285,8 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
256
285
|
`custcol_*` if a custom column is added) — `journalLineLookup` then resolves it via
|
|
257
286
|
`lookupId('Items','netsuiteItemInternalId',…)` and the group key already carries the item slot; AND (2)
|
|
258
287
|
update **`fixer.php`** to SELECT the item column in its JE SuiteQL, resolve NS item→local id (reuse
|
|
259
|
-
`createMissingForecastItemFromNetSuite` self-heal), and
|
|
288
|
+
`createMissingForecastItemFromNetSuite` self-heal), and keep the group key `(salesRep, item, customer)`
|
|
289
|
+
(customer was added under TRUE-80129; the item slot is already present).
|
|
260
290
|
**`checker.php` needs no change.** Consider whether item should self-heal (salesRep does **not** — see
|
|
261
291
|
the sale-import doc). The salesRep dimension itself is the JE line custom column `custcol_sales_rep_line`
|
|
262
292
|
(wired under TRUE-79862).
|
|
@@ -519,6 +549,20 @@ None — Forecast2 is a single shared dataset.
|
|
|
519
549
|
|
|
520
550
|
## Change history
|
|
521
551
|
|
|
552
|
+
- 2026-07-14 — **`fixer.php` now resolves `customerId` on JE lines, mirroring the importer +
|
|
553
|
+
recorded the manual all-time backfill** (TRUE-80129, dfranks). `fixJournalEntries()` (the
|
|
554
|
+
hand-maintained mirror of `buildJournalEntryRows`) previously hard-coded `customerId = NULL`;
|
|
555
|
+
it now adds a `Customers` lookup (same pattern as `fixOpportunities()`), selects
|
|
556
|
+
**`tl.entity AS customer`** (a **native** column → no `> 0`/`IS NOT NULL` perf-trap workaround
|
|
557
|
+
needed), extends the group key to `(salesRep, item, customer)` — keying identically to the
|
|
558
|
+
importer or its upsert dupes — and adds `customerId` to the read-back SELECT + change-detection
|
|
559
|
+
compare (guarding the "column dropped from change-detection" footgun) + UPDATE + INSERT. New
|
|
560
|
+
const `JE_CUSTOMER_LINE_FIELD = 'entity'`. Also documented that the **backfill of existing JE
|
|
561
|
+
rows is a manual, all-time developer step** (DELETE all `journalEntry` Sales rows, then
|
|
562
|
+
`fixer.php --commit --prod --category sales` over full JE history) — the checker only flags
|
|
563
|
+
revenue/profit drift, so a missing-only `customerId` is never auto-detected. Corrected the
|
|
564
|
+
misattributed `files:` path: `_Component_Forecast_SaleImport` lives in **worker2**, not
|
|
565
|
+
`_underscore`. (dfranks)
|
|
522
566
|
- 2026-07-07 — **Recorded that these reconcilers are the only backstop for NetSuite sublist
|
|
523
567
|
line-field inline edits + added `Logs.Webhook` forensics** (SALES profit-drift forensics; invoice
|
|
524
568
|
6715127, 2026-02-20, $3,392.70). A line-level inline edit (e.g. `TRANLINE.MCOSTESTIMATE`) fires no
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| [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 |
|
|
15
15
|
| [Error Reporting — Issue/Event Aggregation (agreed POST-to-receiver design)](features/error-reporting-issue-event.md) | Platform-wide error-reporting infrastructure for TOGA 2.0, built around a two-table **Issue / Event** aggregation model in the shared **Core Logs DB**. | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
16
16
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
17
|
-
| [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). |
|
|
17
|
+
| [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). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.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/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
18
18
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
|
|
19
19
|
| [_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 |
|
|
20
20
|
| [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 |
|
|
@@ -6,11 +6,11 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
13
|
+
- worker2/Component/Forecast/Db/Db.php
|
|
14
14
|
- _underscore/Component/Api/Netsuite/Netsuite.php
|
|
15
15
|
- worker2/Worker/Netsuite/Invoice.php
|
|
16
16
|
- worker2/Worker/Netsuite/CashSale.php
|
|
@@ -57,14 +57,20 @@ The shared engine is `_Component_Forecast_SaleImport`; thin worker2 handlers
|
|
|
57
57
|
- **Sign convention differs from the cron** — the raw REST record is all-positive, so the
|
|
58
58
|
webhook applies the sign factor **uniformly to every line**, not just shipping. Copying
|
|
59
59
|
the cron's sign handling reintroduces the credit-memo sign bug. (See Sign convention.)
|
|
60
|
-
- The engine is
|
|
61
|
-
|
|
60
|
+
- **The engine is checked into the `worker2` repo, NOT `_underscore`** — despite its
|
|
61
|
+
`_Component_*` name. `_Component_Forecast_SaleImport` and `_Component_Forecast_Db` both
|
|
62
|
+
live under `worker2/Component/Forecast/…` and declare `namespace worker;` (the project-local
|
|
63
|
+
`_Component_*` requirement). The class-name convention alone does **not** imply an
|
|
64
|
+
`_underscore` home — planning/context tools repeatedly assert `_underscore` and are **wrong**;
|
|
65
|
+
a `git` checkout of `_underscore` has **no** `Component/Forecast/SaleImport/SaleImport.php`.
|
|
66
|
+
Only `_Component_Api_Netsuite` (the REST client, `fetchRecord`/`createRecord`) is genuinely in
|
|
67
|
+
`_underscore`. (See Gotchas.)
|
|
62
68
|
|
|
63
69
|
## Key files / entry points
|
|
64
70
|
| File | Role |
|
|
65
71
|
|---|---|
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
72
|
+
| `worker2/Component/Forecast/SaleImport/SaleImport.php` | the engine (shared body for all four types); checked into **worker2**, declares `namespace worker;` |
|
|
73
|
+
| `worker2/Component/Forecast/Db/Db.php` | shared NS-REST + sublist-pagination helpers (also **worker2**, `namespace worker;`) |
|
|
68
74
|
| `worker2/Worker/Netsuite/Invoice.php` etc. | four sale-type thin handlers; `post`/`put` → `sync`, `delete` → `removeAll` |
|
|
69
75
|
| `worker2/Worker/Netsuite/JournalEntry.php` | thin JE handler; `post`/`put` → `syncJournalEntry`, `delete` → `removeAllJournalEntry` |
|
|
70
76
|
| `test/@dave/test_je_lifecycle.php` | JE lifecycle harness (direct-engine create/update/delete) |
|
|
@@ -237,8 +243,34 @@ item-line upsert/reconcile machinery (`syncLines`/`guardedInsert`/`deleteRows`).
|
|
|
237
243
|
`51200→249` COGS Service. Read-only resolver tool: `test/@dave/probe_je_accounts.php`.
|
|
238
244
|
- **Likely future one-line tweak:** if the Agent sub-accounts should count, add `41300.01` to
|
|
239
245
|
the revenue list and `51200.01` to the cost list.
|
|
240
|
-
-
|
|
241
|
-
|
|
246
|
+
- **Customer is now resolved on JE lines and is a THIRD grouping dimension (TRUE-80129,
|
|
247
|
+
dfranks 2026-07-14).** `buildJournalEntryRows` previously hard-coded `customerId = null` on
|
|
248
|
+
every JE row. It now reads `JE_LINE_CUSTOMER_FIELD = 'entity'` — a **NATIVE JE-line reference
|
|
249
|
+
field**, unlike the `custcol_*` salesRep/item fields — via the same `journalLineLookup()`
|
|
250
|
+
bare-lookup helper, resolving against `Customers.netsuiteInternalId` (the identical
|
|
251
|
+
table/column the four sale-transaction types already use for `customerId`). `customerId` is
|
|
252
|
+
folded into the group key as a third dimension (see below), so JE lines sharing rep/item but
|
|
253
|
+
differing by customer now split into **separate** `Forecast.Sales` rows.
|
|
254
|
+
- **Non-Customer entities resolve to null with no explicit type check needed** — a JE line's
|
|
255
|
+
`entity` may be a vendor or employee ref, but NetSuite **internal ids are a single global
|
|
256
|
+
sequence across all entity subtypes** (customer/vendor/employee never share an id), so a
|
|
257
|
+
vendor/employee id simply misses `Customers.netsuiteInternalId` and yields null. No
|
|
258
|
+
`is-customer` guard required. (Reusable NetSuite fact: internal ids are globally unique
|
|
259
|
+
across record subtypes, not namespaced per subtype.)
|
|
260
|
+
- Group key uses `json_encode([salesRep, item, customer])` (not a string-join) to stay
|
|
261
|
+
collision-safe — an empty-string separator would let `"5|" + null` collide. (The customer
|
|
262
|
+
slot was added under TRUE-80129; the fixer mirror MUST key identically — see the
|
|
263
|
+
reconciliation doc.)
|
|
264
|
+
|
|
265
|
+
**Backfilling `customerId` onto existing JE rows requires a manual, all-time re-import — the
|
|
266
|
+
code fix alone does NOT backfill (TRUE-80129 decision).** The checker/fixer only detect
|
|
267
|
+
**revenue/profit** drift per JE, so an already-correct-total JE row that is merely **missing
|
|
268
|
+
`customerId`** is **not** flagged discrepant and a normal fixer pass will never repopulate it.
|
|
269
|
+
The chosen backfill (run **manually, once, by the developer after deploy — explicitly out of
|
|
270
|
+
scope for the code PR**): `DELETE` **all** `netsuiteTransactionType='journalEntry'` rows from
|
|
271
|
+
`Sales` (no date-window restriction), then run `fixer.php --commit --prod --category sales`
|
|
272
|
+
over full JE history so `fixJournalEntries()` takes the INSERT branch for every group and
|
|
273
|
+
repopulates all JE rows including `customerId`. (See the reconciliation doc for the fixer side.)
|
|
242
274
|
|
|
243
275
|
**Reversal handling (a JE-only pattern):**
|
|
244
276
|
- Setting a reversal date makes NetSuite auto-create a **paired reversing JE**, but **only
|
|
@@ -524,21 +556,40 @@ success from a `Forecast.Sales` row alone.
|
|
|
524
556
|
webhook or the daily discrepancy-fix.
|
|
525
557
|
- **Local _underscore boots on PHP 8.0.30 (xampp8)** — the floor enforced in `_underscore.php` is
|
|
526
558
|
**8.0.0**, not 8.1.
|
|
527
|
-
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
559
|
+
- **This engine is a project-local `_Component_*` that lives in `worker2`, NOT `_underscore` —
|
|
560
|
+
correcting the earlier assumption.** `_Component_Forecast_SaleImport` and
|
|
561
|
+
`_Component_Forecast_Db` are checked into **`worker2/Component/Forecast/…`** and declare
|
|
562
|
+
`namespace worker;`. A `_Component_*`/`_Model_*` loaded from a project path is legal **only
|
|
563
|
+
when it declares its project namespace** — omit it and the autoloader throws "namespace …
|
|
564
|
+
has not been defined" at **class-load (runtime), not `php -l`** (this exact global-namespace
|
|
565
|
+
mistake caused the SALES drift: webhooks fatally threw before writing, presenting as 7-month
|
|
566
|
+
historical drift). So the naming convention does **not** put the file in `_underscore`; the
|
|
567
|
+
namespace declaration is what makes the worker2 location valid. It references the worker2
|
|
568
|
+
class `_Worker_Netsuite_Item`, resolved lazily at runtime. Only `_Component_Api_Netsuite`
|
|
569
|
+
(the REST client) is actually in `_underscore`. **Planning/context tooling has repeatedly
|
|
570
|
+
mis-asserted an `_underscore` home for this engine — verify against a `git` checkout, not the
|
|
571
|
+
class name.** See the
|
|
537
572
|
[namespace-registration doc](./component-model-namespace-registration.md) for the
|
|
538
|
-
eval-bridge mechanism and the `catch`/`throw`/`extends` backslash-qualify traps
|
|
573
|
+
eval-bridge mechanism and the `catch`/`throw`/`extends` backslash-qualify traps.
|
|
539
574
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
540
575
|
|
|
541
576
|
## Change history
|
|
577
|
+
- 2026-07-14 — **JE lines now resolve `customerId` (was hard-coded null) + corrected the
|
|
578
|
+
engine's repo location to worker2** (TRUE-80129, dfranks). `buildJournalEntryRows` reads
|
|
579
|
+
`JE_LINE_CUSTOMER_FIELD = 'entity'` — a **native** JE-line reference field (not a `custcol_*`,
|
|
580
|
+
unlike salesRep/item) — via `journalLineLookup()` against `Customers.netsuiteInternalId` (same
|
|
581
|
+
table/column the four sale types use), and folds `customerId` into the group key as a third
|
|
582
|
+
dimension: `json_encode([salesRep, item, customer])`. Non-Customer entities (vendor/employee
|
|
583
|
+
refs) resolve to null with no type check because **NetSuite internal ids are one global
|
|
584
|
+
sequence across all entity subtypes**. Recorded that the backfill of existing JE rows is a
|
|
585
|
+
**manual, all-time** developer step (DELETE all `journalEntry` Sales rows, then
|
|
586
|
+
`fixer.php --commit --prod --category sales` over full JE history) — the code fix alone does
|
|
587
|
+
NOT backfill because the checker only flags revenue/profit drift, not a missing customerId.
|
|
588
|
+
**Also corrected a long-standing misattribution:** `_Component_Forecast_SaleImport` and
|
|
589
|
+
`_Component_Forecast_Db` are checked into the **worker2** repo (`namespace worker;`), NOT
|
|
590
|
+
`_underscore` — the `_Component_*` name does not imply an `_underscore` home; a `git` checkout
|
|
591
|
+
of `_underscore` has no such file. Only `_Component_Api_Netsuite` is actually in `_underscore`.
|
|
592
|
+
(dfranks)
|
|
542
593
|
- 2026-07-08 — **Added the webhook-vs-fixer diagnostic playbook** for proving whether inbound
|
|
543
594
|
NetSuite webhooks actually wrote `Forecast.Sales`: correlate `Logs.Webhook` (prod-logs;
|
|
544
595
|
receipt-only, NO status column) against `Core.WorkerJobs` (prod-core; `isSuccess`/
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: bdr-build
|
|
4
|
+
title: BDR web funnel built through Phase 5 (scaffold to real backend)
|
|
5
|
+
author: tcox
|
|
6
|
+
repos: [bdr, ai-bdr]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: shared
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-07-14
|
|
11
|
+
updated: 2026-07-14
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: bdr-build
|
|
15
|
+
**Date:** 2026-07-14
|
|
16
|
+
**Project/Repo:** bdr (2.0) — local `C:\WWW\BDR`, GitHub `agilantsolutions/BDR`, branch `BDR-Development`
|
|
17
|
+
**Task:** Execute PLAN.md Phases 0-5 of the BDR web funnel: get the repo onto GitHub, scaffold the Next.js app, and build the content layer, component library, all screens/flow, and the real HubSpot/Toga backend.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
- **Repo now on GitHub.** `C:\WWW\BDR` git-initialized, branched off remote `_main` (README preserved), pushed to `agilantsolutions/BDR` branch **`BDR-Development`** (developer renamed it from feature/initial-plan-and-mockup). 8 commits: `3a1db6b` plan+mockup import (323 files, `.claude/` gitignored), `08eaf65` PLAN.md path/registry fixes, then Phases 0-5.
|
|
23
|
+
- **CLI git auth PERMANENTLY FIXED on this machine.** `git config --global credential.helper manager` + one interactive `git fetch` popped the GCM browser sign-in; credentials stored. Push/fetch now work from CLI for ALL repos incl. the knowledge repo (`/capture` pushes work). The old "push via GitHub Desktop" memory was deleted as obsolete.
|
|
24
|
+
- **Phase 0** (`dcc3e30`): Next.js 16.2.3 + React 19.2.4 + TS scaffold mirroring `info`'s config. Mockup `styles.css` ported as `src/app/studio.css`. `resolveCampaignId` (?campaign= -> hsCampaignId map -> `default`, slug regex `^[a-z0-9][a-z0-9-]{0,63}$`). ESLint flat-config no-em-dash rule (Literal/TemplateElement/JSXText selectors) — proven to fire. amplify.yml + .env.example.
|
|
25
|
+
- **Phase 1** (`960fe34`): `src/content/` schema.ts (CampaignBundle contract), provider.ts (`resolveCampaign` never throws; unknown slug serves DEFAULT content with requested id), hubspotProvider.ts stub (throws, gated on §9.10), useCampaign.tsx context hook, default.ts = mockup copy verbatim extracted by subagent. Vitest wired (server-only shimmed via alias).
|
|
26
|
+
- **Phase 2** (`90fd7ed`): components.jsx ported to 16+ typed client modules (Ico 27-icon registry + iconFor, AgentArt/SmoothImg/TalkingPreview, VoiceDial+VoiceAudio, ColorSelector, SwapFade with settle guards verbatim, Particles, StepRail, SynapseButton...). `src/lib/agentData.ts` (only home for accent hexes) + `assetPath.ts` (replaces ASSET()/window.__resources, preloadAgentAssets). Assets pruned per §5.8 to 6.2MB (Alex slot-1 art+talk video, walkthrough, brand SVGs).
|
|
27
|
+
- **Phase 3** (`64553b1`): Capabilities.tsx fully config-driven (Q&A pill rotation with mockup fade phases + same-tick click guard), thin AgentFlowRoot (accent -> :root, --mx/--my hover, scroll tint), RichText.tsx `agentize()` replaces `<agent>` token with bundle.agent.name. Schema calibrations: `chrome`, eyebrows, `qbank`, connectNote -> {label,value}.
|
|
28
|
+
- **Phase 4** (`85a4772`): useAgentFlow state machine (go()/back map, stepFor, modal gates, aibdr.v2 localStorage — only voice survives refresh), AutoBuild (verbatim ms timeline, lands on CAMPAIGN agent not hardcoded Alex), CallNow, Schedule (12-month, 30-min slots), Success (calling simulation + summaries + AmbientQuestions), Welcome/WelcomeStep2/EarlyAccess modals, leadActions seam stubs. Creator + TweaksPanel deliberately NOT ported (parked, §9.3/§9.7).
|
|
29
|
+
- **Phase 5** (`34a3910`): `src/server/` toga.ts + hubspot.ts ported from `info` with ALL PII logging stripped (identifier-only logs), validation.ts (16KB body cap, phone/uuid/ISO checks), leadSink.ts + callbackService.ts adapters, /api/call-now + /api/call-later + /api/contact route handlers (always envelope-shaped), page.tsx primes lead from ?hsContactId server-side and NEVER fails rendering, leadActions POST for real. info's dead 501 `visit` route dropped deliberately.
|
|
30
|
+
- **Verification discipline held every phase:** tsc + eslint + vitest + next build + live SSR curl checks before each commit. Final: **67 tests / 7 files green**. Live checks: `?campaign=healthcare` -> data-campaign="healthcare"; POST /api/call-now `{}` -> 400 envelope (not 500); missing env -> 500 envelope, funnel still renders.
|
|
31
|
+
- **Subagent pipeline worked well:** 4 background implementer agents (content extraction, component port, Capabilities, Phase 4 flow, Phase 5 backend) each verified before returning; main thread stayed conductor.
|
|
32
|
+
|
|
33
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
34
|
+
- **`sed`/`perl` in-place replacement of literal `\n` escapes through the Bash tool failed silently** (file unchanged; the `\"` expression in the same sed call DID apply — escaping is mangled through the Windows Git Bash wrapper). Fix that worked: the **Edit tool** with exact old/new strings. The mockup's styles.css lines 759 + 840 contain literal `\n` / `\"` sequences (JS-string generation artifacts) that break the CSS parser — already fixed in `src/app/studio.css`; `mockup/styles.css` left pristine (still broken) on purpose.
|
|
35
|
+
- **`pkill` does not kill Windows processes from Git Bash** — it exits quietly and the old `next start` keeps holding port 3000, serving a STALE build (looks like your new code didn't build). Kill via PowerShell `Get-NetTCPConnection -LocalPort 3000 | Stop-Process` or `taskkill //PID <pid> //F` (netstat to find PID).
|
|
36
|
+
- **The Bash tool's cwd resets to `c:\WWW` between some calls** — `npm run <script>` then fails with "Missing script" because c:\WWW has no package.json. Always `cd /c/WWW/BDR &&` first.
|
|
37
|
+
- **Pre-commit path scan regex missed `c:\WWW\...` single-backslash absolute paths** (pattern only matched escaped/forward-slash forms) — PLAN.md was pushed with machine paths in it (git-workflow violation), fixed in follow-up `08eaf65`. Scan for `[A-Za-z]:[\\/]` case-insensitively next time.
|
|
38
|
+
- **ESLint linted `mockup/**` (110 errors)** until `mockup/**` was added to globalIgnores — the prototype is reference material, not shipped code.
|
|
39
|
+
- **CLI push/fetch without GCM sign-in**: `fatal: could not read Username` — resolved permanently (see What WORKED); do not fall back to GitHub Desktop workflows.
|
|
40
|
+
|
|
41
|
+
## Not tried yet (candidates for next session)
|
|
42
|
+
- **§9.10 decision (gates Phase 6):** how campaign content is modeled in HubSpot (HubDB likely vs custom object; nested Q&A pool/sets/callSummaries are the hard part). Needs the HubSpot instance owner. Then: implement hubspotProvider (fetch by slug + map + revalidation cache + no-em-dash normalization), seed campaign 1 from default.ts, slim DEFAULT to engineering fallback.
|
|
43
|
+
- **Phase 6:** second campaign (healthcare proof points already in the Q pool) proving zero-code campaign adds.
|
|
44
|
+
- **Phase 7:** pixel-parity pass vs mockup screens/screenshots, GA4 (react-ga4 installed, not wired), accessibility.
|
|
45
|
+
- **Phase 8:** Amplify SSR app creation + env vars (values from info's deployment), `togatech` integration mechanism (§9.6), PR + merge strategy for `BDR-Development` -> `_main`.
|
|
46
|
+
- **Local run QA:** developer was about to `npm run dev` and walk the funnel; feedback may yield fidelity fixes.
|
|
47
|
+
- Success screen consuming `submitOk` (currently exposed by useAgentFlow, not yet surfaced in UI).
|
|
48
|
+
|
|
49
|
+
## Current file state
|
|
50
|
+
| File | Status | Notes |
|
|
51
|
+
|------|--------|-------|
|
|
52
|
+
| `C:\WWW\BDR` (whole repo) | 8 commits pushed to `BDR-Development` | Working tree clean at `34a3910`. No PR opened (developer said "no PR to do yet"). |
|
|
53
|
+
| `src/app/` | layout + page (server: resolve slug -> bundle -> lead prime) + studio.css (fixed) + api/{call-now,call-later,contact} | |
|
|
54
|
+
| `src/content/` | schema.ts, default.ts (mockup copy interim), provider.ts, hubspotProvider.ts (throwing stub), useCampaign.tsx | |
|
|
55
|
+
| `src/components/`, `src/flow/`, `src/server/`, `src/lib/` | Complete per phases 2-5 | Creator/TweaksPanel not ported (parked). |
|
|
56
|
+
| `test/` (7 files, 67 tests) | All green | Includes route validation, CST formatting, slots, phone. |
|
|
57
|
+
| `knowledge/2.0/apps/ai-bdr/*` (team repo) | NOT yet re-synced with Phase 3/4 schema calibrations or build progress | `/capture` pending — run it next session start or after QA. |
|
|
58
|
+
|
|
59
|
+
## Decisions made
|
|
60
|
+
- **No Tailwind** (deviation from mirroring info): Tailwind 4's preflight reset would fight the mockup's hand-authored CSS and threaten pixel parity; PLAN §5.1 makes it optional. Rejected: including it for config parity.
|
|
61
|
+
- **DEFAULT bundle carries the mockup's copy verbatim as interim dev content** (documented in default.ts header) until HubSpot is seeded post-§9.10, then slim to engineering fallback. Rejected: building screens against lorem-ipsum minimal DEFAULT (no parity possible).
|
|
62
|
+
- **Schema calibrated by building, not upfront:** Phase 3/4 added chrome, qbank, eyebrows, earlyAccess/phone/recap groups, connectNote {label,value}, template tokens `<agent>/<date>/<tz>/<time>/<detail>`. The §6 sketch was a starting point; the code schema is now the contract.
|
|
63
|
+
- **AutoBuild lands on `bundle.agent`**, not hardcoded Alex — agent identity is campaign config (PLAN §2.2 design implication).
|
|
64
|
+
- **info's `visit` route dropped** (permanent 501 stub, no caller): dead attack surface. Recorded in contact route header.
|
|
65
|
+
- **PII logging stripped at port time** (info debt): identifier-only logs everywhere; Toga error paths strip query strings (contact lookup carries email in query).
|
|
66
|
+
- **`.claude/` harness gitignored** in the BDR repo (machine-local, installed via npx toga-ai; committing snapshots the whole KB stale).
|
|
67
|
+
- **GCM one-time sign-in** chosen over GitHub Desktop-only pushes: fixes CLI push machine-wide, permanently.
|
|
68
|
+
|
|
69
|
+
## Blockers
|
|
70
|
+
- **§9.10 HubSpot content mechanism undecided** — gates hubspotProvider + campaign seeding (Phase 6). Owner: whoever runs the HubSpot instance.
|
|
71
|
+
- **First-campaign direction/copy from leadership** still pending (fills bundle 1, doesn't block engine work).
|
|
72
|
+
- **Real-call testing needs env values** (HUBSPOT_ACCESS_TOKEN + 4 TOGA_* vars) from info's deployment — deliberate action, hits live Toga and can trigger an actual callback.
|
|
73
|
+
|
|
74
|
+
## Exact next step
|
|
75
|
+
> `cd C:\WWW\BDR && npm run dev`, open http://localhost:3000, walk the full funnel (Welcome -> Build my agent -> AutoBuild -> Capabilities -> Call Now/Schedule -> Success) side-by-side with `mockup/AI BDR (standalone).html`, and note fidelity gaps for the Phase 7 parity pass. Then run `/capture` to re-sync the ai-bdr knowledge docs (schema calibrations + build progress).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
_Saved by /session-save on 2026-07-14_
|
package/package.json
CHANGED