toga-ai 1.0.329 → 1.0.331
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/2.0/apps/ai-bdr/features/bdr-web-funnel-plan.md +15 -2
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-app.md +143 -0
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-content-model.md +48 -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/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`/
|
|
@@ -6,7 +6,7 @@ project: "AI-BDR"
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: draft
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
@@ -16,8 +16,15 @@ files:
|
|
|
16
16
|
related:
|
|
17
17
|
- architecture.md
|
|
18
18
|
- web-funnel-content-model.md
|
|
19
|
+
- web-funnel-app.md
|
|
19
20
|
---
|
|
20
21
|
|
|
22
|
+
> **Execution status (2026-07-14):** PLAN **Phases 0–5 are BUILT and QA'd** — the web
|
|
23
|
+
> app engine, full mockup port, and real backend are done. Remaining work is
|
|
24
|
+
> decision-gated, not code (§9.10 HubSpot mechanism, leadership copy, Phase 7 polish,
|
|
25
|
+
> Phase 8 deploy). The built codebase is documented in `web-funnel-app.md`; this doc
|
|
26
|
+
> stays a plan mirror. This body still mirrors `BDR/PLAN.md`.
|
|
27
|
+
|
|
21
28
|
# BDR: Multi-Campaign "AI BDR / Agent Studio" Web App — Implementation Plan
|
|
22
29
|
|
|
23
30
|
> **Status:** Draft 2 (needs one more review). Open items tracked in §9; final campaign direction /
|
|
@@ -194,7 +201,7 @@ the bundle carries the UUID so a slug maps to the right campaign for logging.
|
|
|
194
201
|
## 4. Decisions locked at kickoff
|
|
195
202
|
| Decision | Choice | Consequence |
|
|
196
203
|
|---|---|---|
|
|
197
|
-
| Repo name | **`bdr`** |
|
|
204
|
+
| Repo name | **`bdr`** | Greenfield repo, registered in the TOGA registry (`agilantsolutions/BDR`). |
|
|
198
205
|
| Framework | **Next.js (App Router) + TypeScript** | Chosen because the site is highly public: SSR + SEO, and a built-in server for the secret-bearing HubSpot/Toga/content calls. Matches `info`, so its patterns port directly. |
|
|
199
206
|
| UI source | **The mockup** | Rebuild its Agent Studio flow faithfully (pixel-perfect). The mockup is React, so components port cleanly into Next.js client components. |
|
|
200
207
|
| Backend source | **`info`** | Reuse HubSpot/Toga/callback logic directly (same framework). |
|
|
@@ -551,5 +558,11 @@ so fidelity should be near-exact.
|
|
|
551
558
|
- [ ] Pixel-perfect parity pass against the mockup screens + screenshots.
|
|
552
559
|
|
|
553
560
|
## Change history
|
|
561
|
+
- 2026-07-14 — Phases 0–5 executed and QA'd (engine + full mockup port + real backend);
|
|
562
|
+
built codebase now documented in `web-funnel-app.md`. `BDR/PLAN.md` itself corrected:
|
|
563
|
+
absolute local paths genericized, and the stale "not yet in TOGA registry" note fixed
|
|
564
|
+
(bdr IS registered). Repo now lives at `agilantsolutions/BDR` (branch `BDR-Development`);
|
|
565
|
+
the local `.claude/` harness is gitignored (devs pull it via `npx toga-ai`). Body still
|
|
566
|
+
mirrors PLAN.md. (tcox)
|
|
554
567
|
- 2026-07-13 — Draft 2 resync: re-mirrored the body from the current `BDR/PLAN.md`. Framework locked to Next.js App Router (earlier React/Vite SPA idea dropped); content store decided HubSpot over Contentful (rationale in §4/§5.2); no-hardcoding (HubSpot-owned, fetched by slug from the start); same-product framing confirmed (front door of the existing AI-BDR backend, "backend relationship" open question resolved); meeting-specific dates/cadence removed. Still a draft. (tcox)
|
|
555
568
|
- 2026-07-08 — Captured the full BDR web-funnel implementation plan verbatim into the knowledge repo as a companion to the distilled `web-funnel-content-model.md` feature doc (redundancy intentional). (tcox)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Web Funnel — Built Next.js App (structure, stack, how to run)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-14
|
|
10
|
+
owners: [tcox]
|
|
11
|
+
files:
|
|
12
|
+
- bdr/PLAN.md
|
|
13
|
+
- bdr/.gitignore
|
|
14
|
+
- bdr/eslint.config.mjs
|
|
15
|
+
- bdr/amplify.yml
|
|
16
|
+
- bdr/src/app
|
|
17
|
+
- bdr/src/lib/resolveCampaignId.ts
|
|
18
|
+
- bdr/src/content
|
|
19
|
+
- bdr/src/components
|
|
20
|
+
- bdr/src/flow
|
|
21
|
+
- bdr/src/server
|
|
22
|
+
- bdr/src/app/api
|
|
23
|
+
- bdr/test
|
|
24
|
+
- bdr/public
|
|
25
|
+
- bdr/mockup/styles.css
|
|
26
|
+
related:
|
|
27
|
+
- web-funnel-content-model.md
|
|
28
|
+
- bdr-web-funnel-plan.md
|
|
29
|
+
- ../architecture.md
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What this is
|
|
33
|
+
|
|
34
|
+
The **built** state of the BDR web funnel — the Next.js UI / lead-capture front
|
|
35
|
+
door of the existing AI-BDR product (see `../architecture.md`). The forward-looking
|
|
36
|
+
plan lives in `bdr-web-funnel-plan.md`; the config-driven content contract lives in
|
|
37
|
+
`web-funnel-content-model.md`. **This doc covers the actual shipped codebase: the
|
|
38
|
+
stack, the directory map, what was ported vs. parked, the test suite, and the
|
|
39
|
+
decisions that are load-bearing for anyone touching the repo.**
|
|
40
|
+
|
|
41
|
+
As of this writing **PLAN Phases 0–5 are BUILT and QA'd** — the web app engine and
|
|
42
|
+
full mockup port are complete, all screens/modals render config-driven from a
|
|
43
|
+
bundle, and the real backend is wired behind adapters. Developer QA verdict: the UI
|
|
44
|
+
is "very close, minimal adjustments to be near exact" vs. the mockup. Remaining work
|
|
45
|
+
is **decision-gated, not code** (see "What is parked / not built").
|
|
46
|
+
|
|
47
|
+
- **Repo:** `agilantsolutions/BDR` on GitHub, working branch `BDR-Development`. The
|
|
48
|
+
machine-local `.claude/` harness is **gitignored** — committing it would snapshot
|
|
49
|
+
the whole KB stale; devs get the harness via `npx toga-ai`, not from this repo.
|
|
50
|
+
|
|
51
|
+
## Stack
|
|
52
|
+
|
|
53
|
+
- **Next.js 16.2.3, App Router + TypeScript**, mirroring the `info` repo's config.
|
|
54
|
+
- **No Tailwind (deliberate).** Tailwind 4's preflight reset would fight the mockup's
|
|
55
|
+
hand-authored CSS (ported verbatim as `src/app/studio.css`) and threaten pixel
|
|
56
|
+
parity. Do **not** add Tailwind to this repo — the styling is intentionally the
|
|
57
|
+
mockup's own CSS-variable system. See the styles.css escape-sequence gotcha below.
|
|
58
|
+
- **ESLint flat config** carries a **no-em-dash rule** on shipped strings
|
|
59
|
+
(`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
|
|
60
|
+
rule on in-repo strings (`eslint.config.mjs`).
|
|
61
|
+
- **Vitest** test suite: **67 tests across 7 files, all green.** Every phase was
|
|
62
|
+
verified with `tsc` + lint + build + a live SSR `curl` before commit.
|
|
63
|
+
- **Deploy target:** AWS Amplify (SSR), `amplify.yml` present (Phase 8 not done).
|
|
64
|
+
|
|
65
|
+
## Directory map (what got built)
|
|
66
|
+
|
|
67
|
+
- `src/app/*` — App Router shell + pages; `page.tsx` is a server component that
|
|
68
|
+
resolves the campaign and primes the lead server-side (from `?hsContactId`) and
|
|
69
|
+
**never fails rendering**. `src/app/studio.css` is the mockup CSS ported verbatim.
|
|
70
|
+
- `src/lib/resolveCampaignId.ts` — campaign entry resolution: `?campaign=<slug>` →
|
|
71
|
+
`?hsCampaignId=` mapped → `DEFAULT`. Slug regex `^[a-z0-9][a-z0-9-]{0,63}$`.
|
|
72
|
+
- `src/content/*` — the content layer (schema/provider/default/hook). See
|
|
73
|
+
`web-funnel-content-model.md` for the contract; the schema is now THE contract.
|
|
74
|
+
- `src/components/*` — full mockup component library port: `Ico` (27-icon registry
|
|
75
|
+
hydrating bundle icon keys), `SwapFade`, `TalkingPreview`, `VoiceDial`, etc., with
|
|
76
|
+
the mockup's motion hardening ported **verbatim** (do not "simplify" the settle
|
|
77
|
+
guards / effect-owned timers away).
|
|
78
|
+
- `src/flow/*` — `useAgentFlow` state machine (the `go()` / `back` map, modal gates)
|
|
79
|
+
plus the screens. Persists to `localStorage` (`aibdr.v2`) where **only voice
|
|
80
|
+
survives a refresh**; the flow restarts at Welcome.
|
|
81
|
+
- `src/server/*` — the real backend, ported from `info`'s `toga.ts` / `hubspot.ts`
|
|
82
|
+
behind `LeadSink` + `CallbackService` adapters (detail in
|
|
83
|
+
`web-funnel-content-model.md`).
|
|
84
|
+
- `src/app/api/*` — route handlers `call-now`, `call-later`, `contact`.
|
|
85
|
+
- `public/*` — assets pruned to the **default campaign only** (~6.2 MB).
|
|
86
|
+
- `test/*` — the 67-test Vitest suite.
|
|
87
|
+
|
|
88
|
+
## How the built app behaves (key runtime facts)
|
|
89
|
+
|
|
90
|
+
- **Agent identity is campaign config.** AutoBuild lands on the campaign's
|
|
91
|
+
`bundle.agent`, **not** a hardcoded "Alex" — the presented agent (art, accent,
|
|
92
|
+
voice, name) comes from the resolved bundle. The manual Creator is parked, so the
|
|
93
|
+
user does not pick the agent in the default flow.
|
|
94
|
+
- **Template tokens** `<agent>` / `<date>` / `<tz>` / `<time>` / `<detail>` in bundle
|
|
95
|
+
strings are substituted at render by an `agentize()` helper.
|
|
96
|
+
- **Funnel always renders.** `resolveCampaign` never throws — an unknown slug or a
|
|
97
|
+
provider failure returns the `DEFAULT` bundle. The server page primes the lead but
|
|
98
|
+
never fails rendering on a bad/absent contact.
|
|
99
|
+
- **Boundary validation on every route handler.** 16 KB body cap; phone / UUID / ISO
|
|
100
|
+
checks; all handlers return the `{isSuccess, status, error, messages, data}`
|
|
101
|
+
envelope (same contract as `info`/Toga).
|
|
102
|
+
|
|
103
|
+
## What is parked / not built
|
|
104
|
+
|
|
105
|
+
- **Creator screen + `TweaksPanel` — deliberately NOT ported** (parked per plan
|
|
106
|
+
§9.3 / §9.7).
|
|
107
|
+
- **`hubspotProvider` is a throwing stub**, gated on the §9.10 HubSpot content-
|
|
108
|
+
modeling decision. Until HubSpot is seeded, `DEFAULT_BUNDLE` carries the mockup's
|
|
109
|
+
copy verbatim as documented **interim dev content**; it slims to a bare
|
|
110
|
+
engineering fallback once HubSpot is seeded.
|
|
111
|
+
- **`info`'s dead 501 `visit` route was dropped** on purpose (attack surface, no
|
|
112
|
+
caller).
|
|
113
|
+
- **Remaining work is decision-gated, not code:** §9.10 HubSpot content mechanism
|
|
114
|
+
(HubDB likely — gates the provider + campaign seeding, Phase 6); leadership's
|
|
115
|
+
first-campaign copy; Phase 7 pixel/GA4/accessibility polish; Phase 8 Amplify SSR +
|
|
116
|
+
`togatech` integration.
|
|
117
|
+
|
|
118
|
+
## Gotchas
|
|
119
|
+
|
|
120
|
+
- **The mockup's `mockup/styles.css` contains literal `\n` and `\"` escape sequences**
|
|
121
|
+
(JS-string generation artifacts, around lines ~759 / ~840) that break real CSS
|
|
122
|
+
parsers. This was **fixed in the ported `src/app/studio.css`**, but
|
|
123
|
+
`mockup/styles.css` was intentionally left pristine — so **anyone re-porting the
|
|
124
|
+
mockup CSS must re-apply this fix.**
|
|
125
|
+
- **No PII in logs.** All of `info`'s PII-logging was stripped on port (identifier-
|
|
126
|
+
only logs); the Toga error paths **strip query strings** because the contact
|
|
127
|
+
lookup carries the email in the query. Do not reintroduce value logging.
|
|
128
|
+
- **No Tailwind** — see Stack. The mockup CSS is the styling system; adding Tailwind
|
|
129
|
+
risks pixel parity.
|
|
130
|
+
- **`.claude/` is gitignored** in the BDR repo on purpose — do not commit the
|
|
131
|
+
harness; pull it with `npx toga-ai`.
|
|
132
|
+
|
|
133
|
+
## Change history
|
|
134
|
+
- 2026-07-14 — Initial doc: recorded the BUILT state of the BDR web funnel (PLAN
|
|
135
|
+
Phases 0–5 executed + QA'd). Next.js 16.2.3 App Router/TS, deliberate no-Tailwind,
|
|
136
|
+
ESLint no-em-dash rule, `resolveCampaignId`, full mockup component/flow port
|
|
137
|
+
(`useAgentFlow`, Ico registry, motion hardening verbatim), agent identity as
|
|
138
|
+
campaign config, real backend behind LeadSink/CallbackService with PII logging
|
|
139
|
+
stripped and boundary-validated route handlers, 67 green Vitest tests. Recorded the
|
|
140
|
+
parked items (Creator/TweaksPanel, throwing hubspotProvider, dropped `visit` route)
|
|
141
|
+
and the `mockup/styles.css` escape-sequence re-port gotcha. (tcox)
|
|
142
|
+
</content>
|
|
143
|
+
</invoke>
|
|
@@ -6,22 +6,31 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: draft
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/content/schema.ts
|
|
13
13
|
- bdr/src/content/hubspotProvider.ts
|
|
14
14
|
- bdr/src/content/useCampaign.ts
|
|
15
15
|
- bdr/src/content/default.ts
|
|
16
|
+
- bdr/src/lib/resolveCampaignId.ts
|
|
16
17
|
- bdr/src/server/leadSink.ts
|
|
17
18
|
- bdr/src/server/callbackService.ts
|
|
18
19
|
- bdr/src/app/api/call-now/route.ts
|
|
19
20
|
- bdr/src/app/api/call-later/route.ts
|
|
21
|
+
- bdr/src/app/api/contact/route.ts
|
|
20
22
|
related:
|
|
21
23
|
- ../architecture.md
|
|
22
24
|
- call-orchestration.md
|
|
25
|
+
- web-funnel-app.md
|
|
23
26
|
---
|
|
24
27
|
|
|
28
|
+
> **Status note:** the content **contract** below (schema, provider seam, adapters)
|
|
29
|
+
> is now BUILT and QA'd (PLAN Phases 0–5). The built codebase — structure, stack,
|
|
30
|
+
> how to run — is documented in `web-funnel-app.md`. The one part still **not** built
|
|
31
|
+
> is the HubSpot content *sourcing* (`hubspotProvider` is a throwing stub gated on the
|
|
32
|
+
> §9.10 mechanism decision), which is why this doc stays `draft`.
|
|
33
|
+
|
|
25
34
|
## What this is
|
|
26
35
|
|
|
27
36
|
The **public-facing web funnel** is the new UI / lead-capture front door of the **same
|
|
@@ -100,6 +109,24 @@ optional `build` (AutoBuild beats), `analytics`, and `features[]` capability fla
|
|
|
100
109
|
`RichLine` supports the accent-inked span the mockup uses (e.g. `"<agent> is ready to
|
|
101
110
|
work."`). The mapping layer is the seam between HubSpot storage and this contract.
|
|
102
111
|
|
|
112
|
+
**Schema calibrated by building (the §6 plan sketch was a sketch; `src/content/schema.ts`
|
|
113
|
+
is now THE contract).** Additions made while building the real screens: `chrome`
|
|
114
|
+
(wordmark / rail labels); capabilities `eyebrows` + qbank strings + `howLink`;
|
|
115
|
+
`connectNote` split to `{label, value}`; `earlyAccess` / `phone` / `recap` groups;
|
|
116
|
+
`build` made **required**. Bundle template tokens `<agent>` / `<date>` / `<tz>` /
|
|
117
|
+
`<time>` / `<detail>` are substituted **at render** by an `agentize()` helper (they are
|
|
118
|
+
plain strings in the bundle, so HubSpot only ever stores strings).
|
|
119
|
+
|
|
120
|
+
**`resolveCampaign` never throws** (built): an unknown slug or a provider failure
|
|
121
|
+
returns the `DEFAULT` bundle, so the funnel always renders. Entry resolution lives in
|
|
122
|
+
`src/lib/resolveCampaignId.ts` (`?campaign=<slug>` → mapped `?hsCampaignId=` →
|
|
123
|
+
`DEFAULT`; slug regex `^[a-z0-9][a-z0-9-]{0,63}$`).
|
|
124
|
+
|
|
125
|
+
**`hubspotProvider` is a throwing stub** explicitly gated on the §9.10 HubSpot-modeling
|
|
126
|
+
decision — it is not wired yet. Until HubSpot is seeded, **`DEFAULT_BUNDLE` carries the
|
|
127
|
+
mockup's copy verbatim as documented interim dev content**; once HubSpot is seeded it
|
|
128
|
+
slims to a bare engineering safety-net fallback (not authored marketing copy).
|
|
129
|
+
|
|
103
130
|
## Backend reuse (from the `info` repo; existing AI-BDR calls)
|
|
104
131
|
|
|
105
132
|
The funnel's Call Now / Schedule screens submit to a real backend **ported from the
|
|
@@ -118,6 +145,16 @@ so only the impl changes later:
|
|
|
118
145
|
- `CallbackService.requestCall(ref, whenOrNow, phone)` → Toga `requestCall`, immediate vs.
|
|
119
146
|
scheduled mapped to `CONTACT_CALL_TYPE_UUID`.
|
|
120
147
|
|
|
148
|
+
**Built (PLAN Phase 5):** `info`'s `toga.ts` / `hubspot.ts` are ported into `src/server/`
|
|
149
|
+
behind those adapters with **all of `info`'s PII logging stripped** (identifier-only
|
|
150
|
+
logs; the Toga error paths **strip query strings** because the contact lookup carries
|
|
151
|
+
the email in the query). Route handlers `/api/call-now`, `/api/call-later`, `/api/contact`
|
|
152
|
+
**validate at the boundary** (16 KB body cap; phone / uuid / ISO checks) and always
|
|
153
|
+
return the `{isSuccess, status, error, messages, data}` envelope. The page primes the
|
|
154
|
+
lead server-side from `?hsContactId` and never fails rendering. `info`'s dead 501
|
|
155
|
+
`visit` route was deliberately dropped (attack surface, no caller). Verified by 67 green
|
|
156
|
+
Vitest tests. See `web-funnel-app.md` for the full built structure.
|
|
157
|
+
|
|
121
158
|
Note on "campaign": `info` uses HubSpot **only to fetch the contact**; the URL's
|
|
122
159
|
`hsCampaignId` is passed to Toga as the attribution campaign UUID
|
|
123
160
|
(`addContactToCampaign`) — attribution-only in `info`. **Fetching marketing content by
|
|
@@ -158,6 +195,16 @@ right campaign for logging.
|
|
|
158
195
|
`var(--accent)` (+ derived `--accent-fill`); never hardcode an accent value.
|
|
159
196
|
|
|
160
197
|
## Change history
|
|
198
|
+
- 2026-07-14 — Content contract BUILT + QA'd (PLAN Phases 0–5): schema calibrated by
|
|
199
|
+
building the real screens (`src/content/schema.ts` is now THE contract — chrome,
|
|
200
|
+
capabilities eyebrows/qbank/howLink, `connectNote` {label,value}, earlyAccess/phone/
|
|
201
|
+
recap groups, `build` required, `agentize()` token substitution); `resolveCampaign`
|
|
202
|
+
never throws (unknown slug/provider failure → DEFAULT); `hubspotProvider` is a throwing
|
|
203
|
+
stub gated on §9.10; `DEFAULT_BUNDLE` carries the mockup copy as interim dev content.
|
|
204
|
+
Backend ported from `info` behind LeadSink/CallbackService with PII logging stripped,
|
|
205
|
+
boundary-validated route handlers, dead `visit` route dropped; 67 green Vitest tests.
|
|
206
|
+
Built structure split out to `web-funnel-app.md`. Kept `draft` because HubSpot content
|
|
207
|
+
sourcing (§9.10) is still open. (tcox)
|
|
161
208
|
- 2026-07-13 — Draft 2 resync to the current plan: framework locked to Next.js App Router
|
|
162
209
|
(earlier React/Vite SPA idea dropped); recorded the **HubSpot-over-Contentful** content-store
|
|
163
210
|
decision + rationale (marketing already in HubSpot, campaign model already exists, trivial
|
package/package.json
CHANGED