toga-ai 1.0.656 → 1.0.658
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/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +139 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/persona-model-and-levy-gating.md +25 -7
- package/knowledge/clients/compass-usa/profile.md +13 -3
- package/knowledge/clients/compass-usa/workflows/granting-persona-bundle-access.md +163 -0
- package/knowledge/clients/compass-usa/workflows/persona-refactor-migration.md +34 -1
- package/knowledge/sessions/2026-08-26-bdr-landing-call-flow-tcox.md +95 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
|
+
| [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
|
|
6
7
|
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | 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, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.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, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.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, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql |
|
|
7
8
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
8
9
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
13
|
+
related:
|
|
14
|
+
- ../architecture.md
|
|
15
|
+
- ../../_underscore/features/acl-permission-chain.md
|
|
16
|
+
- ../../_underscore/features/tracking-number-bridges.md
|
|
17
|
+
- ../../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
Most `dbchanges2` files are **additive data grants** run by hand against production, often more than
|
|
23
|
+
once (once per environment, or twice because someone was not sure it took). Two mechanics decide
|
|
24
|
+
whether that is safe: how you generate the `uuid` every 2.0 table demands, and how you guard the
|
|
25
|
+
insert. Both have a trap that fails **silently** rather than erroring.
|
|
26
|
+
|
|
27
|
+
Every 2.0 table carries a `uuid` column that is `char(36) NOT NULL` **with no default** — the model
|
|
28
|
+
layer fills it in application code, so a hand-written `INSERT` must supply it itself.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
### Generating a real UUID4 inline
|
|
33
|
+
|
|
34
|
+
The standard is "UUID4, random — never MySQL `UUID()`" (`UUID()` is v1: MAC address + timestamp, and
|
|
35
|
+
sequential). MySQL 8's `RANDOM_BYTES()` gives a genuinely random, correctly **version-4-shaped** value
|
|
36
|
+
in one expression:
|
|
37
|
+
|
|
38
|
+
```sql
|
|
39
|
+
LOWER(CONCAT(HEX(RANDOM_BYTES(4)), '-', HEX(RANDOM_BYTES(2)), '-4', RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', SUBSTRING('89ab', FLOOR(1 + RAND() * 4), 1), RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', HEX(RANDOM_BYTES(6))))
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The two literals are what make it v4 rather than just random hex: the `'-4'` pins the version nibble,
|
|
43
|
+
and `SUBSTRING('89ab', …)` picks the variant nibble. Verified working on prod MySQL **8.0.39**.
|
|
44
|
+
|
|
45
|
+
**Existing rows are not the pattern to copy.** Plenty of live rows across these tables are random hex
|
|
46
|
+
that is *not* v4-shaped (older migrations concatenated `RANDOM_BYTES` without the version/variant
|
|
47
|
+
nibbles). They are fine and must not be "fixed" — but do not use them as the template for new SQL.
|
|
48
|
+
|
|
49
|
+
### ⚠ `DISTINCT` cannot dedupe a row that carries a random uuid
|
|
50
|
+
|
|
51
|
+
This is the silent one. In an `INSERT … SELECT`, putting the uuid expression inside a
|
|
52
|
+
`SELECT DISTINCT` **disables the dedupe entirely** — the per-row random uuid makes every candidate
|
|
53
|
+
row unique, so `DISTINCT` has nothing to collapse and every duplicate id passes through.
|
|
54
|
+
|
|
55
|
+
```sql
|
|
56
|
+
# WRONG — DISTINCT never removes anything; the uuid differs on every row
|
|
57
|
+
INSERT INTO Personas_Items (uuid, personaId, itemId)
|
|
58
|
+
SELECT DISTINCT
|
|
59
|
+
LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
|
|
60
|
+
@personaId,
|
|
61
|
+
bi.itemId
|
|
62
|
+
FROM BundleItems bi
|
|
63
|
+
INNER JOIN Bundles b ON b.id = bi.bundleId
|
|
64
|
+
WHERE b.number = '243';
|
|
65
|
+
|
|
66
|
+
# CORRECT — dedupe the id set in a derived table, generate the uuid in the OUTER select
|
|
67
|
+
INSERT INTO Personas_Items (uuid, personaId, itemId)
|
|
68
|
+
SELECT
|
|
69
|
+
LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
|
|
70
|
+
@personaId,
|
|
71
|
+
src.itemId
|
|
72
|
+
FROM (
|
|
73
|
+
SELECT DISTINCT bi.itemId
|
|
74
|
+
FROM BundleItems bi
|
|
75
|
+
INNER JOIN Bundles b ON b.id = bi.bundleId
|
|
76
|
+
WHERE
|
|
77
|
+
b.number = '243' AND
|
|
78
|
+
bi.isActive = 1 AND
|
|
79
|
+
bi.itemId IS NOT NULL
|
|
80
|
+
) src;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The same shape is the fix for the self-referencing `INSERT … NOT EXISTS` error 1093 (MySQL will not
|
|
84
|
+
let you read the table you are inserting into) — wrap the read in a derived table.
|
|
85
|
+
|
|
86
|
+
### Guarding the insert: know whether the table has a unique key
|
|
87
|
+
|
|
88
|
+
Whether a re-run is loud or silent depends entirely on the target table's indexes, and bridge tables
|
|
89
|
+
in 2.0 are **inconsistent** about this — some have a composite unique key, some do not. Check
|
|
90
|
+
before you write, not after.
|
|
91
|
+
|
|
92
|
+
- **Unique key present** — a re-run errors. Loud, recoverable, but it aborts the rest of the file.
|
|
93
|
+
Add a guard if you want a clean no-op.
|
|
94
|
+
- **No unique key** — a re-run **silently duplicates every row**, and nothing tells you. Always guard
|
|
95
|
+
these.
|
|
96
|
+
|
|
97
|
+
Both guard shapes are in use and equivalent:
|
|
98
|
+
|
|
99
|
+
```sql
|
|
100
|
+
# LEFT JOIN … IS NULL
|
|
101
|
+
FROM Users u
|
|
102
|
+
INNER JOIN Roles r ON r.name = 'SuperUser'
|
|
103
|
+
LEFT JOIN Users_Roles ur ON ur.userId = u.id AND ur.roleId = r.id
|
|
104
|
+
WHERE
|
|
105
|
+
u.email = '…' AND
|
|
106
|
+
ur.id IS NULL;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**A guarded insert that inserts zero rows is a success, not a wasted file** — see
|
|
110
|
+
[Client-DB grant migrations are re-runnable NO-OPs](../../_underscore/features/acl-permission-chain.md).
|
|
111
|
+
|
|
112
|
+
### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
|
|
113
|
+
|
|
114
|
+
The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
|
|
115
|
+
against `@parentId` — means **every phase must run in the same connection**. Reconnecting between
|
|
116
|
+
phases leaves `@parentId` `NULL` and the child inserts either fail on the FK or write `NULL`. Say so
|
|
117
|
+
in the file header, and make the unguarded parent insert the one phase you skip on a re-run (set
|
|
118
|
+
`@parentId` to the existing row by hand instead).
|
|
119
|
+
|
|
120
|
+
## Gotchas
|
|
121
|
+
|
|
122
|
+
- **A "re-run safe" file is usually only re-run safe *per phase*.** If Phase 1 creates the parent row
|
|
123
|
+
with no `NOT EXISTS` guard, re-running the *whole file* creates a **second parent** and then hangs
|
|
124
|
+
a full set of children off it. Re-run safety of the child phases does not make the file idempotent.
|
|
125
|
+
- **`number` columns are strings.** `Bundles.number`, `Personas.number` and friends are varchar, not
|
|
126
|
+
int — quote them (`WHERE b.number = '243'`) and `CAST(… AS UNSIGNED)` when you need numeric sort
|
|
127
|
+
order. An unquoted comparison forces a string→number coercion that will not use the index.
|
|
128
|
+
- **`Client_*` files may not reach into `Core`.** Unrelated to uuids but it bites the same kind of
|
|
129
|
+
file: separate production clusters mean a `Core.` reference is unrunnable in prod even though it
|
|
130
|
+
works locally. Hardcode `Core.Records` / `Core.RecordFields` ids as literals — see
|
|
131
|
+
[dbchanges2 architecture](../architecture.md).
|
|
132
|
+
|
|
133
|
+
## Change history
|
|
134
|
+
- 2026-08-26 — Initial: recorded the inline **UUID4** expression built on MySQL 8 `RANDOM_BYTES()`
|
|
135
|
+
(version/variant nibbles pinned; verified on prod 8.0.39) and the silent
|
|
136
|
+
**`SELECT DISTINCT` + random-uuid** trap that lets duplicate rows through — dedupe the id set in a
|
|
137
|
+
derived table and generate the uuid in the outer select. Added the unique-key/guard rule for bridge
|
|
138
|
+
tables (no unique key = silent duplicates on re-run) and the `LAST_INSERT_ID()` one-session
|
|
139
|
+
constraint. Extracted from the Compass Creative Studio persona grant. (bala)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 67 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 11 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
| [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i | _underscore/Model/Compass/ApprovalDecision.php, worker2/Worker/Client/Compass/ApprovalReassignment.php, worker2/Worker/Client/Compass/PeopleFile.php |
|
|
21
21
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
22
22
|
| [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
|
|
23
|
+
| [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
|
|
23
24
|
| [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
|
|
24
25
|
| [Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)](workflows/odp-edi-import-recovery.md) | 1.0 | How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron **3a** created the ODP SalesOrder header, the follow-up item | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json |
|
|
25
26
|
| [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json, library/app/client/compass.php |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
@@ -15,6 +15,7 @@ related:
|
|
|
15
15
|
- ../profile.md
|
|
16
16
|
- ../workflows/persona-refactor-migration.md
|
|
17
17
|
- ../workflows/persona-population-env-comparison.md
|
|
18
|
+
- ../workflows/granting-persona-bundle-access.md
|
|
18
19
|
- ../../compass-canada/profile.md
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -40,16 +41,25 @@ below.
|
|
|
40
41
|
|
|
41
42
|
| Persona | id | Meaning |
|
|
42
43
|
|---|---|---|
|
|
43
|
-
| Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. |
|
|
44
|
+
| Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. **Still named "All" in prod** — the refactor is not live there (verified 2026-08-26). |
|
|
44
45
|
| Levy | **24** | Levy-sector catalogue. |
|
|
45
|
-
| Non-Levy | **resolved by name** — 38 on dev-sandbox
|
|
46
|
+
| Non-Levy | **resolved by name** — **38 on dev-sandbox only; does not exist in prod** | Everything that is not a printer. Granted to non-Levy users only. |
|
|
46
47
|
| VIP | 35 | VIP add-on. |
|
|
47
48
|
| CDL (MacBook / Surface) | 30 | `@compassdigital.io` + VIP users. |
|
|
48
49
|
|
|
49
|
-
**Never hardcode the Non-Levy id.** `Personas.AUTO_INCREMENT`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
**Never hardcode the Non-Levy id.** On dev-sandbox `Personas.AUTO_INCREMENT` was 38 (max existing id
|
|
51
|
+
37), so the migration's `number = '40'` yielded **id 38**, not 40. The original plan and the (now
|
|
52
|
+
closed) worker2 PR both hardcoded **40** and would have written to the wrong persona.
|
|
53
|
+
|
|
54
|
+
> **⚠ Correction (verified read-only against prod, 2026-08-26): "id 38" was never a prod fact, and is
|
|
55
|
+
> now definitively wrong there.** Prod has **no Non-Levy persona**, and `Personas.id` **38** is
|
|
56
|
+
> **"MyDining- KDS"** — prod's auto-increment moved past 38 after 2026-08-06. Persona **number 40** is
|
|
57
|
+
> taken by that same row. Whenever the migration does run in prod the persona will land on some higher
|
|
58
|
+
> id, which is precisely what name-resolution protects against. Current prod number map and the
|
|
59
|
+
> knock-on effect on the migration's Phase 1:
|
|
60
|
+
> [Persona Refactor](../workflows/persona-refactor-migration.md).
|
|
61
|
+
|
|
62
|
+
`PeopleFile` resolves it at runtime:
|
|
53
63
|
|
|
54
64
|
```php
|
|
55
65
|
const PERSONA_NAME_US_NON_LEVY = 'Non-Levy';
|
|
@@ -128,6 +138,14 @@ VIP (35) and CDL (30) handling is orthogonal and applies to both tenants unchang
|
|
|
128
138
|
constants from this file into tickets, docs, or chat transcripts.
|
|
129
139
|
|
|
130
140
|
## Change history
|
|
141
|
+
- 2026-08-26 — **Correction from a read-only prod check.** The "Non-Levy = id **38** on dev-sandbox
|
|
142
|
+
*and* prod" claim is wrong: prod has **no Non-Levy persona** and id 38 is **"MyDining- KDS"**, so
|
|
143
|
+
prod's auto-increment is past 38 and persona 1 is still named **"All"**. Reinforces resolve-by-name.
|
|
144
|
+
Also linked the new [bundle-grant workflow](../workflows/granting-persona-bundle-access.md), which
|
|
145
|
+
covers the *other* half of the persona model this doc does not: what a persona grant physically
|
|
146
|
+
consists of (`Personas_Bundles` + **`Personas_Items`** + `Personas_Assortments`) and that the
|
|
147
|
+
effective persona set is a UNION of the user's own personas, their location's, and their sector's.
|
|
148
|
+
(bala)
|
|
131
149
|
- 2026-08-13 — Split the `Users` row lifecycle out of this doc into
|
|
132
150
|
[PEOPLE-file User Lifecycle](./people-file-user-lifecycle.md) (duplicate accounts from the 5-day
|
|
133
151
|
lookup window; raw-SQL deactivation that fires no model hook). This doc stays scoped to persona /
|
|
@@ -18,7 +18,7 @@ project: _Underscore
|
|
|
18
18
|
client: compass-usa
|
|
19
19
|
type: profile
|
|
20
20
|
status: active
|
|
21
|
-
updated: 2026-08-
|
|
21
|
+
updated: 2026-08-26
|
|
22
22
|
owners: [jcardinal, bala, tcox, apeterson, dfranks]
|
|
23
23
|
files: []
|
|
24
24
|
related:
|
|
@@ -27,6 +27,7 @@ related:
|
|
|
27
27
|
- features/people-file-user-lifecycle.md
|
|
28
28
|
- workflows/persona-refactor-migration.md
|
|
29
29
|
- workflows/persona-population-env-comparison.md
|
|
30
|
+
- workflows/granting-persona-bundle-access.md
|
|
30
31
|
- features/mits-sales-order-transmission-alerting.md
|
|
31
32
|
- features/asn-to-item-fulfillment.md
|
|
32
33
|
- features/cost-centers.md
|
|
@@ -132,8 +133,17 @@ separate, related client (see its own profile).
|
|
|
132
133
|
users got the full catalogue). Never trust pre-2026-08 Levy persona state as intentional.
|
|
133
134
|
- [Persona Refactor — 8-phase migration & deploy order](workflows/persona-refactor-migration.md)
|
|
134
135
|
(TRUE-75705) — splits persona 1 into Base (printers, everyone) + Non-Levy. **Run on dev-sandbox
|
|
135
|
-
only; production is NOT migrated
|
|
136
|
-
|
|
136
|
+
only; production is NOT migrated** (re-verified 2026-08-26: prod persona 1 is still "All", there is
|
|
137
|
+
no Non-Levy persona, and id 38 is now "MyDining- KDS"). worker2 must deploy first; Phase 8 (ACL
|
|
138
|
+
expression 235) ships with the toga2-commerce hack removal.
|
|
139
|
+
- [Granting a user access to a bundle (persona grant) + SuperUser](workflows/granting-persona-bundle-access.md)
|
|
140
|
+
— the recurring "give user X sight of kit N" request. **Five tables, not one**: a bundle grant
|
|
141
|
+
without `Personas_Items` renders a **half-visible kit**, because item-level ACL hides kit lines
|
|
142
|
+
whose item the persona cannot see. Also: a Compass persona needs **no** settings rows (all nine
|
|
143
|
+
persona-settings tables are empty across all 38 prod personas), "bundle 243" is `Bundles.number`
|
|
144
|
+
not `id`, and **`Users` has no `username` column** — it is `c_hrEmpUsername`, stored lowercase.
|
|
145
|
+
Super user = `Roles.id 5` added **additively** via `Users_Roles`, never replacing Base /
|
|
146
|
+
Compass Base.
|
|
137
147
|
- [Prod ↔ dev-sandbox persona comparison](workflows/persona-population-env-comparison.md) — the
|
|
138
148
|
full-population diff technique, the **join-on-email** rule (`Users.id` means different people per
|
|
139
149
|
environment), and the drift bundles (188/191/242) that mimic a catalogue grant.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Granting a Compass user access to a bundle (persona grant) and the SuperUser role
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: compass-usa
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
13
|
+
related:
|
|
14
|
+
- ../features/persona-model-and-levy-gating.md
|
|
15
|
+
- ./persona-refactor-migration.md
|
|
16
|
+
- ./persona-population-env-comparison.md
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
"Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a
|
|
25
|
+
super user". It looks like a one-row insert into `Personas_Bundles`. It is not — a bundle grant
|
|
26
|
+
without the matching **item** grant renders a **half-visible kit**, because item-level ACL hides any
|
|
27
|
+
kit line whose item the persona cannot see.
|
|
28
|
+
|
|
29
|
+
A complete grant touches **five tables**. `dbchanges2/Client_Compass/2026-08-26a -
|
|
30
|
+
CompassCreativeStudioPersona.sql` is the working template: a 6-phase additive script that creates the
|
|
31
|
+
persona "Compass Creative Studio" (number 41), grants bundle 243, its 19 items and the 6 assortments
|
|
32
|
+
those items live in, assigns the persona to one user, and adds SuperUser.
|
|
33
|
+
|
|
34
|
+
All facts below were verified read-only against **production `Client_Compass` on 2026-08-26**.
|
|
35
|
+
|
|
36
|
+
## The five tables
|
|
37
|
+
|
|
38
|
+
| Table | Grants | Composite unique key? | Optional? |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| `Personas` (`id`, `uuid`, `name`, `number`) | the persona itself | — | no |
|
|
41
|
+
| `Personas_Bundles` (`personaId`, `bundleId`) | the kit appears | **yes** — re-run **errors** | no |
|
|
42
|
+
| `Personas_Items` (`personaId`, `itemId`) | the kit's **lines** are visible | **no** — re-run **silently duplicates** | **no — see below** |
|
|
43
|
+
| `Personas_Assortments` (`personaId`, `assortmentId`) | browsing the accessory categories | **no** — re-run **silently duplicates** | no |
|
|
44
|
+
| `Users_Personas` (`userId`, `personaId`) | the user holds the persona | **yes** — re-run **errors** | no |
|
|
45
|
+
|
|
46
|
+
That asymmetry is the thing to remember: the two bridges that most need a guard are exactly the two
|
|
47
|
+
that will not protect themselves.
|
|
48
|
+
|
|
49
|
+
### `Personas_Items` is not optional
|
|
50
|
+
|
|
51
|
+
Every `BundleItems.itemId` of the bundle must also be granted to the persona. Concretely: of bundle
|
|
52
|
+
243's **19** active line items, persona 1 ("All", which the user already held) covered only **2** —
|
|
53
|
+
so a bundle-only grant would have shown the kit with 17 of its lines missing.
|
|
54
|
+
|
|
55
|
+
### Derive the assortment set — never hardcode it
|
|
56
|
+
|
|
57
|
+
An assortment qualifies when it holds one of the bundle's items **and** the persona that owns the
|
|
58
|
+
bundle today already grants it. Joining it out means the new persona inherits exactly the browse
|
|
59
|
+
scope the current owner has, with no list to maintain:
|
|
60
|
+
|
|
61
|
+
```sql
|
|
62
|
+
SELECT DISTINCT ai.assortmentId
|
|
63
|
+
FROM Bundles b
|
|
64
|
+
INNER JOIN BundleItems bi ON bi.bundleId = b.id AND bi.isActive = 1
|
|
65
|
+
INNER JOIN AssortmentItems ai ON ai.itemId = bi.itemId
|
|
66
|
+
INNER JOIN Personas_Bundles pbSource ON pbSource.bundleId = b.id AND pbSource.personaId <> @personaId
|
|
67
|
+
INNER JOIN Personas_Assortments pa ON pa.assortmentId = ai.assortmentId AND pa.personaId = pbSource.personaId
|
|
68
|
+
WHERE b.number = '243'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `personaId <> @personaId` exclusion matters: by the time this phase runs, the **new** persona
|
|
72
|
+
also owns the bundle, and without it the join would feed on itself.
|
|
73
|
+
|
|
74
|
+
For bundle 243 this resolves to assortments **2** (Monitors), **4** (Computer Accessories), **6**
|
|
75
|
+
(Apple Accessories), **8** (Keyboard/Keypad & Mouse), **10** (Cables) and **19** (Computers).
|
|
76
|
+
**An inactive assortment in the set is normal** — 19 is `isActive = 0` and is still granted, because
|
|
77
|
+
the grant mirrors the source persona rather than judging the data.
|
|
78
|
+
|
|
79
|
+
## A new Compass persona needs NO settings rows
|
|
80
|
+
|
|
81
|
+
Verified across **all 38** personas in prod: `PersonaAppSettings`, `PersonaGlobalSettings`,
|
|
82
|
+
`PersonaPageSettings`, `PersonaRecordSettings`, `PersonaSectionSettings`,
|
|
83
|
+
`PersonaRecordFieldSettings`, `PersonaTranslations`, `Personas_Currencies` and
|
|
84
|
+
`Personas_VendorItems` are **all empty — 0 rows**. A Compass persona is purely **catalogue scoping**
|
|
85
|
+
(bundles / items / assortments). Do not go hunting for supporting configuration; there is none.
|
|
86
|
+
|
|
87
|
+
## Persona numbers in prod (2026-08-26)
|
|
88
|
+
|
|
89
|
+
**38 personas.** Numbers in use: **1-23, 27-40, and 100** ("QA Full Access", id 37). **The next free
|
|
90
|
+
number is 41.** `Personas.number` is a *string* column, so quote it.
|
|
91
|
+
|
|
92
|
+
## "Bundle 243" is `Bundles.number`, not `Bundles.id`
|
|
93
|
+
|
|
94
|
+
The business always quotes the **number**. Bundle **243** is `Bundles.id` **171** ("Compass Creative
|
|
95
|
+
Studio MacBook"). Resolve it in the query (`WHERE b.number = '243'`) rather than pasting an id, and
|
|
96
|
+
never assume the two match.
|
|
97
|
+
|
|
98
|
+
## Finding the user
|
|
99
|
+
|
|
100
|
+
**`Client_Compass.Users` has no `username` column** — the natural first query fails with
|
|
101
|
+
`Unknown column 'username'`. The Compass login / personnel username lives in **`c_hrEmpUsername`**,
|
|
102
|
+
and it is stored **lowercase**: people quote it uppercase ("KINGK01") but the row says `kingk01`.
|
|
103
|
+
Look users up by `email` or `c_hrEmpUsername` (lowercased), which is also how the nightly PEOPLE cron
|
|
104
|
+
matches them — see [PEOPLE-File User Lifecycle](../features/people-file-user-lifecycle.md).
|
|
105
|
+
|
|
106
|
+
## Making someone a super user
|
|
107
|
+
|
|
108
|
+
There is **no `isSuperUser` flag on `Users`** — it is a role row. `Client_Compass.Roles.id` **5**,
|
|
109
|
+
name **`SuperUser`** (113 holders as of 2026-08-26), granted **additively** through
|
|
110
|
+
`Users_Roles (userId, roleId)`, which is unique on the pair.
|
|
111
|
+
|
|
112
|
+
**SuperUser is never a replacement for the base roles.** The established prod pattern for a Compass
|
|
113
|
+
super user is **Base (1) + SuperUser (5) + Compass Base (7)**, optionally **+ Manager (8)**. Role
|
|
114
|
+
permissions resolve as a **union** (see
|
|
115
|
+
[ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)), so adding
|
|
116
|
+
role 5 while removing 1 or 7 takes access *away*.
|
|
117
|
+
|
|
118
|
+
## Verify the way the ACL actually resolves personas
|
|
119
|
+
|
|
120
|
+
A user's **effective** persona set is three UNIONed sources, not just `Users_Personas`:
|
|
121
|
+
|
|
122
|
+
1. `Users_Personas` — personas held directly.
|
|
123
|
+
2. `Locations_Personas` for the user's own `locationId`.
|
|
124
|
+
3. `Locations_Personas` on the user's **sector** — five `parentLocationId` hops up
|
|
125
|
+
(location → complex → district → region → division → sector).
|
|
126
|
+
|
|
127
|
+
**Check all three before adding anything** — the user may already reach the bundle through their
|
|
128
|
+
location, in which case the right answer is "no change needed". After the grant, re-run the same
|
|
129
|
+
resolution and confirm the bundle number appears; the verification block at the end of the template
|
|
130
|
+
file does exactly this. The same three-source resolution is what the full-population comparison
|
|
131
|
+
tooling uses — see
|
|
132
|
+
[Prod vs dev-sandbox persona comparison](./persona-population-env-comparison.md).
|
|
133
|
+
|
|
134
|
+
## Gotchas
|
|
135
|
+
|
|
136
|
+
- **⚠ Re-running the whole file creates a SECOND persona.** Phase 1's `INSERT INTO Personas` has no
|
|
137
|
+
guard, so a second run inserts another persona with the same name and number, and the child phases
|
|
138
|
+
then hang a full set of rows off it. The child phases are individually re-run safe; the **file is
|
|
139
|
+
not idempotent**. To repeat any phase, set `@personaId` to the existing persona id by hand and skip
|
|
140
|
+
Phase 1.
|
|
141
|
+
- **`@personaId` is a session variable — run every phase in ONE session.** A reconnect between phases
|
|
142
|
+
leaves it `NULL`.
|
|
143
|
+
- **`SELECT DISTINCT` will not dedupe a row carrying a generated uuid.** The per-row random value
|
|
144
|
+
makes every row distinct. Dedupe the id set in a derived table and generate the uuid in the outer
|
|
145
|
+
select — full explanation and the UUID4 expression in
|
|
146
|
+
[Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
|
|
147
|
+
- **⚠ Do not write this SQL against the persona-refactor target state.** As of 2026-08-26 the refactor
|
|
148
|
+
has **not** shipped to prod: persona 1 is still named **"All"** (not "Base"), there is **no
|
|
149
|
+
"Non-Levy" persona**, and persona **number 40 belongs to id 38 "MyDining- KDS"**. See
|
|
150
|
+
[Persona Refactor](./persona-refactor-migration.md).
|
|
151
|
+
- **Everything here is additive.** The user keeps persona 1 and their existing roles; nothing is
|
|
152
|
+
removed. A "remove access" request is a different, destructive job and needs its own review.
|
|
153
|
+
|
|
154
|
+
## Change history
|
|
155
|
+
- 2026-08-26 — Initial: documented the five-table bundle grant (and that **`Personas_Items` is
|
|
156
|
+
mandatory** — item-level ACL hides kit lines whose item the persona cannot see; persona 1 covered
|
|
157
|
+
only 2 of bundle 243's 19 items), the unique-key asymmetry that makes `Personas_Items` /
|
|
158
|
+
`Personas_Assortments` duplicate silently on re-run, the **derived** assortment join, and that
|
|
159
|
+
**every persona settings table in prod is empty across all 38 personas** so a new persona needs no
|
|
160
|
+
config. Added the `Bundles.number` vs `Bundles.id` trap, the missing `Users.username` column
|
|
161
|
+
(`c_hrEmpUsername`, lowercase), the SuperUser role recipe (`Roles.id 5`, additive, Base + SuperUser
|
|
162
|
+
+ Compass Base), and the three-source effective-persona verification. Template file:
|
|
163
|
+
`2026-08-26a - CompassCreativeStudioPersona.sql` (written, not executed). (bala)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-06 - PersonaRefactor.sql
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
related:
|
|
17
17
|
- ../features/persona-model-and-levy-gating.md
|
|
18
18
|
- ./persona-population-env-comparison.md
|
|
19
|
+
- ./granting-persona-bundle-access.md
|
|
19
20
|
- ../profile.md
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -29,6 +30,32 @@ Levy-sector users). One SQL file, **8 phases**, plus a companion verification sc
|
|
|
29
30
|
> (toga2-commerce #306, dbchanges2 #368, worker2 #43) were closed and the work rewritten against
|
|
30
31
|
> current code and data — treat any plan or skill text predating 2026-08-06 as stale.
|
|
31
32
|
|
|
33
|
+
### ⚠ Everything below Phase 1 describes a TARGET state. Prod is not in it (re-verified 2026-08-26)
|
|
34
|
+
|
|
35
|
+
The phase table, the persona ids and the post-migration figures in this doc describe what prod will
|
|
36
|
+
look like **after** the migration. Read read-only against production `Client_Compass` on
|
|
37
|
+
**2026-08-26**, prod is still pre-migration:
|
|
38
|
+
|
|
39
|
+
| Claim you might carry over | Production reality, 2026-08-26 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Persona 1 is named "Base" | Persona 1 is still named **"All"** |
|
|
42
|
+
| A "Non-Levy" persona exists | **It does not exist in prod at all** |
|
|
43
|
+
| Non-Levy will land on `Personas.id` **38** | **Id 38 is already taken** — it is **"MyDining- KDS"** |
|
|
44
|
+
| Phase 1's hardcoded `number = '40'` is free | **Number 40 is in use** (by id 38). Numbers **1-23, 27-40 and 100** are taken across **38** personas; **41 is the next free number** |
|
|
45
|
+
|
|
46
|
+
Two consequences before this is ever run in prod:
|
|
47
|
+
|
|
48
|
+
1. **Phase 1's hardcoded `number = '40'` must be re-checked** — it would create a second persona
|
|
49
|
+
carrying a number that already belongs to "MyDining- KDS".
|
|
50
|
+
2. **The "id will be 38" reasoning no longer holds in prod.** `Personas.AUTO_INCREMENT` has moved
|
|
51
|
+
past 38 since 2026-08-06, so the new persona will get some higher id. This is exactly why
|
|
52
|
+
`PeopleFile` resolves Non-Levy **by name** — see
|
|
53
|
+
[Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Never hardcode
|
|
54
|
+
the id, and re-read prod rather than this table before running.
|
|
55
|
+
|
|
56
|
+
**Any other Compass persona work must target prod-as-it-is, not this target state** — see
|
|
57
|
+
[Granting a Compass user access to a bundle](./granting-persona-bundle-access.md).
|
|
58
|
+
|
|
32
59
|
The persona semantics and the Levy-detection code this depends on live in
|
|
33
60
|
[Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Read that first;
|
|
34
61
|
the migration is meaningless without the `isLevySector` fix.
|
|
@@ -130,6 +157,12 @@ All checks are written as **invariants** or as comparisons against a captured **
|
|
|
130
157
|
| Fixed verification user list (HAQIKAH.AARON, KYLE.AARON) | HAQIKAH.AARON is inactive, KYLE.AARON does not exist — **find test users dynamically** |
|
|
131
158
|
|
|
132
159
|
## Change history
|
|
160
|
+
- 2026-08-26 — **Correction, not new work.** Re-verified read-only against prod: the refactor is
|
|
161
|
+
**still not live**. Prod persona 1 is named **"All"** (not "Base"), there is **no Non-Levy
|
|
162
|
+
persona**, and **id 38 is now "MyDining- KDS"** — so the "Non-Levy = id 38" reasoning and Phase 1's
|
|
163
|
+
hardcoded `number = '40'` (number 40 belongs to id 38) are both stale for prod. Prod holds 38
|
|
164
|
+
personas using numbers 1-23, 27-40 and 100; next free number is 41. Added the target-state warning
|
|
165
|
+
block so nobody writes prod SQL against the post-migration table. (bala)
|
|
133
166
|
- 2026-08-06 — Initial: rewritten 8-phase migration (earlier draft's Phase 5+9 collapsed into one
|
|
134
167
|
Levy-excluding assignment; Phase 6 restricted to active users; Phase 7f deletes the retired persona
|
|
135
168
|
by name; Phase 8 makes ACL expression 235 purely additive). Documented the B2 verification-order
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: bdr-landing-call-flow
|
|
4
|
+
title: BDR /landing "Call me now" flow built to the mock, real callback route wired but dormant
|
|
5
|
+
author: tcox
|
|
6
|
+
repos: [bdr]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: shared
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-08-26
|
|
11
|
+
updated: 2026-08-26
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: bdr-landing-call-flow
|
|
15
|
+
**Date:** 2026-08-26
|
|
16
|
+
**Project/Repo:** bdr (2.0)
|
|
17
|
+
**Task:** On the BDR cybersecurity landing page (C:\WWW\BDR, route `/landing`): acted on the design lead's UI review by adding the chat drawer's "Call me now" button and its full flow (CallNow screen -> calling screen) ported from the restored `AI BDR - Security/` mockup, fixed the "Book a meeting" sizing, and wired a REAL anonymous-lead callback route (`POST /api/landing-call`) that stays dormant (503 -> simulation) until a cybersecurity Toga campaign exists and `TOGA_SECURITY_CAMPAIGN_UUID` is set. Also: diagnosed the chat fallback (origin gate), verified the Calendly link is dead, resolved the `_dev-sandbox` merge conflict. Nothing committed in the BDR working tree.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
- **Chat fallback root-caused with browser-shaped curl probes.** From origin `http://localhost:3000` the full talos-chat flow works end-to-end (token minted, real Sonnet reply with suggestions/cta/cited_slides). From `https://bdr.dev.sandbox.togatech.com` the session mint returns **403** (origin not allowlisted). CORS is fine (`access-control-allow-origin: *`, `authorization,content-type`); one 502 on a preflight was transient (3/3 retries -> 200). Recorded in `landing-chat-drawer.md`; short message for the Talos backend owner was drafted for the developer to send.
|
|
23
|
+
- **Calendly link verified dead**: `calendly.com/toga-technology/risk-assessment` AND `calendly.com/toga-technology` both 404; TOGA has no Calendly. Real booking system is **Cal.com team `toga-technology`** (worker2 `[calcom] team_slug`); the only public event type is `discovery-call` ("Healthcare AI Discovery Call"); prod `Client_True.Campaigns` both use `c_meetingType = discovery-call`. No security event type exists. Recorded in `security-landing-page.md`.
|
|
24
|
+
- **`_dev-sandbox` merge of TRUE-80965**: single conflicted file `src/content/security.ts`, 4 hunks, all resolved to the TRUE-80965 side with `perl -0pi -e 's/<<<<<<< HEAD\r?\n.*?=======\r?\n(.*?)>>>>>>> TRUE-80965\r?\n/$1/gs'`; resolved file byte-identical to `MERGE_HEAD`; tsc/lint/367 tests green; staged. Developer commits the merge.
|
|
25
|
+
- **"Book a meeting" sizing fixed**: root cause = the mock's chat actions are `<button>`s (browser default ~13.3px button font) while ours is an `<a>` inheriting the panel body size. `.security-shell .chatactions .btn{ font-size:13.5px; padding:0 16px; min-height:44px }`.
|
|
26
|
+
- **Call flow built to the mock**: `ChatPanel` `onCall` prop + "Call me now" button (above Book, mock order); `SecurityCallNow.tsx` (trust column, business email with live domain hint, masked phone, submit gated on valid email AND viable phone); `SecurityCalling.tsx` (dialing -> Connected + mm:ss -> End call -> Call ended with rotating recap + Copy, Call me again / Book a meeting / Back to overview); `securityCallFlow.ts` pure helpers; `SecurityLanding` became a `landing|call|calling` router via `SwapFade` (outMs 240 / inMs 460 / rise 12 / drop 18, mock app-shell timings); copy in `security.ts` `callNow` + `calling`. Evidence: tsc clean, lint clean, vitest 376/376 at that point, `next build` clean, `/landing` still prerendered static.
|
|
27
|
+
- **Real callback path built, gated**: `POST /api/landing-call` {email, phoneNumber} -> validate -> require env `TOGA_SECURITY_CAMPAIGN_UUID` (else **503 before any upstream call**) -> `togaLeadSink.upsertLead` (name from email via `nameFromEmail`) -> `togaCallbackService.requestCall(uuid,{type:"now"},phone)` (same path as /api/call-now: dialer + fast dial) -> status/share tokens. Client `submitLandingCall()` returns `{live, statusHandle}`; any non-success -> simulation. `SecurityCalling` drives the live path through `useCallStatusViewModel` (real phases, real Vapi recap, no "End call" on live, unavailable holds at dialing). `/api/landing-call` on the /api/call-now limiter bucket + matcher. `.env.example` documents the var. Evidence: tsc clean, lint clean, **vitest 385/385**, `next build` clean (`ƒ /api/landing-call`, `/landing` static).
|
|
28
|
+
- **Mockup restored + eslint ignore restored**: `"AI BDR - Security/**"` back in `globalIgnores`; lint clean.
|
|
29
|
+
- **KB captured** (`/capture`): CREATE `landing-call-flow.md`; UPDATE `security-landing-page.md`, `landing-chat-drawer.md`, `web-funnel-app.md`, `live-call-status.md`. Pushed to `_main`, 417 docs. Earlier manual pushes today/yesterday: 595620a (mockup deleted, now corrected), 0fa110e (origin gate), 0924ef5 (Calendly dead).
|
|
30
|
+
|
|
31
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
32
|
+
- **`perl -0pi -e 's/<<<<<<< HEAD\n.*?=======\n(.*?)>>>>>>> TRUE-80965\n/$1/gs'` (LF-only) matched NOTHING** on `src/content/security.ts` — the file is **CRLF**. Use the `\r?\n` variant above. 12 markers untouched meant nothing was mangled; check `grep -c '^<<<<<<<'` after any automated resolution.
|
|
33
|
+
- **Bash single-quoted heredoc for a long markdown file** (session save) failed: `unexpected EOF while looking for matching `''`. Use the Write tool for multi-line files.
|
|
34
|
+
- **Relative paths after a `cd` in one Bash call**: the shell cwd resets to `c:\WWW` between calls, so `cat src/...` in the next call fails with "No such file". Always use absolute paths (`/c/WWW/BDR/...`).
|
|
35
|
+
- **Using `43fdf2b6-7a5d-4e85-be6b-911f42aa787f` as the Toga dialer campaign** — it is the TALOS CHAT campaign (api.togaiq.com `campaign_registry`), NOT a `Client_True.Campaigns` uuid. The dialer would find nothing. Prod Toga campaigns are only `15d0c02f-501e-e081-2ead-61b78c854223` and `d8f7abe6-8f06-0218-03d1-50db575` (both healthcare).
|
|
36
|
+
- **Reusing the studio `CallNow.tsx` / `Success.tsx` on /landing** — rejected before trying: both read `useCampaign()` (CampaignBundle context) and `useLeadRef()`, neither provided on `/landing`; studio CallNow also has no email field. Purpose-built `SecurityCallNow` / `SecurityCalling` instead.
|
|
37
|
+
- **Pointing `TOGA_SECURITY_CAMPAIGN_UUID` at the healthcare test campaign to exercise the real path** — deliberately NOT done and the developer agreed not to: the healthcare Vapi assistant would pitch the wrong offer to a real phone.
|
|
38
|
+
- **Consuming blox Talos directly** (carried forward): unpublished to npm + React `^18` peers vs BDR React 19.2.4. Do not attempt until blox publishes with React 19 peers.
|
|
39
|
+
- **Glob on `c:\WWW` / BDR root** (carried forward): ripgrep timeout. Use `ls`/`find`/`rg` with explicit paths.
|
|
40
|
+
- **Grep content-mode showing `\*` for CSS `/*`** (carried forward): display artifact, not corruption.
|
|
41
|
+
|
|
42
|
+
## Not tried yet (candidates for next session)
|
|
43
|
+
- **Commit**: the whole working tree (call flow, real route, eslint restore, sizing fix) is uncommitted on top of the `_dev-sandbox` merge (which is staged, uncommitted). Branch naming needs tcox's call.
|
|
44
|
+
- **Send the Alex (apeterson) note** — drafted (in the conversation and summarized in `landing-chat-drawer.md`), not sent: adapter matches his `TalosAdapter` seam, blockers (unpublished, React 18 peers), design divergence (Call me now / Book a meeting actions, cobalt theme).
|
|
45
|
+
- **Send the backend-owner note** — drafted: allowlist `https://bdr.dev.sandbox.togatech.com` (later the prod domain) for talos-chat; campaign `43fdf2b6…`.
|
|
46
|
+
- **Enable real calls** when a cybersecurity campaign + Vapi assistant exist in Toga (runbook `ai-bdr/docs/client-onboarding-sop.md`): set `TOGA_SECURITY_CAMPAIGN_UUID` in `.env.local` + Amplify console, redeploy that version. First live run must verify: Toga accepts an EMPTY `lastName` for email-only leads (e.g. `jdoe@` -> firstName "Jdoe", lastName ""); deployed env has `TOGA_VAPI_TOKEN` (else the screen holds at dialing with the unavailable note).
|
|
47
|
+
- **Booking link**: create a Cal.com team event type for the security offer (`https://cal.com/team/toga-technology/<slug>`), then set `security.ts` `primaryCtaHref` (one line; drives page CTA, drawer Book, calling-screen Book) and pass the same URL as `cta_url` on the talos campaign ingest so the chat's `book_call` cta resolves (currently `cta: null`). Interim option: `discovery-call` (title mismatch "Healthcare AI").
|
|
48
|
+
- Render the chat reply's `cta` button and `cited_slides` thumbnails (needs `GET /api/ai/campaign/{id}`; presigned URLs expire ~7 days).
|
|
49
|
+
- Visual QA of the call screens at phone widths; `SecurityCalling` at <=840px relies on studio.css `.ended-cta` stacking.
|
|
50
|
+
- Optional visible "assistant unavailable" notice in the drawer when a chat reply is `offline: true` (currently silent).
|
|
51
|
+
- KB hygiene (unrelated): 20+ docs end with stray `</content>`/`</invoke>` lines (`grep -rln "^</invoke>$" knowledge/`); `validate` does not catch it.
|
|
52
|
+
|
|
53
|
+
## Current file state
|
|
54
|
+
| File | Status | Notes |
|
|
55
|
+
|------|--------|-------|
|
|
56
|
+
| src/components/ChatPanel.tsx | modified | `onCall` prop; "Call me now" `<button>` above "Book a meeting" `<a>` |
|
|
57
|
+
| src/flow/screens/SecurityCallNow.tsx | created | Mock CallNow port: email + phone, domain hint, trust list; submit -> `submitLandingCall` -> `onConnect(detail, result)`; never blocks on backend |
|
|
58
|
+
| src/flow/screens/SecurityCalling.tsx | created | Mock Success (calling) port with live/sim split via `useCallStatusViewModel`; `SecurityCallSummary` renders real Vapi recap or rotating static variant + Copy |
|
|
59
|
+
| src/flow/screens/SecurityLanding.tsx | modified | `screen` state `landing\|call\|calling`, `detail`, `callResult`; SwapFade router; `backToOverview` resets recap rotation + scrollTo(0,0); `agentName` derived from `talos.title` |
|
|
60
|
+
| src/lib/securityCallFlow.ts | created | `fillTokens`, `emailDomain`, `isCallFormComplete`, `formatCallTimer`, `nextSummaryVariantIndex`, `resetSummaryRotation` |
|
|
61
|
+
| src/lib/leadName.ts | created | `nameFromEmail` (local part split on `._-+`, digits stripped, empty lastName allowed) |
|
|
62
|
+
| src/flow/landingCallApi.ts | created | `submitLandingCall(email, phone, fetchImpl?)` -> `{live, statusHandle}`; 20s timeout; logs status only |
|
|
63
|
+
| src/app/api/landing-call/route.ts | created | Anonymous-lead callback route, 503 when `TOGA_SECURITY_CAMPAIGN_UUID` unset/non-uuid; mirrors /api/call-now token minting |
|
|
64
|
+
| src/proxy.ts | modified | `/api/landing-call` on the call limiter branch + matcher |
|
|
65
|
+
| .env.example | modified | `TOGA_SECURITY_CAMPAIGN_UUID` documented (must be the CYBERSECURITY Toga campaign, not the chat campaign id) |
|
|
66
|
+
| src/content/security.ts | modified (+ dev's tabs/"Ask Alex Anything") | `talos.callCta`; new `callNow` + `calling` groups (incl. `calling.live` strings); merge-resolved to TRUE-80965 side |
|
|
67
|
+
| src/app/security.css | modified | chat action sizing fix; `.connect-intro*` / `.connect-trust` / `.agent-caption` / `.field-hint` ported; `.ended-cta a.btn` underline reset |
|
|
68
|
+
| eslint.config.mjs | modified | `"AI BDR - Security/**"` ignore RESTORED (mockup back) |
|
|
69
|
+
| AI BDR - Security/ (folder) | restored by dev | design reference, in repo again |
|
|
70
|
+
| test/securityCallFlow.test.ts | created | 9 tests |
|
|
71
|
+
| test/landingCallRoute.test.ts | created | 9 tests (400s, 503 pre-upstream, 500 without echoing input, nameFromEmail) |
|
|
72
|
+
| next-env.d.ts | modified (generated) | churn from tsc/build; discard if unwanted |
|
|
73
|
+
|
|
74
|
+
All BDR changes uncommitted; the `_dev-sandbox` merge is staged and awaiting `git commit`.
|
|
75
|
+
|
|
76
|
+
## Decisions made
|
|
77
|
+
- **"Match the mock" = match the UI; wire everything that CAN be wired** (developer clarification). Result: real callback route built but dormant; simulation only as fallback.
|
|
78
|
+
- **Keep the real route in the repo, dormant, until a cybersecurity Toga campaign exists** (developer: "as long as it's wired up and ready for the campaign it's good"). Rejected: removing it (rebuild later); testing against the healthcare campaign.
|
|
79
|
+
- **Gate = env var `TOGA_SECURITY_CAMPAIGN_UUID`, 503 before any upstream call when unset** — enabling is config-only; the client treats any non-success as "play the simulation".
|
|
80
|
+
- **Purpose-built `SecurityCallNow`/`SecurityCalling` instead of reusing studio screens** — studio screens need bundle/lead contexts `/landing` lacks; the mock's CallNow differs (email field, trust column).
|
|
81
|
+
- **Live path never fakes progress**: no "End call" on a real call; unavailable status holds at dialing with a note; real Vapi recap replaces the static one; static recap only in simulation.
|
|
82
|
+
- **Anonymous lead name derived from the email local part** (`nameFromEmail`), lastName may be empty — flagged unverified. Rejected: adding a name field (not in the mock); inventing a placeholder surname (pollutes CRM).
|
|
83
|
+
- **Mock deviations kept**: no AmbientQuestions (bundle-driven); recap Copy without Share sheet; "Schedule a call" -> "Book a meeting" (Calendly) since no schedule screen; footnote Privacy/Terms = real togatech.com links.
|
|
84
|
+
- **Book-a-meeting sizing**: pin font-size/padding on `.chatactions .btn` rather than converting the anchor to a button (it must remain a link to open Calendly).
|
|
85
|
+
- **TRUE-80692 alignment: keep the disposable bridge shell**; send Alex a facts-first note (drafted). Rejected: pausing drawer work until blox publishes.
|
|
86
|
+
- **KB filing stays under `2.0/apps/ai-bdr/`**; mockup deletion history kept honest (superseded, not erased).
|
|
87
|
+
|
|
88
|
+
## Blockers
|
|
89
|
+
None hard. External dependencies: (1) a cybersecurity campaign + Vapi assistant in Toga (owner outside bdr) before real calls can be enabled; (2) Talos backend owner to allowlist the sandbox/prod origins for talos-chat; (3) a Cal.com event type for the security offer (Cal.com owner) before the booking CTA works; (4) apeterson/design alignment on the shared component; (5) tcox's branch/commit go-ahead.
|
|
90
|
+
|
|
91
|
+
## Exact next step
|
|
92
|
+
> In `C:\WWW\BDR`, decide the branch, then commit the staged `_dev-sandbox` merge and the working-tree call-flow work (`git add -A` — includes the restored `AI BDR - Security/` mockup and eslint ignore). Then send the two drafted notes (Talos backend owner: allowlist `https://bdr.dev.sandbox.togatech.com`; apeterson: shared-component alignment). Real calls stay off until you have the cybersecurity campaign UUID for `TOGA_SECURITY_CAMPAIGN_UUID`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
_Saved by /session-save on 2026-08-26_
|
package/package.json
CHANGED