toga-ai 1.0.652 → 1.0.654
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/2.0/apps/_underscore/INDEX.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/calculated-sql-fields.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/netsuite-salesorder-address-phone-sync.md +14 -13
- package/knowledge/2.0/apps/_underscore/features/sales-order-po-number-sourcing.md +71 -8
- package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md +72 -7
- package/knowledge/2.0/apps/ai-bdr/INDEX.md +2 -1
- package/knowledge/2.0/apps/ai-bdr/features/landing-call-flow.md +226 -0
- package/knowledge/2.0/apps/ai-bdr/features/landing-chat-drawer.md +63 -13
- package/knowledge/2.0/apps/ai-bdr/features/live-call-status.md +9 -1
- package/knowledge/2.0/apps/ai-bdr/features/security-landing-page.md +101 -40
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-app.md +17 -1
- package/knowledge/2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md +3 -3
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/features/po-number-upstream-direction.md +37 -10
- package/package.json +1 -1
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
38
38
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
39
39
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
40
|
-
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
41
|
-
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php |
|
|
40
|
+
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
41
|
+
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php |
|
|
42
42
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
|
|
43
43
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
44
44
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Client/Language.php, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, toga25-supply/src/App.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/contexts/AuthContext.tsx, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Core/2026-08-21a - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Compass/2026-08-04a - ApprovalDetailsAssignedManagerPreferredStage.sql, _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
@@ -23,7 +23,7 @@ related:
|
|
|
23
23
|
- ../../api2/features/v2-api-error-codes.md
|
|
24
24
|
- ./model-magic-field-access.md
|
|
25
25
|
- ./model-save-parent-cascade-stored-field-deadlock.md
|
|
26
|
-
- ./
|
|
26
|
+
- ./sales-order-purchase-order-bridge-direction.md
|
|
27
27
|
---
|
|
28
28
|
|
|
29
29
|
## Summary
|
|
@@ -186,7 +186,7 @@ generous end; `Developer` and `Public` get no ACL row for any field.
|
|
|
186
186
|
`Client_Imhouston` (1 of 35 prod) and 7 of 36 client-sandbox schemas, driving the re-runnable
|
|
187
187
|
`Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql`. Recorded the `Base` + `API`
|
|
188
188
|
by-name ACL convention for these rows. Cross-linked
|
|
189
|
-
[PO-to-SO bridge tables](./
|
|
189
|
+
[PO-to-SO bridge tables](./sales-order-purchase-order-bridge-direction.md) — the metadata row makes
|
|
190
190
|
`_purchaseOrders` *requestable*, but the field's SQL was reading the wrong bridge table, so a
|
|
191
191
|
successful rollout still returned blank for most clients. (bala)
|
|
192
192
|
- 2026-08-18 - Documented **how a calculated field becomes requestable**: a client-scoped
|
|
@@ -16,7 +16,7 @@ related:
|
|
|
16
16
|
- acl-permission-chain.md
|
|
17
17
|
- address-uniqueness-normalization.md
|
|
18
18
|
- carrier-shipping-labels.md
|
|
19
|
-
-
|
|
19
|
+
- sales-order-purchase-order-bridge-direction.md
|
|
20
20
|
- ../../toga2-supply/features/fulfill-and-ship.md
|
|
21
21
|
- ../../api2/features/record-scripts.md
|
|
22
22
|
---
|
|
@@ -41,15 +41,16 @@ changes behaviour for all 22 clients at once**.
|
|
|
41
41
|
(and was NULL on the order tested), while the **bridge covers 12,643 of 12,658** orders. Resolve the
|
|
42
42
|
PO through the bridge, not the column.
|
|
43
43
|
|
|
44
|
-
**⚠ …and it is the
|
|
45
|
-
read.** This trait writes the PO link via the api2 child key **`purchaseOrderSalesOrders`**,
|
|
46
|
-
|
|
47
|
-
`_Model_Client_SalesOrder::_purchaseOrders()` queried **`SalesOrders_PurchaseOrders
|
|
48
|
-
table populated by a *different*
|
|
49
|
-
|
|
50
|
-
72,134 prod orders
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
**⚠ …and it is the UPSTREAM bridge, which the shared `SalesOrders._purchaseOrders` field did not
|
|
45
|
+
read.** This trait writes the PO link via the api2 child key **`purchaseOrderSalesOrders`**, landing
|
|
46
|
+
in **`PurchaseOrders_SalesOrders`** (upstream — the *customer's* PO). The base calculated field
|
|
47
|
+
`_Model_Client_SalesOrder::_purchaseOrders()` queried **`SalesOrders_PurchaseOrders`** (downstream),
|
|
48
|
+
a *different* table populated by a *different* pipeline — the 1.0
|
|
49
|
+
`App_Api_Toga2::syncPurchaseOrderFromNetsuite` cron — holding Agilant's **vendor** POs. So every
|
|
50
|
+
client on this trait stored a correct PO the field could not see: **41,381 of 72,134** prod orders
|
|
51
|
+
rendered blank until a downstream-then-upstream `COALESCE` was added. Read
|
|
52
|
+
[SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md) **before** touching either
|
|
53
|
+
side of this link — the child key you choose decides which table you write, silently.
|
|
53
54
|
|
|
54
55
|
## ⚠ Four traps when writing a field on this shared trait
|
|
55
56
|
|
|
@@ -102,11 +103,11 @@ intended, not a fault.
|
|
|
102
103
|
## Change history
|
|
103
104
|
|
|
104
105
|
- 2026-08-26 — Corrected the PO half of the "where the values come from" table: the
|
|
105
|
-
`purchaseOrderSalesOrders` child key this trait posts writes the **
|
|
106
|
+
`purchaseOrderSalesOrders` child key this trait posts writes the **upstream** bridge
|
|
106
107
|
(`PurchaseOrders_SalesOrders`, customer PO), while the shared `SalesOrders._purchaseOrders`
|
|
107
|
-
calculated field read the **
|
|
108
|
+
calculated field read the **downstream** bridge (`SalesOrders_PurchaseOrders`, Agilant vendor PO) — so
|
|
108
109
|
all 22 trait clients stored a PO the UI could not display (41,381 of 72,134 prod orders blank).
|
|
109
|
-
Cross-linked
|
|
110
|
+
Cross-linked [SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md). (bala)
|
|
110
111
|
- 2026-08-11 — TRUE-80824: created. Prefilled TOGa Supply's Reference 1 / Reference 2 / Phone # from
|
|
111
112
|
NetSuite by sourcing `SalesOrders.number`, the bridge-linked purchase order (`otherRefNum` — **not**
|
|
112
113
|
`customerPurchaseOrder`, ~59% populated vs the bridge's 12,643/12,658), and the shipping address's
|
|
@@ -6,11 +6,13 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
13
13
|
- _underscore/Model/Compass/SalesOrder.php
|
|
14
|
+
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
15
|
+
- dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql
|
|
14
16
|
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
15
17
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
|
|
16
18
|
- dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql
|
|
@@ -50,12 +52,53 @@ deliberately **not** set: the value is a scalar comma-joined string, not a perso
|
|
|
50
52
|
via `resolvePathInsensitive(valueKey)`, normalising an empty string to an em-dash.
|
|
51
53
|
|
|
52
54
|
### The tenant layer (`_purchaseOrders`)
|
|
55
|
+
|
|
56
|
+
**Only TWO classes in the whole tree define this method** — verified with
|
|
57
|
+
`grep -rn "function _purchaseOrders" _underscore/`:
|
|
58
|
+
|
|
53
59
|
| Tenant | Implementation | Direction read |
|
|
54
60
|
|---|---|---|
|
|
55
|
-
| **Compass USA** |
|
|
56
|
-
| **Quad Graphics** | no override → base implementation | downstream
|
|
57
|
-
|
|
|
58
|
-
|
|
61
|
+
| **Compass USA and Compass Canada** | both inherit `_Model_Compass_SalesOrder::_purchaseOrders()` (`_underscore/Model/Compass/SalesOrder.php:238`) — a `UNION ALL` of its own vendor POs plus Office Depot's POs traversed SO→PO→SO→PO across the multi-tier chain | downstream **+ upstream** |
|
|
62
|
+
| **Quad Graphics** | no override → base implementation | downstream, then upstream fallback |
|
|
63
|
+
| base (`_Model_Client_SalesOrder:349`) | the shared calculated field | **downstream, falling back to upstream** (see below) |
|
|
64
|
+
|
|
65
|
+
**⚠ Correction (2026-08-26): Compass Canada is not on the base implementation.** The earlier "no
|
|
66
|
+
`Model/Compasscanada/` directory" reading looked in the wrong place — Compass's tenants nest one
|
|
67
|
+
level deeper, at `_underscore/Model/Compass/Canada/SalesOrder.php`, where
|
|
68
|
+
`_Model_Compass_Canada_SalesOrder extends _Model_Compass_SalesOrder`. Compass USA is
|
|
69
|
+
`Model/Compass/Usa/SalesOrder.php` and does the same. So **any change to the Compass override moves
|
|
70
|
+
both Compass tenants**, and neither is affected by a change to the base. Compass Canada's upstream
|
|
71
|
+
bridge is in fact **empty** (0 rows; 735 of 788 orders resolve downstream) — it is downstream-only,
|
|
72
|
+
not upstream-only.
|
|
73
|
+
|
|
74
|
+
### The base field now falls back to upstream (`COALESCE`)
|
|
75
|
+
|
|
76
|
+
Because the trait-based NetSuite sync writes **upstream** while the base field read **downstream**,
|
|
77
|
+
41,381 of 72,134 prod orders across the 19 base-model tenants resolved no PO at all. The base
|
|
78
|
+
`_purchaseOrders()` is now a **`COALESCE` of two scalar subqueries** — downstream first, upstream as
|
|
79
|
+
the fallback — which fixes every upstream tenant at once and needed **no per-tenant override**.
|
|
80
|
+
|
|
81
|
+
Branch state as verified **2026-08-26** (do not assume this is live):
|
|
82
|
+
|
|
83
|
+
| Repo | Change | Where it lives |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `_underscore` | the `COALESCE` fallback | commit `4579e0e7` on `TRUE-81284`, merged into **`_beta`**, `_sandbox-client`, `_sandbox-dev`. **Not on `_production`.** |
|
|
86
|
+
| `dbchanges2` | `Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql` (the `CustomRecordFields` row for the schemas the 2026-08-11b rollout missed) | commit `0ca4012` on `TRUE-81284`, pushed. **Not on `_main` or `_beta`.** |
|
|
87
|
+
|
|
88
|
+
Verified while applied: **zero existing values changed for any tenant** — the fallback only fills
|
|
89
|
+
nulls — while coverage went NYCHH 64 → 819 of 819, Elite 57 → 288 of 288, Quad 475 → 1,456 of 1,463.
|
|
90
|
+
The MySQL 8 plan stayed indexed (both subqueries `ref` on `idx_salesOrderId`, then `eq_ref` on the
|
|
91
|
+
`PurchaseOrders` primary key) and `COALESCE` short-circuits, so the upstream subquery only runs when
|
|
92
|
+
the downstream one is null.
|
|
93
|
+
|
|
94
|
+
#### ⚠ Still open — which direction wins when an order has both
|
|
95
|
+
|
|
96
|
+
Downstream-first shows the **vendor** PO, while the sales-orders **list** (whose table-view join
|
|
97
|
+
reads upstream) shows the **customer** PO, so one order can display two different PO numbers in the
|
|
98
|
+
list and in the modal. Orders carrying both are real and not rare: NYCHH 64, Elite 55, Quad 469.
|
|
99
|
+
Downstream-first was chosen, then questioned in favour of treating the list as the source of truth;
|
|
100
|
+
a per-tenant override on `_Model_Nychh_SalesOrder` preferring upstream was offered and **not built**.
|
|
101
|
+
**Settle this before quoting either value as correct.**
|
|
59
102
|
|
|
60
103
|
### `poSelection` is a different thing
|
|
61
104
|
The `poSelection` config on the `sales-order-record-actions` surface picks **which** linked PO the
|
|
@@ -74,14 +117,34 @@ the two — changing one does not move the other.
|
|
|
74
117
|
- **Surface seed `id`s are deterministic; surface `uuid`s are not.** Seeds insert with `UUID()`, so
|
|
75
118
|
the element uuid differs per environment. A uuid quoted in an older migration will **not** match
|
|
76
119
|
the uuid in a live payload. Key surface updates on the literal `id` (here, `id = 7`).
|
|
77
|
-
- **Downstream-only base implementation
|
|
78
|
-
tenant `_purchaseOrders`
|
|
120
|
+
- **Downstream-only base implementation was a trap for upstream tenants.** On an upstream-only
|
|
121
|
+
tenant `_purchaseOrders` resolved cleanly (no `EV-8`) and returned `null` forever. Closed on
|
|
122
|
+
`_beta` by the `COALESCE` fallback above. See
|
|
79
123
|
[SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md) and
|
|
80
124
|
[NYCHH PO Number](../../../clients/nychh/features/po-number-upstream-direction.md).
|
|
125
|
+
- **A correct value here still may not reach the older TOGa Supply order-details modal.** That UI is
|
|
126
|
+
config-driven and declares the display row and the API field request **separately**: the
|
|
127
|
+
`DEFAULTFIELDS` and `QUAD` field sets ask for a `_purchaseOrders` row without listing
|
|
128
|
+
`_purchaseOrders` in `apiFields.fetchOrderDetails`, so it renders blank regardless of the SQL. See
|
|
129
|
+
[order-detail field config](../../toga2-supply/features/order-detail-field-config-and-customer-name.md).
|
|
130
|
+
- **Fixing the SQL is not enough on a tenant whose schema lacks the metadata row.** No
|
|
131
|
+
`CustomRecordFields` row (`recordId` 14, field `_purchaseOrders`) means `EV-8` no matter what the
|
|
132
|
+
model returns, and the original per-client rollout **missed schemas** — prod 1 of 35, sandbox 7 of
|
|
133
|
+
36. See [calculated SQL fields](./calculated-sql-fields.md).
|
|
81
134
|
- Dropping `isPersonaValue` is intentional; re-adding it will make the FE try to treat the string
|
|
82
135
|
as a persona array.
|
|
83
136
|
|
|
84
137
|
## Change history
|
|
138
|
+
- 2026-08-26 — Base `_purchaseOrders()` became a **`COALESCE`** of downstream-then-upstream, closing
|
|
139
|
+
the upstream-tenant gap for all base-model tenants with no per-tenant override (verified: no
|
|
140
|
+
existing value changed; NYCHH 64 → 819/819, Elite 57 → 288/288, Quad 475 → 1,456/1,463; plan stays
|
|
141
|
+
indexed). Recorded its branch state (`_beta`, **not** `_production`) and left the
|
|
142
|
+
**downstream-vs-upstream tie-break open** for orders carrying both (NYCHH 64, Elite 55, Quad 469),
|
|
143
|
+
where the modal and the list can disagree. **Corrected the tenant table**: Compass Canada is not on
|
|
144
|
+
the base implementation — it lives at `Model/Compass/Canada/` and inherits the Compass override, so
|
|
145
|
+
both Compass tenants move together and Compass Canada's upstream bridge is empty. Added the two
|
|
146
|
+
other independent causes of a blank PO (missing `CustomRecordFields` row → `EV-8`; the older supply
|
|
147
|
+
order-details field set never requesting the field). (bala)
|
|
85
148
|
- 2026-08-25 — Repointed `SurfaceElements` id 7 to `_purchaseOrders` for all clients
|
|
86
149
|
(`Core/2026-08-25a`, reverting `Core/2026-07-23a`) and made the FE adapter honour the surface
|
|
87
150
|
`valueKey` instead of a hardcoded path; recorded the corrected per-tenant map, which supersedes
|
package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
CHANGED
|
@@ -6,17 +6,20 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/PurchaseOrders/SalesOrder.php
|
|
13
13
|
- _underscore/Model/Client/SalesOrders/PurchaseOrder.php
|
|
14
14
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
15
15
|
- _underscore/Model/Client/SalesOrder.php
|
|
16
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
17
|
+
- library/app/api/toga2.php
|
|
16
18
|
related:
|
|
17
19
|
- ./sales-order-po-number-sourcing.md
|
|
18
20
|
- ./recursive-item-fulfillments.md
|
|
19
21
|
- ./fulfillable-item-propagation.md
|
|
22
|
+
- ./netsuite-salesorder-address-phone-sync.md
|
|
20
23
|
- ../../api2/features/v2-rest-query-contract.md
|
|
21
24
|
- ../../../clients/nychh/features/po-number-upstream-direction.md
|
|
22
25
|
---
|
|
@@ -47,10 +50,45 @@ origin; the second is what was generated from it.
|
|
|
47
50
|
- A multi-tier tenant (Compass USA) has **both**, chained SO→PO→SO→PO.
|
|
48
51
|
- The only in-code statement of the distinction today is a docblock at
|
|
49
52
|
`_underscore/Model/Client/ItemFulfillment.php:136-140`.
|
|
50
|
-
- The base `_purchaseOrders` calculated field on `_Model_Client_SalesOrder` (line ~348)
|
|
51
|
-
**downstream** table only. See
|
|
53
|
+
- The base `_purchaseOrders` calculated field on `_Model_Client_SalesOrder` (line ~348) read the
|
|
54
|
+
**downstream** table only until 2026-08-25, when a **fallback to upstream** was added. See
|
|
52
55
|
[sales-order PO Number sourcing](./sales-order-po-number-sourcing.md).
|
|
53
56
|
|
|
57
|
+
### Two writers, one per direction — and the child key is what picks the table
|
|
58
|
+
|
|
59
|
+
The two directions are not two views of one write; they are populated by **two different sync
|
|
60
|
+
pipelines**, in **different frameworks**:
|
|
61
|
+
|
|
62
|
+
| | UPSTREAM (`PurchaseOrders_SalesOrders`) | DOWNSTREAM (`SalesOrders_PurchaseOrders`) |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Written by | **2.0** `_Trait_Netsuite_SalesOrder` (`_underscore/Trait/Netsuite/SalesOrder.php` ~:664-680 and ~:720) | **1.0** cron `App_Api_Toga2::syncPurchaseOrderFromNetsuite` (`library/app/api/toga2.php:2028`, posts the child key at :2504) |
|
|
65
|
+
| api2 child key | `purchaseOrderSalesOrders` | `salesOrderPurchaseOrders` |
|
|
66
|
+
| PO number stored | NetSuite `$nsOrder->otherRefNum` — the **customer's** PO (`EIT0011108`, `LOI-Woodhull`, `GAVELL/08252026`, Quad `4992713`) | the **vendor** PO — numeric NetSuite PO numbers (`168984`), Elite's `P100019` |
|
|
67
|
+
|
|
68
|
+
**⚠ The child key is the only thing that decides which table you write, and getting it wrong is
|
|
69
|
+
silent.** api2 treats `salesOrderPurchaseOrders` and `purchaseOrderSalesOrders` as two unrelated
|
|
70
|
+
child collections, so a transposed key writes a perfectly valid row into the *other* direction and
|
|
71
|
+
the reading side simply never sees it. No error, no warning. Read the key aloud, like the routes.
|
|
72
|
+
|
|
73
|
+
Because **22 client `SalesOrder` models compose `_Trait_Netsuite_SalesOrder`**, every NetSuite-synced
|
|
74
|
+
tenant writes **upstream** rows — which is why upstream-only tenants are the norm on that trait
|
|
75
|
+
rather than the exception. (Grep the composition list before assuming a trait change is
|
|
76
|
+
client-scoped; note the same grep also matches the `SalesOrderItem` / `SalesOrderStage` models, so
|
|
77
|
+
count `SalesOrder.php` files only or you will report 66.)
|
|
78
|
+
|
|
79
|
+
### How much downstream-only cost, measured on prod
|
|
80
|
+
|
|
81
|
+
Because the base field read downstream only while the trait wrote upstream, **41,381 of 72,134**
|
|
82
|
+
sales orders across the 19 base-model tenants returned no PO at all — only 30,753 resolved. Worst
|
|
83
|
+
affected: NYCHH 64/822, Canon 594/6,290, GroWrk 1,226/12,793, S&P Global 971/9,468, Endeavor Health
|
|
84
|
+
1,919/11,836. **An order can also carry both directions at once** (NYCHH 64, Elite 55, Quad 469),
|
|
85
|
+
which is what makes the tie-break in
|
|
86
|
+
[PO Number sourcing](./sales-order-po-number-sourcing.md) a real decision rather than a formality.
|
|
87
|
+
|
|
88
|
+
All **35** prod client schemas contain all four tables (`SalesOrders`, `PurchaseOrders`, both
|
|
89
|
+
bridges), verified via `information_schema` — so reading either direction is structurally safe
|
|
90
|
+
everywhere. Present is not populated.
|
|
91
|
+
|
|
54
92
|
## Gotchas
|
|
55
93
|
|
|
56
94
|
- **An empty array is the symptom.** `200` / `totalRecordCount 0` with **no** `EZ-*` or `EV-*`
|
|
@@ -62,11 +100,38 @@ origin; the second is what was generated from it.
|
|
|
62
100
|
- **The routes are near-homographs.** `sales-order-purchase-orders` vs
|
|
63
101
|
`purchase-order-sales-orders`. Read them aloud before wiring a front-end call.
|
|
64
102
|
- **Downstream-only assumptions are baked into shared code.** Any tenant whose POs are purely
|
|
65
|
-
upstream (verified: NYCHH
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
upstream (verified: NYCHH) silently gets `null`/empty from the shared downstream-reading helpers
|
|
104
|
+
rather than an error.
|
|
105
|
+
- **⚠ Compass Canada is NOT upstream-only, and it DOES have model overrides** — corrected
|
|
106
|
+
2026-08-26. The earlier "no `Model/Compasscanada/` directory" reading looked in the wrong place:
|
|
107
|
+
the folder is **`_underscore/Model/Compass/Canada/`**, and
|
|
108
|
+
`_Model_Compass_Canada_SalesOrder extends _Model_Compass_SalesOrder`, so Compass Canada inherits
|
|
109
|
+
**Compass's** `_purchaseOrders` override, not the base one. Its **upstream bridge is completely
|
|
110
|
+
empty (0 rows)** while 735 of 788 orders resolve a PO downstream. When checking whether a tenant
|
|
111
|
+
has overrides, search for the class name (`grep -rn "class _Model_.*_SalesOrder extends"`) rather
|
|
112
|
+
than guessing a directory — Compass's tenants nest one level deeper than everyone else's.
|
|
113
|
+
- **⚠ Do NOT paper over the direction with an `ojoin` on a shared single-record fetch.** It is
|
|
114
|
+
tempting because it works in one place: the TOGa Supply NYCHH orders **list** joins the upstream
|
|
115
|
+
bridge and is correct — but only because NYCHH's upstream bridge happens to be **1:1** (823 rows /
|
|
116
|
+
823 distinct sales orders, max 1 PO per SO). The same join in the shared
|
|
117
|
+
`fetchOrdersDetails` (`toga2-supply/src/pages/Orders/api/OrdersApi.ts:397`) breaks three tenants:
|
|
118
|
+
**Compass** has up to **6,440** downstream POs on one sales order (row multiplication on a
|
|
119
|
+
single-record fetch), **Compass Canada's upstream bridge is empty** (735 of 788 would drop to
|
|
120
|
+
**zero**), and **Prudential** has only **1,868 of 26,843** orders upstream (92% populated would
|
|
121
|
+
become 7%). A scalar `GROUP_CONCAT` calculated field is the right mechanism precisely because it
|
|
122
|
+
collapses many POs without multiplying rows.
|
|
68
123
|
|
|
69
124
|
## Change history
|
|
125
|
+
- 2026-08-26 — Added the **write** side: the two directions are populated by two different
|
|
126
|
+
pipelines (2.0 `_Trait_Netsuite_SalesOrder` writes **upstream** customer POs from `otherRefNum`;
|
|
127
|
+
the 1.0 `syncPurchaseOrderFromNetsuite` cron writes **downstream** vendor POs), and the api2 child
|
|
128
|
+
key (`purchaseOrderSalesOrders` vs `salesOrderPurchaseOrders`) is the only thing selecting the
|
|
129
|
+
table — a transposed key writes the wrong direction silently. Quantified the downstream-only cost
|
|
130
|
+
on prod (41,381 of 72,134 orders returned no PO across the 19 base-model tenants) and noted
|
|
131
|
+
tenants carrying both directions. **Corrected the Compass Canada claim**: it has overrides at
|
|
132
|
+
`Model/Compass/Canada/` and inherits the Compass `_purchaseOrders` override, and its upstream
|
|
133
|
+
bridge is empty — it is downstream-only, not upstream-only. Added the "do not fix this with an
|
|
134
|
+
`ojoin`" gotcha with the Compass / Compass Canada / Prudential regressions it would cause. (bala)
|
|
70
135
|
- 2026-08-25 — Documented the two-direction bridge after an NYCHH "empty array" debug that was
|
|
71
136
|
purely a wrong-direction query; recorded route/model/record-id mapping and the empty-vs-EZ
|
|
72
137
|
diagnostic signature (apeterson)
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
| [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Execution status (2026-07-16):** PLAN **Phases 0–5 are BUILT and QA'd**, Phase 6 > was **mostly already built** (the `info` contract was implemented at comm | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
|
|
7
7
|
| [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
|
|
8
8
|
| [AI-BDR Campaign Chat — Backend Contract + Frontend Widget Handoff](features/campaign-chat-handoff.md) | The consolidated backend + frontend handoff for the **AI-BDR campaign chat** — a grounded AI-BDR chat widget for landing pages, powered by the Talos backend at | ai-bdr-campaign-handoff.md, bdr/src/lib/talosChat.ts |
|
|
9
|
+
| [Landing "Call me now" Flow (/landing) — screens + the dormant real callback route](features/landing-call-flow.md) | The **"Call me now" path on `/landing`**: a two-screen flow (call form → calling/recap) plus a **real, wired, but deliberately dormant** backend route (`POST /a | bdr/src/flow/screens/SecurityCallNow.tsx, bdr/src/flow/screens/SecurityCalling.tsx, bdr/src/lib/securityCallFlow.ts, bdr/src/lib/leadName.ts, bdr/src/app/api/landing-call/route.ts, bdr/src/flow/landingCallApi.ts, bdr/src/flow/screens/SecurityLanding.tsx, bdr/src/components/ChatPanel.tsx, bdr/src/content/security.ts, bdr/src/app/security.css, bdr/src/proxy.ts, bdr/.env.example, bdr/test/securityCallFlow.test.ts, bdr/test/landingCallRoute.test.ts |
|
|
9
10
|
| [Talos Chat Drawer on /landing (first campaign-chat consumer)](features/landing-chat-drawer.md) | The **right-anchored chat drawer on `/landing`**, wired to the **real** Talos campaign-chat backend at `https://api.togaiq.com`. | bdr/src/components/ChatPanel.tsx, bdr/src/components/ChatMarkdown.tsx, bdr/src/lib/talosChat.ts, bdr/src/content/security.ts, bdr/src/flow/screens/SecurityLanding.tsx, bdr/src/app/security.css, bdr/test/talosChat.test.ts, bdr/test/chatMarkdown.test.ts |
|
|
10
11
|
| [Live Call Status — Vapi call API → funnel Success screen lifecycle](features/live-call-status.md) | The BDR funnel's Success screen shows the **real** state of the call it just requested — dialing / ringing / answered-and-talking / ended / not-answered — inste | bdr/src/server/vapiCall.ts, bdr/src/server/callStatus.ts, bdr/src/server/statusToken.ts, bdr/src/server/callbackService.ts, bdr/src/server/workerDialer.ts, bdr/src/lib/callStatus.ts, bdr/src/app/api/call-status/route.ts, bdr/src/app/api/call-now/route.ts, bdr/src/flow/useCallStatusViewModel.ts, bdr/src/flow/callStatusApi.ts, bdr/src/flow/screens/Success.tsx, bdr/src/content/schema.ts, bdr/src/content/default.ts, bdr/src/proxy.ts |
|
|
11
|
-
| [Cybersecurity Landing Page (/landing — the site's current main page)](features/security-landing-page.md) | A single-page **cybersecurity services marketing landing page** at **`/landing`** in the `bdr` repo — and, as of 2026-08-24, **the site's main page**: `/` **307 | bdr/src/app/landing/page.tsx, bdr/src/app/security.css, bdr/src/flow/screens/SecurityLanding.tsx, bdr/src/content/security.ts, bdr/src/components/LogoSymbol.tsx, bdr/src/components/AssistantMark.tsx, bdr/src/components/Ico.tsx, bdr/src/components/Particles.tsx, bdr/src/components/ChatPanel.tsx, bdr/src/components/ChatMarkdown.tsx, bdr/src/lib/talosChat.ts, bdr/next.config.ts, bdr/eslint.config.mjs |
|
|
12
|
+
| [Cybersecurity Landing Page (/landing — the site's current main page)](features/security-landing-page.md) | A single-page **cybersecurity services marketing landing page** at **`/landing`** in the `bdr` repo — and, as of 2026-08-24, **the site's main page**: `/` **307 | bdr/src/app/landing/page.tsx, bdr/src/app/security.css, bdr/src/flow/screens/SecurityLanding.tsx, bdr/src/flow/screens/SecurityCallNow.tsx, bdr/src/flow/screens/SecurityCalling.tsx, bdr/src/lib/securityCallFlow.ts, bdr/src/content/security.ts, bdr/src/components/LogoSymbol.tsx, bdr/src/components/AssistantMark.tsx, bdr/src/components/Ico.tsx, bdr/src/components/Particles.tsx, bdr/src/components/ChatPanel.tsx, bdr/src/components/ChatMarkdown.tsx, bdr/src/lib/talosChat.ts, bdr/next.config.ts, bdr/eslint.config.mjs |
|
|
12
13
|
| [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
|
|
13
14
|
| [Web Funnel — Built Next.js App (structure, stack, how to run)](features/web-funnel-app.md) | > **READ FIRST (2026-08-24) — the funnel is currently UNREACHABLE.** `/` now > **307-redirects to `/landing`**, the new cybersecurity landing page > ([security- | bdr/PLAN.md, bdr/next.config.ts, bdr/.gitignore, bdr/src/server/hubspot.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/server/workerDialer.ts, bdr/src/server/mailer.ts, bdr/src/server/statusToken.ts, bdr/src/server/vapiCall.ts, bdr/src/server/callStatus.ts, bdr/src/lib/summaryText.ts, bdr/src/lib/emailFormat.ts, bdr/src/lib/callStatus.ts, bdr/src/app/api/share-summary/route.ts, bdr/src/app/api/call-status/route.ts, bdr/src/flow/shareSummaryApi.ts, bdr/src/flow/callStatusApi.ts, bdr/src/flow/useCallStatusViewModel.ts, bdr/src/flow/screens/Success.tsx, bdr/src/flow/screens/CallNow.tsx, bdr/src/flow/screens/Schedule.tsx, bdr/src/flow/screens/CallSummary.tsx, bdr/src/content/schema.ts, bdr/src/content/default.ts, bdr/test/togaEnrichment.test.ts, bdr/test/fastDial.test.ts, bdr/eslint.config.mjs, bdr/amplify.yml, bdr/src/app, bdr/src/app/layout.tsx, bdr/src/app/page.tsx, bdr/src/app/studio.css, bdr/src/lib/resolveCampaignId.ts, bdr/src/lib/scheduleSlots.ts, bdr/src/lib/formatPhone.ts, bdr/src/lib/analytics.ts, bdr/src/lib/rateLimit.ts, bdr/src/lib/clientIp.ts, bdr/src/proxy.ts, bdr/.env.example, bdr/src/components/GoogleAnalytics.tsx, bdr/src/components/RichText.tsx, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/server/toga.ts, bdr/src/app/api, bdr/test, bdr/test/togaPrimaryPhone.test.ts, bdr/public, bdr/mockup/styles.css |
|
|
14
15
|
| [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | > **Status note (RESOLVED 2026-07-16):** the content **contract** below (schema, > provider seam, adapters) is BUILT and QA'd (PLAN Phases 0–5). | bdr/src/content/schema.ts, bdr/src/content/provider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/lib/resolveCampaignId.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts, bdr/src/app/api/contact/route.ts |
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Landing "Call me now" Flow (/landing) — screens + the dormant real callback route
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [tcox]
|
|
11
|
+
files:
|
|
12
|
+
- bdr/src/flow/screens/SecurityCallNow.tsx
|
|
13
|
+
- bdr/src/flow/screens/SecurityCalling.tsx
|
|
14
|
+
- bdr/src/lib/securityCallFlow.ts
|
|
15
|
+
- bdr/src/lib/leadName.ts
|
|
16
|
+
- bdr/src/app/api/landing-call/route.ts
|
|
17
|
+
- bdr/src/flow/landingCallApi.ts
|
|
18
|
+
- bdr/src/flow/screens/SecurityLanding.tsx
|
|
19
|
+
- bdr/src/components/ChatPanel.tsx
|
|
20
|
+
- bdr/src/content/security.ts
|
|
21
|
+
- bdr/src/app/security.css
|
|
22
|
+
- bdr/src/proxy.ts
|
|
23
|
+
- bdr/.env.example
|
|
24
|
+
- bdr/test/securityCallFlow.test.ts
|
|
25
|
+
- bdr/test/landingCallRoute.test.ts
|
|
26
|
+
related:
|
|
27
|
+
- security-landing-page.md
|
|
28
|
+
- landing-chat-drawer.md
|
|
29
|
+
- live-call-status.md
|
|
30
|
+
- call-orchestration.md
|
|
31
|
+
- web-funnel-app.md
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## What this is
|
|
35
|
+
|
|
36
|
+
The **"Call me now" path on `/landing`**: a two-screen flow (call form → calling/recap)
|
|
37
|
+
plus a **real, wired, but deliberately dormant** backend route
|
|
38
|
+
(`POST /api/landing-call`) that requests an actual Vapi callback for an **anonymous**
|
|
39
|
+
visitor.
|
|
40
|
+
|
|
41
|
+
It was built 2026-08-26 on the design lead's UI review of the landing page: *"mock is
|
|
42
|
+
right; missing a Call me now button + flow; book a meeting slightly large; check with
|
|
43
|
+
Alex re shared component."*
|
|
44
|
+
|
|
45
|
+
**Read this first:** the route is **off by default**. It requires
|
|
46
|
+
`TOGA_SECURITY_CAMPAIGN_UUID`, and **no cybersecurity campaign exists in Toga yet**, so
|
|
47
|
+
in every environment today the flow runs its **simulation**. That is the intended end
|
|
48
|
+
state for now — *"wired up and ready for the campaign"* — not an unfinished build.
|
|
49
|
+
|
|
50
|
+
## The two campaigns are NOT the same thing (read before wiring anything)
|
|
51
|
+
|
|
52
|
+
AI-BDR has **two independent campaign systems** with unrelated ids. Confusing them is the
|
|
53
|
+
easiest mistake to make on this page, because both serve the same cybersecurity offer.
|
|
54
|
+
|
|
55
|
+
| | **Talos chat campaign** | **Toga dialer campaign** |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Lives in | `api.togaiq.com` → `campaign_registry` | Toga platform → `Client_True.Campaigns` |
|
|
58
|
+
| Used by | the chat drawer ([landing-chat-drawer.md](landing-chat-drawer.md)) | the callback/dialer path (this doc) |
|
|
59
|
+
| Security id | `43fdf2b6-7a5d-4e85-be6b-911f42aa787f` ("External Pen Testing") | **none exists** |
|
|
60
|
+
| Env var | `NEXT_PUBLIC_TALOS_API_BASE` (base URL only) | `TOGA_SECURITY_CAMPAIGN_UUID` |
|
|
61
|
+
|
|
62
|
+
**`43fdf2b6-7a5d-4e85-be6b-911f42aa787f` cannot be used for
|
|
63
|
+
`TOGA_SECURITY_CAMPAIGN_UUID`.** It is a Talos chat-grounding campaign, not a dialer
|
|
64
|
+
campaign, and Toga has never heard of it.
|
|
65
|
+
|
|
66
|
+
`Client_True.Campaigns` in **prod** holds exactly two rows (verified 2026-08-26), both
|
|
67
|
+
**healthcare**, both `c_meetingType = discovery-call`:
|
|
68
|
+
|
|
69
|
+
- `15d0c02f-501e-e081-2ead-61b78c854223` — "26.05 - AI BDR - Ryan Nitti"
|
|
70
|
+
- `d8f7abe6-8f06-0218-03d1-50db575` — "26.04 - AI BDR TEST CAMPAIGN"
|
|
71
|
+
|
|
72
|
+
So there is **no cybersecurity dialer campaign and no cybersecurity Vapi assistant**.
|
|
73
|
+
That is the single blocker for real calls from `/landing`, and it is owned **outside the
|
|
74
|
+
`bdr` repo** (Toga platform + Vapi config).
|
|
75
|
+
|
|
76
|
+
## Screen flow
|
|
77
|
+
|
|
78
|
+
`SecurityLanding.tsx` gained a page-level `screen` state — `landing | call | calling` —
|
|
79
|
+
swapped with `SwapFade` using the **mock's app-shell timings** (`outMs 240`, `inMs 460`,
|
|
80
|
+
`rise 12`, `drop 18`). `backToOverview` resets the recap rotation and scrolls to top.
|
|
81
|
+
|
|
82
|
+
1. **`SecurityCallNow.tsx`** (new) — the mock's security *CallNow* screen: trust column,
|
|
83
|
+
**business email** field with a live hint (*"we'll assess the domain **acme.com**"*,
|
|
84
|
+
derived by `emailDomain`), and a masked phone field. Submit is enabled only when
|
|
85
|
+
`isValidEmail` **and** `isViablePhone` (`isCallFormComplete`).
|
|
86
|
+
2. **`SecurityCalling.tsx`** (new) — the mock's *Success* screen in calling mode:
|
|
87
|
+
`dialing` → **Connected** with an `mm:ss` timer → **End call** → **Call ended** with a
|
|
88
|
+
recap, **Copy**, and three exits: *Call me again* / *Book a meeting* (booking link) /
|
|
89
|
+
*Back to overview*.
|
|
90
|
+
3. **`lib/securityCallFlow.ts`** (new) — the pure helpers, all unit-tested:
|
|
91
|
+
`fillTokens`, `emailDomain`, `isCallFormComplete`, `formatCallTimer`, and the
|
|
92
|
+
recap-summary rotation (short / medium / large by visit count).
|
|
93
|
+
|
|
94
|
+
Entry points: the drawer's new **"Call me now"** button (see
|
|
95
|
+
[landing-chat-drawer.md](landing-chat-drawer.md)) via `ChatPanel`'s new `onCall` prop.
|
|
96
|
+
All copy lives in `src/content/security.ts` under the new `callNow` and `calling` groups
|
|
97
|
+
plus `talos.callCta`.
|
|
98
|
+
|
|
99
|
+
### Why new screens instead of reusing the studio ones
|
|
100
|
+
|
|
101
|
+
`src/flow/screens/CallNow.tsx` and `Success.tsx` (the parked studio funnel) read the
|
|
102
|
+
studio **`CampaignBundle` + lead contexts**, which `/landing` does not provide, and the
|
|
103
|
+
studio `CallNow` has **no email field** at all (the studio funnel primes identity from
|
|
104
|
+
`?hsContactId`). Reuse would have meant threading bundle/lead plumbing into a static
|
|
105
|
+
marketing page.
|
|
106
|
+
|
|
107
|
+
### Deliberate deviations from the mock
|
|
108
|
+
|
|
109
|
+
- **No `AmbientQuestions`** — that component is bundle-driven.
|
|
110
|
+
- Recap has **Copy** but **no Share sheet**.
|
|
111
|
+
- The mock's *"Schedule a call"* became **"Book a meeting"** (booking link), because
|
|
112
|
+
`/landing` has no schedule screen.
|
|
113
|
+
- Footnote **Privacy / Terms are live `togatech.com` links** (funnel precedent), not the
|
|
114
|
+
mock's inert `#` hashes.
|
|
115
|
+
|
|
116
|
+
## The real path — `POST /api/landing-call` (wired, dormant)
|
|
117
|
+
|
|
118
|
+
Built as a **separate route from `/api/call-now`** because the landing visitor is
|
|
119
|
+
**anonymous**: there is no `?hsContactId` priming on `/landing`, so there is no HubSpot
|
|
120
|
+
contact and no lead ref to work from — only the email and phone the visitor just typed.
|
|
121
|
+
|
|
122
|
+
Request `{ email, phoneNumber }`. Order of operations:
|
|
123
|
+
|
|
124
|
+
1. **Validate** — `isValidEmail` + `MAX_EMAIL_LENGTH`, `isValidUSPhone`.
|
|
125
|
+
2. **Config gate** — require `TOGA_SECURITY_CAMPAIGN_UUID` (must be a Toga uuid) or
|
|
126
|
+
return a **503 envelope** *"Live callback is not configured"* **before any upstream
|
|
127
|
+
call**. This is what keeps the route inert without a feature flag.
|
|
128
|
+
3. **Lead** — `togaLeadSink.upsertLead`, with `firstName`/`lastName` derived from the
|
|
129
|
+
email local part by the new `src/lib/leadName.ts` (`nameFromEmail`):
|
|
130
|
+
`jane.doe@…` → `Jane` `Doe`; `jdoe@…` → `Jdoe` + an **empty** lastName. Links the
|
|
131
|
+
campaign.
|
|
132
|
+
4. **Dial** — `togaCallbackService.requestCall(uuid, { type: "now" }, phone)` — the
|
|
133
|
+
**same code path as `/api/call-now`** (contact write + worker fast dial; see
|
|
134
|
+
[live-call-status.md](live-call-status.md)).
|
|
135
|
+
5. **Tokens** — mints the same purpose-scoped status/share tokens as the studio flow.
|
|
136
|
+
|
|
137
|
+
`src/flow/landingCallApi.ts` (`submitLandingCall`) is the client seam and returns
|
|
138
|
+
`{ live, statusHandle }`. **Any** non-success — 503, 400, 500, network — resolves to
|
|
139
|
+
`live: false` and the screen runs the **simulation**. `SecurityCallNow`'s submit posts
|
|
140
|
+
the request but **never blocks the screen advance**.
|
|
141
|
+
|
|
142
|
+
### Live vs. simulation in `SecurityCalling`
|
|
143
|
+
|
|
144
|
+
Mirrors the studio `Success.tsx` split, using the existing
|
|
145
|
+
`useCallStatusViewModel` (see [live-call-status.md](live-call-status.md)):
|
|
146
|
+
|
|
147
|
+
| | Simulation (today) | Live (once configured) |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| Phases | scripted dialing → connected → ended | real Vapi states: dialing → on call (real elapsed) → ended / *"We could not reach you"* |
|
|
150
|
+
| Recap | static, rotating short/medium/large | the **real Vapi recap** replaces it |
|
|
151
|
+
| "End call" button | shown | **hidden** — a browser cannot hang up a phone call |
|
|
152
|
+
| Status unavailable | n/a | **holds at dialing** with a note; never fakes progress |
|
|
153
|
+
|
|
154
|
+
Rate limiting: `/api/landing-call` was added to `src/proxy.ts`'s **call-limiter branch and
|
|
155
|
+
matcher**, sharing the `/api/call-now` bucket.
|
|
156
|
+
|
|
157
|
+
## Turning it on later (config only — no code change)
|
|
158
|
+
|
|
159
|
+
1. Create the **cybersecurity campaign + Vapi assistant** in Toga — runbook:
|
|
160
|
+
`ai-bdr/docs/client-onboarding-sop.md` (see [call-orchestration.md](call-orchestration.md)).
|
|
161
|
+
2. Set `TOGA_SECURITY_CAMPAIGN_UUID` in `.env.local` and in the **Amplify console** (it is
|
|
162
|
+
already covered by the build-time `env | grep` recipe in `amplify.yml` that materializes
|
|
163
|
+
`TOGA_*` into `.env.production` — see [web-funnel-app.md](web-funnel-app.md)).
|
|
164
|
+
3. Redeploy (console vars only reach a build that starts **after** they are saved).
|
|
165
|
+
|
|
166
|
+
**Rejected alternatives (do not redo them):**
|
|
167
|
+
|
|
168
|
+
- *Remove the route until the campaign exists* — it would simply be rebuilt later; the
|
|
169
|
+
env gate already makes it inert.
|
|
170
|
+
- *Point it at a healthcare campaign to test with a real phone* — **do not**. The
|
|
171
|
+
assistant would pitch the wrong offer to a real person.
|
|
172
|
+
|
|
173
|
+
## Open items before the first live run
|
|
174
|
+
|
|
175
|
+
- **An empty `lastName` on an email-only lead is UNVERIFIED against Toga.** `jdoe@…`
|
|
176
|
+
produces `Jdoe` + `""`; nobody has confirmed Toga accepts that on `upsertLead`.
|
|
177
|
+
- **The deployed environment needs `TOGA_VAPI_TOKEN`** for live status to render. Without
|
|
178
|
+
it the call still happens, but the screen **holds at dialing with the unavailable note**
|
|
179
|
+
— by design, not a bug.
|
|
180
|
+
- The recap's "Book a meeting" inherits the landing page's booking-link problem (see
|
|
181
|
+
[security-landing-page.md](security-landing-page.md)).
|
|
182
|
+
|
|
183
|
+
## Gotchas
|
|
184
|
+
|
|
185
|
+
- **Two campaign systems, same product.** See the table above. A Talos `campaign_id` in
|
|
186
|
+
`TOGA_SECURITY_CAMPAIGN_UUID` fails the uuid/Toga check and the route 503s — which
|
|
187
|
+
looks exactly like "not configured," so check *which* id you pasted before debugging.
|
|
188
|
+
- **The 503 is a designed state, not an error to fix.** A dormant route returning
|
|
189
|
+
*"Live callback is not configured"* is the expected production behavior today.
|
|
190
|
+
- **Never let the network decide the UX.** `submitLandingCall` is fire-and-continue: the
|
|
191
|
+
screen advances regardless, and only the *content* (real vs. simulated) depends on the
|
|
192
|
+
response.
|
|
193
|
+
- **No "End call" on the live path.** If you see one during a live call, the live/sim
|
|
194
|
+
branch is wrong — the browser has no way to hang up a Vapi phone call.
|
|
195
|
+
- **An anchor styled as `.btn` renders smaller than a real `<button>` sibling** unless
|
|
196
|
+
`font-size` is pinned — see the drawer doc's gotcha; the same trap produced the
|
|
197
|
+
oversized "Book a meeting" button this session.
|
|
198
|
+
|
|
199
|
+
## How to work on it
|
|
200
|
+
|
|
201
|
+
- 18 new Vitest tests: `test/securityCallFlow.test.ts` (9) and
|
|
202
|
+
`test/landingCallRoute.test.ts` (9).
|
|
203
|
+
- Verified 2026-08-26: `tsc` clean, `eslint` clean, **vitest 385/385**, `next build`
|
|
204
|
+
clean — the route builds as `ƒ /api/landing-call` and **`/landing` stays prerendered
|
|
205
|
+
static** (the call screens are client-side; the POST only happens on submit).
|
|
206
|
+
- **Nothing is committed** — all of it lives in the `bdr` working tree; the developer
|
|
207
|
+
merges and commits.
|
|
208
|
+
|
|
209
|
+
## Change history
|
|
210
|
+
- 2026-08-26 — Initial doc. BUILT the `/landing` "Call me now" flow: `ChatPanel` gained an
|
|
211
|
+
`onCall` prop and a "Call me now" action above "Book a meeting"; new `SecurityCallNow` /
|
|
212
|
+
`SecurityCalling` screens driven by a page-level `landing|call|calling` state in
|
|
213
|
+
`SecurityLanding` (SwapFade on the mock's app-shell timings), new pure helpers in
|
|
214
|
+
`lib/securityCallFlow.ts`, and new `callNow`/`calling` content groups. BUILT the REAL
|
|
215
|
+
callback path — `POST /api/landing-call` for ANONYMOUS visitors (validate → require
|
|
216
|
+
`TOGA_SECURITY_CAMPAIGN_UUID` else 503 before any upstream call → `upsertLead` with a
|
|
217
|
+
name derived from the email local part via new `lib/leadName.ts` → `requestCall` on the
|
|
218
|
+
same path as `/api/call-now` → purpose-scoped status/share tokens), the
|
|
219
|
+
`submitLandingCall` client seam, the live/sim split in `SecurityCalling`, and the
|
|
220
|
+
`/api/call-now` rate-limit bucket. DECIDED to keep it **dormant** until the campaign
|
|
221
|
+
exists (enabling is config only); rejected deleting the route and rejected testing
|
|
222
|
+
against a healthcare campaign. DISCOVERED the Talos chat campaign id is NOT a Toga
|
|
223
|
+
dialer campaign and that prod has only two healthcare dialer campaigns — no
|
|
224
|
+
cybersecurity campaign or Vapi assistant exists, which is the real blocker. Open:
|
|
225
|
+
empty-`lastName` acceptance and `TOGA_VAPI_TOKEN` in the deployed env. tsc/eslint clean,
|
|
226
|
+
385/385 tests, `/landing` still prerendered static. Uncommitted at capture. (tcox)
|
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/components/ChatPanel.tsx
|
|
@@ -19,6 +19,7 @@ files:
|
|
|
19
19
|
- bdr/test/chatMarkdown.test.ts
|
|
20
20
|
related:
|
|
21
21
|
- security-landing-page.md
|
|
22
|
+
- landing-call-flow.md
|
|
22
23
|
- campaign-chat-handoff.md
|
|
23
24
|
- web-funnel-app.md
|
|
24
25
|
- ../../toga-blox/features/talos-assistant.md
|
|
@@ -92,11 +93,22 @@ in the adapter).
|
|
|
92
93
|
into the response type by `toTalosResponse()`. The backend's **silent
|
|
93
94
|
lead/qualification fields are never read** — deliberate, per the handoff.
|
|
94
95
|
|
|
96
|
+
## Drawer actions (both render as of 2026-08-26)
|
|
97
|
+
|
|
98
|
+
The drawer's action row under the message list now carries **two** buttons, in the
|
|
99
|
+
mockup's order:
|
|
100
|
+
|
|
101
|
+
1. **"Call me now"** — new 2026-08-26, driven by `ChatPanel`'s new **`onCall` prop**. It
|
|
102
|
+
swaps `/landing` to the call screens; the flow, its dormant real backend, and the
|
|
103
|
+
Talos-chat-vs-Toga-dialer campaign distinction live in
|
|
104
|
+
[landing-call-flow.md](landing-call-flow.md). **(This corrects the previous "Call me
|
|
105
|
+
now is NOT rendered" note** — that was true only while the sole destination was the
|
|
106
|
+
parked studio call screen. Purpose-built landing screens replaced that dead end.)
|
|
107
|
+
2. **"Book a meeting"** — an `<a>` to the same booking href as the page's primary CTA
|
|
108
|
+
(still the dead Calendly link; see [security-landing-page.md](security-landing-page.md)).
|
|
109
|
+
|
|
95
110
|
## Deliberate deviations from the mockup drawer
|
|
96
111
|
|
|
97
|
-
- **"Call me now" is NOT rendered.** Its destination is the parked studio call screen,
|
|
98
|
-
unreachable from this page. The standing drawer action is **"Book a meeting"** →
|
|
99
|
-
the same Calendly href as the page's primary CTA.
|
|
100
112
|
- **`cta` and `cited_slides` are captured and typed but not rendered yet.** Both are
|
|
101
113
|
clean follow-ups; `cited_slides` additionally needs `GET /api/ai/campaign/{id}` plus
|
|
102
114
|
presigned-URL freshness handling (those URLs expire ~7 days — see the handoff).
|
|
@@ -128,15 +140,36 @@ So the decision: build the shell now, swap it later.
|
|
|
128
140
|
documented theming ceiling), so even fully themed it will **not** match the security
|
|
129
141
|
mockup drawer pixel-for-pixel. Accept that at swap time or fund the spacing-token pass.
|
|
130
142
|
|
|
143
|
+
**Status 2026-08-26 — decision re-affirmed: keep the disposable bridge shell.** A note to
|
|
144
|
+
**apeterson** is **drafted but not yet sent** (the developer will send it). What it has to
|
|
145
|
+
cover, so the alignment conversation starts from facts:
|
|
146
|
+
|
|
147
|
+
- our adapter **already matches his `TalosAdapter` seam** and is **wired live**;
|
|
148
|
+
- the two blockers are still blox-side and unscheduled — **unpublished to npm**, and
|
|
149
|
+
**React `^18` peers vs. BDR's React 19**;
|
|
150
|
+
- the **design divergence** the security mock introduces versus blox `TalosPanel`: the
|
|
151
|
+
**"Call me now" / "Book a meeting" action buttons under the message list**, and the
|
|
152
|
+
cobalt **`.security-shell` theme**. Those must be reconciled as **blox props/theme vs.
|
|
153
|
+
app-side composition** before a swap is possible — that decision is the real gate now,
|
|
154
|
+
not just publishing.
|
|
155
|
+
|
|
131
156
|
## Styling
|
|
132
157
|
|
|
133
158
|
The `.chatveil` / `.chatpanel` block was ported from the security mockup's `styles.css`
|
|
134
159
|
into `security.css` and is **scoped under `.security-shell`** like every other landing
|
|
135
|
-
rule. (
|
|
136
|
-
is
|
|
160
|
+
rule. (**Correction to an earlier note:** the mockup folder was deleted 2026-08-25 but
|
|
161
|
+
**re-added 2026-08-26**, so `security.css` is *not* the only copy — the mockup is back in
|
|
162
|
+
the repo as the design reference. `security.css` remains the only *shipped* copy.)
|
|
163
|
+
`z-index: 120`, width `min(400px, 92vw)`, reduced-motion respected.
|
|
137
164
|
`.chatmd-p` / `.chatmd-list` carry the reply rhythm. Escape and scrim click close the
|
|
138
165
|
drawer; focus moves into the composer on open and is restored to the opener on close.
|
|
139
166
|
|
|
167
|
+
**Action-row sizing (fixed 2026-08-26).** "Book a meeting" rendered visibly larger than the
|
|
168
|
+
mockup's. Root cause: the mockup's chat actions are `<button>`s, which browsers draw at the
|
|
169
|
+
~13.3px default button font because `.btn` never sets `font: inherit`; ours is an `<a>`
|
|
170
|
+
inheriting the panel's body size. Both are now pinned:
|
|
171
|
+
`.security-shell .chatactions .btn{font-size:13.5px;padding:0 16px;min-height:44px}`.
|
|
172
|
+
|
|
140
173
|
## Gotchas
|
|
141
174
|
|
|
142
175
|
- **`api.togaiq.com` chat endpoints are origin/UA-gated — verified 2026-08-25 with
|
|
@@ -157,21 +190,38 @@ drawer; focus moves into the composer on open and is restored to the opener on c
|
|
|
157
190
|
any merged/trimmed turns would break the strict alternating shape and return a 422.
|
|
158
191
|
- **The chat is fully client-side**, which is why `/landing` still builds as a
|
|
159
192
|
**prerendered static** route. Adding a server call here changes that.
|
|
160
|
-
- **`/landing` is not rate-limited** (`src/proxy.ts` matches
|
|
161
|
-
|
|
162
|
-
|
|
193
|
+
- **`/landing` is not rate-limited** (`src/proxy.ts` matches `/` and the API routes, not
|
|
194
|
+
the page). The drawer talks to a third-party origin, not a BDR API route, so the limiter
|
|
195
|
+
still does not apply to *chat* — but the abuse surface exists on the page. The drawer's
|
|
196
|
+
new "Call me now" action does hit a BDR route, and that one **is** limited
|
|
197
|
+
(`/api/landing-call`, on the `/api/call-now` bucket).
|
|
163
198
|
|
|
164
199
|
## How to work on it
|
|
165
200
|
|
|
166
201
|
- 30 new Vitest tests across `test/talosChat.test.ts` and `test/chatMarkdown.test.ts`
|
|
167
202
|
(message-shape rules including the 30-cap alternation, 401 re-mint-once, fallback on
|
|
168
203
|
network error and on 5xx, alternation preserved across a fallback turn, adapter
|
|
169
|
-
reset, markdown safety). Suite **
|
|
170
|
-
no-em-dash rule), and `next build` all clean
|
|
171
|
-
- **
|
|
172
|
-
|
|
204
|
+
reset, markdown safety). Suite **385/385 green** as of 2026-08-26 (was 367 before the
|
|
205
|
+
call-flow tests); `tsc`, `eslint` (incl. the no-em-dash rule), and `next build` all clean.
|
|
206
|
+
- **Shipped state:** the drawer landed on `_dev-sandbox` via **TRUE-80965**, merged by the
|
|
207
|
+
developer 2026-08-25. **TRUE-80966** ("Connect Talos UI to Backend Services") is
|
|
208
|
+
engineering-complete — chat is live from allowlisted origins — but **acceptance is
|
|
209
|
+
pending** on three items: the sandbox **origin allowlist**, rendering of **`cta` /
|
|
210
|
+
`cited_slides`**, and the **absent CRM lead sink**. The 2026-08-26 action-row changes are
|
|
211
|
+
uncommitted working tree.
|
|
173
212
|
|
|
174
213
|
## Change history
|
|
214
|
+
- 2026-08-26 — The drawer now renders **both** mockup actions: a new **"Call me now"**
|
|
215
|
+
button (via `ChatPanel`'s new `onCall` prop) above "Book a meeting" — **reversing** the
|
|
216
|
+
2026-08-24 decision not to render it, which only held while the destination was the
|
|
217
|
+
parked studio call screen (see [landing-call-flow.md](landing-call-flow.md) for the new
|
|
218
|
+
screens). FIXED "Book a meeting" rendering too large (`<a>` inherits body size, sibling
|
|
219
|
+
`<button>`s use the ~13.3px browser default; both pinned in `.chatactions .btn`).
|
|
220
|
+
CORRECTED the styling note — the security mockup was re-added to the repo on 2026-08-26,
|
|
221
|
+
so `security.css` is only the *shipped* copy, not the *only* copy. Re-affirmed the
|
|
222
|
+
TRUE-80692 "keep the disposable bridge shell" decision and recorded the drafted (not yet
|
|
223
|
+
sent) note to apeterson, including the new design divergence — action buttons + cobalt
|
|
224
|
+
theme — that must be reconciled as blox props/theme vs. app-side. (tcox)
|
|
175
225
|
- 2026-08-25 — Verified the origin gate with browser-shaped probes: `localhost:3000` is
|
|
176
226
|
allowed and returns live replies end-to-end; `bdr.dev.sandbox.togatech.com` gets 403 at
|
|
177
227
|
session mint (not allowlisted → deployed drawer runs on scripted fallback). Recorded the
|
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/server/vapiCall.ts
|
|
@@ -24,6 +24,7 @@ files:
|
|
|
24
24
|
- bdr/src/content/default.ts
|
|
25
25
|
- bdr/src/proxy.ts
|
|
26
26
|
related:
|
|
27
|
+
- landing-call-flow.md
|
|
27
28
|
- web-funnel-app.md
|
|
28
29
|
- vapi-integration.md
|
|
29
30
|
- call-orchestration.md
|
|
@@ -144,6 +145,13 @@ rules that are specific to this feature:
|
|
|
144
145
|
looks fake in this environment" is an **env** symptom, not a code bug.
|
|
145
146
|
|
|
146
147
|
## Change history
|
|
148
|
+
- 2026-08-26 — **Second consumer:** `/landing`'s new `SecurityCalling` screen reuses this
|
|
149
|
+
pipeline unchanged (`useCallStatusViewModel` + `/api/call-status` + the same
|
|
150
|
+
purpose-scoped status token), fed by the new `POST /api/landing-call` for anonymous
|
|
151
|
+
visitors. Two behavior notes specific to it: it shows **no "End call" button on the live
|
|
152
|
+
path**, and an **unavailable** status **holds at dialing** rather than faking progress.
|
|
153
|
+
The route is env-gated and currently dormant — see
|
|
154
|
+
[landing-call-flow.md](landing-call-flow.md). No change to this feature's own code. (tcox)
|
|
147
155
|
- 2026-07-29 — Created. BUILT the live call-status pipeline: `src/server/vapiCall.ts` reads
|
|
148
156
|
`GET https://api.vapi.ai/call/{id}` with the worker's Vapi bearer credential and drives a real
|
|
149
157
|
dialing / active / ended / not-answered lifecycle on the Success screen (replacing the ~5s
|
|
@@ -6,12 +6,15 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/app/landing/page.tsx
|
|
13
13
|
- bdr/src/app/security.css
|
|
14
14
|
- bdr/src/flow/screens/SecurityLanding.tsx
|
|
15
|
+
- bdr/src/flow/screens/SecurityCallNow.tsx
|
|
16
|
+
- bdr/src/flow/screens/SecurityCalling.tsx
|
|
17
|
+
- bdr/src/lib/securityCallFlow.ts
|
|
15
18
|
- bdr/src/content/security.ts
|
|
16
19
|
- bdr/src/components/LogoSymbol.tsx
|
|
17
20
|
- bdr/src/components/AssistantMark.tsx
|
|
@@ -24,6 +27,7 @@ files:
|
|
|
24
27
|
- bdr/eslint.config.mjs
|
|
25
28
|
related:
|
|
26
29
|
- landing-chat-drawer.md
|
|
30
|
+
- landing-call-flow.md
|
|
27
31
|
- campaign-chat-handoff.md
|
|
28
32
|
- web-funnel-app.md
|
|
29
33
|
- web-funnel-content-model.md
|
|
@@ -38,19 +42,21 @@ and the 7-screen Agent Studio funnel is parked (see `web-funnel-app.md`). Its jo
|
|
|
38
42
|
conversion: *"Book your free risk assessment."*
|
|
39
43
|
|
|
40
44
|
It is a **pixel-faithful port of a designer mockup**, not new design work. The source
|
|
41
|
-
mockup
|
|
42
|
-
`app.jsx`, `screens.jsx`, `components.jsx`, `styles.css`, plus a
|
|
43
|
-
`ground-truth-tokens-and-stack.md` token spec)
|
|
44
|
-
2026-08-25
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
mockup lives at **`BDR/AI BDR - Security/`** — a React 18 UMD + in-browser Babel prototype
|
|
46
|
+
(`app.jsx`, `screens.jsx`, `components.jsx`, `styles.css`, plus a
|
|
47
|
+
`ground-truth-tokens-and-stack.md` token spec). **History, because an earlier capture got
|
|
48
|
+
it wrong:** the folder was deleted on 2026-08-25 (port believed complete) and then
|
|
49
|
+
**RE-ADDED by the developer on 2026-08-26** — it is **in the repo today** and is the
|
|
50
|
+
standing design reference for further porting. The landing is that mockup's
|
|
51
|
+
**`Capabilities` screen**; because `src/flow/screens/Capabilities.tsx` already exists as a
|
|
52
|
+
studio screen, it was ported under the name **`SecurityLanding`**.
|
|
53
|
+
|
|
54
|
+
**Status (updated 2026-08-26): two backends wired, one of them dormant.** The page carries
|
|
55
|
+
a **Talos chat drawer wired to the real campaign-chat backend** — see
|
|
56
|
+
[landing-chat-drawer.md](landing-chat-drawer.md) — and, as of 2026-08-26, a **"Call me
|
|
57
|
+
now" flow with a real (but env-gated, currently inert) callback route**, which is its own
|
|
58
|
+
subject in [landing-call-flow.md](landing-call-flow.md). That call flow is the page's
|
|
59
|
+
**first form and first lead capture**. Still absent: landing-specific analytics events.
|
|
54
60
|
|
|
55
61
|
## Page structure
|
|
56
62
|
|
|
@@ -61,6 +67,11 @@ events of its own.
|
|
|
61
67
|
screen — **carries its own page shell**: the noise layer, the mote field, the topbar
|
|
62
68
|
lockup, and the header CTA. There is no shared layout wrapper for it.
|
|
63
69
|
|
|
70
|
+
Since 2026-08-26 `SecurityLanding` is also a **tiny router**: a page-level `screen` state
|
|
71
|
+
(`landing | call | calling`) swaps the marketing page for the call screens via `SwapFade`.
|
|
72
|
+
The marketing sections below are the `landing` case; see
|
|
73
|
+
[landing-call-flow.md](landing-call-flow.md) for the other two.
|
|
74
|
+
|
|
64
75
|
Section order inside the shell:
|
|
65
76
|
|
|
66
77
|
1. **Hero** (`.lp-hero`) — eyebrow, headline whose closing clause is the TOGA gradient
|
|
@@ -127,9 +138,12 @@ does not define or defines differently.
|
|
|
127
138
|
was verified byte-identical**, so the 7 existing studio screens do not shift. Any future
|
|
128
139
|
change here must keep that property.
|
|
129
140
|
- **`LogoSymbol.tsx`** / **`AssistantMark.tsx`** — new inline-SVG marks.
|
|
130
|
-
- **`eslint.config.mjs`** —
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
- **`eslint.config.mjs`** — the `"AI BDR - Security/**"` `globalIgnores` entry is
|
|
142
|
+
**present and required**. It was removed alongside the 2026-08-25 mockup deletion and
|
|
143
|
+
**restored on 2026-08-26** when the mockup came back; lint is clean with it. (The studio
|
|
144
|
+
flow's `mockup/**` entry is separate and has always been there.) If the mockup folder is
|
|
145
|
+
ever removed again, remove this entry with it — and if you see lint errors in
|
|
146
|
+
Babel-flavored `.jsx`, this entry is what is missing.
|
|
133
147
|
|
|
134
148
|
## CTA wiring (read this before assuming the page converts)
|
|
135
149
|
|
|
@@ -153,6 +167,13 @@ does not define or defines differently.
|
|
|
153
167
|
the mockup's "Ask Me Anything") both **open the Talos chat drawer** as of 2026-08-24.
|
|
154
168
|
Neither is inert any more. The drawer, its backend adapter, and its offline fallback
|
|
155
169
|
are documented in [landing-chat-drawer.md](landing-chat-drawer.md).
|
|
170
|
+
- **"Call me now"** (new 2026-08-26, in the drawer's action row above "Book a meeting")
|
|
171
|
+
swaps the page to the call screens. The backend behind it is **real but dormant** —
|
|
172
|
+
`POST /api/landing-call` 503s until `TOGA_SECURITY_CAMPAIGN_UUID` is set, and no
|
|
173
|
+
cybersecurity Toga campaign exists yet, so the flow currently runs its **simulation**.
|
|
174
|
+
Full detail, including the *do-not-confuse-these* Talos-chat-vs-Toga-dialer campaign
|
|
175
|
+
distinction and the enable-later runbook, in
|
|
176
|
+
[landing-call-flow.md](landing-call-flow.md).
|
|
156
177
|
|
|
157
178
|
## Responsive / mobile pass (original work, not ported)
|
|
158
179
|
|
|
@@ -171,11 +192,16 @@ ported. All of it is scoped under `.security-shell`.
|
|
|
171
192
|
|
|
172
193
|
## How to work on it
|
|
173
194
|
|
|
174
|
-
- `npx tsc --noEmit`, `npm run lint`, `npm run build` — all clean as of
|
|
175
|
-
`/landing` builds as a **prerendered static** route.
|
|
176
|
-
`GET /` → `307` with `Location: /landing`, `GET /landing` → `200`.
|
|
177
|
-
- **
|
|
178
|
-
|
|
195
|
+
- `npx tsc --noEmit`, `npm run lint`, `npm run build`, `npx vitest run` — all clean as of
|
|
196
|
+
2026-08-26 (385/385 tests); `/landing` still builds as a **prerendered static** route.
|
|
197
|
+
Local prod smoke test: `GET /` → `307` with `Location: /landing`, `GET /landing` → `200`.
|
|
198
|
+
- **Shipping state:** the landing + chat drawer landed on `_dev-sandbox` via **TRUE-80965**
|
|
199
|
+
("integrate Talos UI into landing"), merged by the developer 2026-08-25 — one conflict in
|
|
200
|
+
`security.ts`, resolved to the TRUE-80965 side. **TRUE-80966** ("Connect Talos UI to
|
|
201
|
+
Backend Services") is engineering-complete (chat is live from allowlisted origins) but
|
|
202
|
+
**acceptance is pending**: the sandbox origin allowlist, rendering of `cta` /
|
|
203
|
+
`cited_slides`, and the absent CRM lead sink. The 2026-08-26 call-flow work is
|
|
204
|
+
**uncommitted working tree** — the developer merges and commits.
|
|
179
205
|
|
|
180
206
|
## Known gaps / pending work
|
|
181
207
|
|
|
@@ -186,7 +212,12 @@ ported. All of it is scoped under `.security-shell`.
|
|
|
186
212
|
- **Chat drawer's origin allowlist is not done** — `api.togaiq.com` is origin/UA gated,
|
|
187
213
|
so until localhost / sandbox / prod domains are allowlisted the drawer silently answers
|
|
188
214
|
from its scripted fallback (see [landing-chat-drawer.md](landing-chat-drawer.md)).
|
|
189
|
-
- No
|
|
215
|
+
- **No cybersecurity Toga campaign / Vapi assistant exists**, so the "Call me now" path
|
|
216
|
+
cannot place real calls. Owned outside the `bdr` repo; see
|
|
217
|
+
[landing-call-flow.md](landing-call-flow.md).
|
|
218
|
+
- No landing-specific analytics events.
|
|
219
|
+
- ~~No form, no lead capture~~ — **the call flow added both 2026-08-26** (email + phone →
|
|
220
|
+
`upsertLead`), though the lead write only fires once the campaign env var is set.
|
|
190
221
|
- ~~Mobile pass pending~~ — **done 2026-08-24** (see *Responsive / mobile pass* above).
|
|
191
222
|
- ~~Chat drawer not ported~~ — **done 2026-08-24**, and wired to the real backend rather
|
|
192
223
|
than re-creating the mockup's scripted-only behavior.
|
|
@@ -201,36 +232,66 @@ ported. All of it is scoped under `.security-shell`.
|
|
|
201
232
|
`Capabilities.tsx` is a *studio* screen — hence `SecurityLanding.tsx`. Likewise
|
|
202
233
|
`src/flow/screens/Landing.tsx` is a **studio** screen and has nothing to do with the
|
|
203
234
|
`/landing` route. Check which is meant before editing either.
|
|
204
|
-
- **`.btn` does not reset anchor styling.** Every studio `.btn` is a
|
|
205
|
-
studio.css never needed
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
- **`.btn` does not reset anchor styling — in TWO ways.** Every studio `.btn` is a
|
|
236
|
+
`<button>`, so studio.css never needed to normalize anchors.
|
|
237
|
+
1. *Underline.* An `<a class="btn">` arrives underlined; scoped
|
|
238
|
+
`.security-shell a.btn{text-decoration:none}` fixes it (and a matching
|
|
239
|
+
`.ended-cta a.btn` reset was needed for the call-recap buttons, 2026-08-26).
|
|
240
|
+
2. *Font size (found 2026-08-26).* A real `<button>` renders at the browser's ~13.3px
|
|
241
|
+
default button font unless `font: inherit` is set, while an `<a>` inherits its
|
|
242
|
+
container's body size — so **an anchor-as-`.btn` sitting next to real buttons looks
|
|
243
|
+
visibly larger**. That was the root cause of the "Book a meeting" drawer button being
|
|
244
|
+
oversized; the fix pins the size for both:
|
|
245
|
+
`.security-shell .chatactions .btn{font-size:13.5px;padding:0 16px;min-height:44px}`.
|
|
246
|
+
Whenever you mix anchor and button actions in one row, pin `font-size`.
|
|
208
247
|
- **`Particles` default behavior must stay byte-identical.** The new `palette`/`bold`/
|
|
209
248
|
`radiate` props are additive on purpose; the 7 studio screens depend on the untouched
|
|
210
249
|
default path.
|
|
211
|
-
- **
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
- **The security mockup IS in the repo — do not trust the 2026-08-25 "deleted" note.**
|
|
251
|
+
It was deleted on 2026-08-25 and **re-added on 2026-08-26**; `BDR/AI BDR - Security/` is
|
|
252
|
+
the live design reference again (the call screens were ported from it). Check the working
|
|
253
|
+
tree before assuming either way. Its `styles.css` carries the same literal-`\n`
|
|
254
|
+
corruption class as `mockup/styles.css` (a JS-string generation artifact, see
|
|
255
|
+
`web-funnel-app.md`) — treat ported CSS accordingly.
|
|
217
256
|
- **No `<head>` resource surprises this time**, but the standing rule from the first port
|
|
218
257
|
still applies: a font named in ported CSS is not a loaded font. The landing rides
|
|
219
258
|
`layout.tsx`'s existing Plus Jakarta Sans.
|
|
220
|
-
- **`/landing` is not rate-limited
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
the
|
|
259
|
+
- **`/landing` itself is still not rate-limited** — `src/proxy.ts`'s matcher covers `/`
|
|
260
|
+
and the API routes, not the page. The page now **has a form** (the call flow), but the
|
|
261
|
+
abuse surface is the API route, and **`/api/landing-call` WAS added to the matcher and
|
|
262
|
+
shares the `/api/call-now` limiter bucket** (2026-08-26). The rule stands for anything
|
|
263
|
+
new: a page-level read of a query param or contact data would still need the matcher
|
|
264
|
+
widened.
|
|
265
|
+
- **`src/content/security.ts` is CRLF.** A `\n`-only regex will not match its line
|
|
266
|
+
endings — that silently defeated a scripted merge-conflict resolution during the
|
|
267
|
+
TRUE-80965 merge (resolved by hand to the TRUE-80965 side). Use `\r?\n` in any script
|
|
268
|
+
that rewrites this file.
|
|
224
269
|
|
|
225
270
|
## Change history
|
|
271
|
+
- 2026-08-26 — **CORRECTION to the 2026-08-25 entry below:** the developer **re-added**
|
|
272
|
+
`BDR/AI BDR - Security/` on 2026-08-26. The mockup is in the repo and is the standing
|
|
273
|
+
design reference; the `"AI BDR - Security/**"` `globalIgnores` entry in
|
|
274
|
+
`eslint.config.mjs` was **restored** with it (lint clean). The porting gotchas are live
|
|
275
|
+
again, not retired. (tcox)
|
|
276
|
+
- 2026-08-26 — BUILT the **"Call me now" flow** on the page (design-lead review) and the
|
|
277
|
+
real-but-dormant `POST /api/landing-call` behind it; `SecurityLanding` became a
|
|
278
|
+
`landing|call|calling` SwapFade router over the new `SecurityCallNow`/`SecurityCalling`
|
|
279
|
+
screens. This gives the page its **first form and first lead capture**. Own doc:
|
|
280
|
+
[landing-call-flow.md](landing-call-flow.md). FIXED the oversized drawer "Book a meeting"
|
|
281
|
+
button — root cause was `<a class="btn">` inheriting the panel body size while sibling
|
|
282
|
+
`<button>`s render at the ~13.3px browser default; both are now pinned. Recorded that
|
|
283
|
+
`/api/landing-call` joined the `proxy.ts` call-limiter, and that `security.ts` is CRLF
|
|
284
|
+
(which defeated a `\n`-only conflict-resolution regex during the TRUE-80965 merge to
|
|
285
|
+
`_dev-sandbox`). tsc/eslint/build clean, 385/385 tests, `/landing` still prerendered
|
|
286
|
+
static. (tcox)
|
|
226
287
|
- 2026-08-25 — VERIFIED the primary-CTA booking link is dead: the mockup's Calendly URL and
|
|
227
288
|
the whole `calendly.com/toga-technology` org 404. TOGA books via Cal.com team
|
|
228
289
|
`toga-technology`, which has only the healthcare `discovery-call` event type (both prod
|
|
229
290
|
campaigns use it); no security event type exists. Recorded the fix path (create Cal.com
|
|
230
291
|
event → set `primaryCtaHref` + campaign `cta_url`). (tcox)
|
|
231
|
-
- 2026-08-25 —
|
|
232
|
-
|
|
233
|
-
`globalIgnores` entry was removed with it
|
|
292
|
+
- 2026-08-25 — **[SUPERSEDED 2026-08-26 — the folder was restored; see above.]** The
|
|
293
|
+
source mockup folder `BDR/AI BDR - Security/` was **deleted from the repo** (port
|
|
294
|
+
believed complete). Its `eslint.config.mjs` `globalIgnores` entry was removed with it.
|
|
234
295
|
(tcox)
|
|
235
296
|
- 2026-08-24 — Closed both of the doc's open gaps. BUILT the **mobile pass** as original
|
|
236
297
|
work (560px and 420px breakpoints under `.security-shell`; the `.lp-value-motes` inset
|
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
@@ -67,6 +67,7 @@ related:
|
|
|
67
67
|
- bdr-web-funnel-plan.md
|
|
68
68
|
- live-call-status.md
|
|
69
69
|
- security-landing-page.md
|
|
70
|
+
- landing-call-flow.md
|
|
70
71
|
- ../architecture.md
|
|
71
72
|
- ../workflows/safe-call-loop-testing.md
|
|
72
73
|
- ../../worker2/features/vapi-webhook-handler.md
|
|
@@ -458,6 +459,15 @@ everything?" question in this doc:
|
|
|
458
459
|
even in info's own code — do not carry them into BDR.
|
|
459
460
|
- **BDR deploy env vars = 7 total:** the 5 server-side ones + `NEXT_PUBLIC_GA_MEASUREMENT_ID` +
|
|
460
461
|
(new 2026-07-21) **`TOGA_WORKER_URL`** (the fast-dial gate; unset ⇒ cron-only).
|
|
462
|
+
- **Optional 8th, deliberately UNSET (new 2026-08-26): `TOGA_SECURITY_CAMPAIGN_UUID`** —
|
|
463
|
+
declared in `.env.example`, the on/off gate for `/landing`'s real callback route. Unset ⇒
|
|
464
|
+
`POST /api/landing-call` returns a 503 *"Live callback is not configured"* before touching
|
|
465
|
+
Toga, and the landing call flow runs its simulation. **Leave it unset until a cybersecurity
|
|
466
|
+
Toga campaign + Vapi assistant exist** (none does), and never set it to a Talos chat
|
|
467
|
+
`campaign_id` — different system. See
|
|
468
|
+
[landing-call-flow.md](landing-call-flow.md). It needs no `amplify.yml` change: the
|
|
469
|
+
build-time `env | grep -E '^(HUBSPOT_|TOGA_|NEXT_PUBLIC_|RATE_LIMIT_)'` line already
|
|
470
|
+
matches `TOGA_*`.
|
|
461
471
|
|
|
462
472
|
## Local dev setup (BDR)
|
|
463
473
|
|
|
@@ -751,6 +761,12 @@ would be cached by browsers and crawlers and become painful to undo.
|
|
|
751
761
|
harness; pull it with `npx toga-ai`.
|
|
752
762
|
|
|
753
763
|
## Change history
|
|
764
|
+
- 2026-08-26 — Env-var accounting only: recorded an **optional 8th deploy var**,
|
|
765
|
+
`TOGA_SECURITY_CAMPAIGN_UUID`, declared in `.env.example` and deliberately **left unset**.
|
|
766
|
+
It gates `/landing`'s new real callback route (`POST /api/landing-call`), which 503s and
|
|
767
|
+
falls back to simulation without it; no `amplify.yml` change is needed because the
|
|
768
|
+
build-time `env | grep` recipe already matches `TOGA_*`. The funnel itself is unchanged.
|
|
769
|
+
See [landing-call-flow.md](landing-call-flow.md). (tcox)
|
|
754
770
|
- 2026-08-24 — **DECIDED: `/` 307-redirects to `/landing` and the whole Agent Studio flow is
|
|
755
771
|
parked.** `next.config.ts` gained `redirects()` with `permanent: false` (deliberately
|
|
756
772
|
temporary so browsers/crawlers do not cache it while design settles). The new
|
package/knowledge/2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md
CHANGED
|
@@ -18,7 +18,7 @@ related:
|
|
|
18
18
|
- ../../../../clients/elite/features/supply2-scope.md
|
|
19
19
|
- ../workflows/client-host-scoping.md
|
|
20
20
|
- ../architecture.md
|
|
21
|
-
- ../../_underscore/features/
|
|
21
|
+
- ../../_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
## Summary
|
|
@@ -109,7 +109,7 @@ As of 2026-08-26 that still reports `1` for `DEFAULTFIELDS.json` and the three `
|
|
|
109
109
|
fix was drafted this session but is not present on any branch or stash.** The backend half (the
|
|
110
110
|
`_purchaseOrders` calculated field reading the wrong bridge table) is a separate, independent cause
|
|
111
111
|
of the same blank row; see
|
|
112
|
-
[PO-to-SO bridge tables](../../_underscore/features/
|
|
112
|
+
[PO-to-SO bridge tables](../../_underscore/features/sales-order-purchase-order-bridge-direction.md).
|
|
113
113
|
|
|
114
114
|
### Which field set a host gets
|
|
115
115
|
|
|
@@ -164,7 +164,7 @@ requires **both** changes, not either one:
|
|
|
164
164
|
covers COMPASS / COMPASSCANADA / QUAD (+ an ELITE early return), so **every other host falls
|
|
165
165
|
through to `DEFAULTFIELDS`** — a default-set gap is a platform-wide gap. The four-file fix was
|
|
166
166
|
drafted but is **not on any branch**; the backend half is a separate cause, see
|
|
167
|
-
[PO-to-SO bridge tables](../../_underscore/features/
|
|
167
|
+
[PO-to-SO bridge tables](../../_underscore/features/sales-order-purchase-order-bridge-direction.md). (bala)
|
|
168
168
|
- 2026-08-18 — First capture. Documented that the order-details Customer name is **hardcoded in the
|
|
169
169
|
frontend**, not read from the database: every host's config uses
|
|
170
170
|
`valueKey: "orderBoxDetails.customer.name"`, `orderBoxDetails` exists nowhere in the codebase or
|
package/knowledge/INDEX.md
CHANGED
|
@@ -28,7 +28,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
28
28
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
29
29
|
- **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
30
30
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
31
|
-
- **ai-bdr** (AI-BDR) —
|
|
31
|
+
- **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
32
32
|
- **toga2-commerce** (TOGa Commerce) — 19 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
33
33
|
- **toga25-supply** (TOGa 2.5 Supply) — 12 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
34
34
|
- **toga-blox** (TOGa Blox) — 10 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql |
|
|
5
|
+
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
6
6
|
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql |
|
|
@@ -6,12 +6,14 @@ project: _Underscore
|
|
|
6
6
|
client: nychh
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
13
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
13
14
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
|
|
14
15
|
- dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
|
|
16
|
+
- dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql
|
|
15
17
|
related:
|
|
16
18
|
- ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
17
19
|
- ../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
@@ -22,7 +24,13 @@ related:
|
|
|
22
24
|
|
|
23
25
|
NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live
|
|
24
26
|
in the **upstream** table `PurchaseOrders_SalesOrders` (route `purchase-order-sales-orders`,
|
|
25
|
-
record 276).
|
|
27
|
+
record 276). Their POs are the customer's own numbers — `EIT0011108`, `COR0015555`,
|
|
28
|
+
`LOI-Woodhull` — written from NetSuite `otherRefNum` by `_Trait_Netsuite_SalesOrder`.
|
|
29
|
+
|
|
30
|
+
**Refinement (prod, 2026-08-26): "no downstream rows" holds for the vast majority but not all.** Of
|
|
31
|
+
822 NYCHH sales orders, **64 carry a PO in BOTH directions**, which is exactly the set where the
|
|
32
|
+
list and the modal can disagree — see the open tie-break below. On prod the downstream-only base
|
|
33
|
+
field resolved a PO on only **64 of 822** orders.
|
|
26
34
|
|
|
27
35
|
This produced a symptom that looked like a bug and was not: the sales-order modal's PO Number came
|
|
28
36
|
back as an empty array while the sales-orders **list** column showed a PO number for the same
|
|
@@ -44,16 +52,35 @@ the **upstream** table.
|
|
|
44
52
|
|
|
45
53
|
## Gotchas
|
|
46
54
|
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
- ✅ **The open consequence is addressed on `_beta` — and NOT with a per-tenant override.** The
|
|
56
|
+
shared `_purchaseOrders` field read the **downstream** table only
|
|
57
|
+
(`_underscore/Model/Client/SalesOrder.php:349`), so once the modal bound to `_purchaseOrders`
|
|
58
|
+
NYCHH resolved it without an `EV-8` and got `null`. Rather than a NYCHH-specific field, the
|
|
59
|
+
**base** field became a `COALESCE` of downstream-then-upstream, which fixed every upstream tenant
|
|
60
|
+
at once: NYCHH went from 64 to **819 of 819** orders resolving a PO, with **no existing value
|
|
61
|
+
changed** for any tenant. It sits on `_beta` / the sandbox branches, **not `_production`** — see
|
|
62
|
+
[PO Number sourcing](../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md) for
|
|
63
|
+
the exact commits. NYCHH is also not the only upstream tenant: all 22 clients on
|
|
64
|
+
`_Trait_Netsuite_SalesOrder` write upstream.
|
|
65
|
+
- 🚨 **Still open for NYCHH specifically: the 64 both-direction orders.** Downstream-first makes the
|
|
66
|
+
modal show the **vendor** PO while the list keeps showing the **customer** PO. A
|
|
67
|
+
`_Model_Nychh_SalesOrder` override preferring upstream was offered and not built. Decide which one
|
|
68
|
+
NYCHH should see before treating either surface as authoritative.
|
|
69
|
+
- **The NYCHH orders list is not a template.** Its table-view join through the upstream bridge is
|
|
70
|
+
safe **only** because NYCHH's upstream bridge is coincidentally 1:1 (823 rows / 823 distinct sales
|
|
71
|
+
orders). Copying that join into a shared single-record fetch breaks Compass (up to 6,440 POs on one
|
|
72
|
+
order), Compass Canada (empty upstream bridge) and Prudential (1,868 of 26,843 upstream).
|
|
53
73
|
- Do not "fix" this by adding downstream rows. The direction is correct for NYCHH's business model;
|
|
54
|
-
the shared field is what
|
|
74
|
+
the shared field is what was incomplete.
|
|
55
75
|
|
|
56
76
|
## Change history
|
|
77
|
+
- 2026-08-26 — Closed the open `_purchaseOrders` gap: fixed in the **base** field as a
|
|
78
|
+
downstream-then-upstream `COALESCE` (not a NYCHH override), taking NYCHH from 64 to **819 of 819**
|
|
79
|
+
orders with a PO and changing no existing value; currently on `_beta`, not `_production`. Refined
|
|
80
|
+
"no downstream rows" — **64 of 822** NYCHH orders have both directions, which is the remaining open
|
|
81
|
+
question (modal would show the vendor PO, the list shows the customer PO). Recorded that the NYCHH
|
|
82
|
+
orders-list upstream join is safe only because their upstream bridge is 1:1 and must not be copied
|
|
83
|
+
into a shared fetch. (bala)
|
|
57
84
|
- 2026-08-25 — Diagnosed the empty-array PO Number as a wrong-direction query (upstream data,
|
|
58
85
|
downstream route); ruled out ACL and migrations; recorded the still-open `_purchaseOrders`
|
|
59
86
|
downstream-only gap for this tenant (apeterson)
|
package/package.json
CHANGED