toga-ai 1.0.355 → 1.0.357
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/ai-bdr/features/bdr-web-funnel-plan.md +48 -23
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-app.md +87 -15
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-content-model.md +69 -58
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/profile.md +8 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/approval-decision-flow.md +100 -0
- package/knowledge/clients/compass-usa/profile.md +6 -1
- package/package.json +1 -1
|
@@ -19,11 +19,15 @@ related:
|
|
|
19
19
|
- web-funnel-app.md
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
> **Execution status (2026-07-
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
>
|
|
26
|
-
>
|
|
22
|
+
> **Execution status (2026-07-16):** PLAN **Phases 0–5 are BUILT and QA'd**, Phase 6
|
|
23
|
+
> was **mostly already built** (the `info` contract was implemented at commit `34a3910`),
|
|
24
|
+
> and the 2026-07-16 session added phone prefill + mask, `submitOk` on Success, and GA4.
|
|
25
|
+
> **§9.10 is RESOLVED by dissolution** (HubSpot holds no marketing content — content is
|
|
26
|
+
> in-repo; see `web-funnel-content-model.md`) and **§9.11 is RESOLVED** (consent links →
|
|
27
|
+
> togatech.com pages). Remaining work is decision-gated, not code (leadership copy, Phase 8
|
|
28
|
+
> deploy). The built codebase is documented in `web-funnel-app.md`; this doc stays a plan
|
|
29
|
+
> mirror. This body still mirrors `BDR/PLAN.md` (which got a §5.2 superseded banner + §9.2/
|
|
30
|
+
> §9.10 re-resolution on 2026-07-16).
|
|
27
31
|
|
|
28
32
|
# BDR: Multi-Campaign "AI BDR / Agent Studio" Web App — Implementation Plan
|
|
29
33
|
|
|
@@ -238,6 +242,13 @@ Port approach:
|
|
|
238
242
|
> `NEXT_PUBLIC_` env.
|
|
239
243
|
|
|
240
244
|
### 5.2 Content provider — HubSpot is the source of truth
|
|
245
|
+
> **SUPERSEDED (2026-07-16).** This section's premise — HubSpot as the content source —
|
|
246
|
+
> was **dissolved** by the HubSpot instance owner (Rohan): HubSpot holds no marketing
|
|
247
|
+
> content for this product. Content lives **in the repo** (`src/content/default.ts`); the
|
|
248
|
+
> static `src/content/provider.ts` is permanent; the `hubspotProvider.ts` stub was deleted.
|
|
249
|
+
> Read §9.2 / §9.10 (resolved) and `web-funnel-content-model.md` for the current model. The
|
|
250
|
+
> text below is kept for historical context only.
|
|
251
|
+
|
|
241
252
|
**Decision:** the content store is **HubSpot** (chosen over Contentful). The site's content shape is
|
|
242
253
|
stable / not frequently restructured, so HubSpot's flatter modeling is acceptable, and a dedicated
|
|
243
254
|
headless CMS (Contentful) would only pay off if the content model were rich and frequently
|
|
@@ -490,9 +501,12 @@ so fidelity should be near-exact.
|
|
|
490
501
|
1. **Backend relationship:** RESOLVED — `bdr` is the new UI / front door of TOGA's existing **AI-BDR**
|
|
491
502
|
product (not standalone); it reuses that system's backend calls, and its knowledge is filed under
|
|
492
503
|
`2.0/apps/ai-bdr/`. `CallbackService` uses that system's callback path.
|
|
493
|
-
2. **Content store:** RESOLVED — **
|
|
494
|
-
|
|
495
|
-
|
|
504
|
+
2. **Content store:** RE-RESOLVED (2026-07-16) — **content lives in the repo**
|
|
505
|
+
(`src/content/default.ts`), **not HubSpot.** The earlier "HubSpot-owned content" decision
|
|
506
|
+
was based on a misreading of how the team uses HubSpot; the HubSpot owner (Rohan) confirmed
|
|
507
|
+
HubSpot holds no marketing content for this product. The static `provider.ts` is permanent
|
|
508
|
+
(the `CampaignContentProvider` seam is kept only for a hypothetical future remote source).
|
|
509
|
+
See §9.10 and `web-funnel-content-model.md`.
|
|
496
510
|
3. **Agent choice:** Stay with a campaign-fixed agent (auto-build to a preset), or ship the manual
|
|
497
511
|
Creator (currently parked / "coming soon")? Determines whether `Creator` + full slot art ship now.
|
|
498
512
|
4. **Service picker:** Is the `Landing` service-selection screen (AI BDR + "Talos coming soon") in
|
|
@@ -506,21 +520,24 @@ so fidelity should be near-exact.
|
|
|
506
520
|
9. **Backend hosting:** RESOLVED by the Next.js choice — the secret-bearing HubSpot/Toga calls run in
|
|
507
521
|
BDR's own server components / route handlers; no separate backend to host. Only remaining item is
|
|
508
522
|
confirming the Amplify **SSR** hosting target (not static) at deploy.
|
|
509
|
-
10. **HubSpot content mechanism
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
10. **HubSpot content mechanism — RESOLVED by dissolution (2026-07-16, decision by Rohan,
|
|
524
|
+
the HubSpot instance owner).** There is no HubSpot content mechanism because **HubSpot
|
|
525
|
+
holds no marketing content** for this product and never will — it is **contact-lookup
|
|
526
|
+
only** (the `?hsContactId=` fetch, exactly as the old `info` site). Campaign records are
|
|
527
|
+
added **manually to the Toga database** and reach the app only as the `?hsCampaignId=`
|
|
528
|
+
attribution UUID on entry URLs (same URL contract as `info`). Consequences: campaign
|
|
529
|
+
content lives **in-repo** (`src/content/default.ts` is the authoring surface; every slug
|
|
530
|
+
renders `DEFAULT`); the throwing `hubspotProvider.ts` was **deleted**; `provider.ts`'s
|
|
531
|
+
static provider is the **permanent** implementation; the `CampaignContentProvider` seam is
|
|
532
|
+
retained for a hypothetical future remote source. The prior "HubSpot-owned content"
|
|
533
|
+
assumption (HubDB / custom object / campaign properties) is **moot.**
|
|
534
|
+
11. **Privacy Policy & Terms links (§9.11) — RESOLVED (2026-07-16).** The consent footnotes
|
|
535
|
+
on Call Now / Schedule now link to the existing **togatech.com** pages
|
|
536
|
+
(`https://togatech.com/privacy-policy`, `.../terms-and-conditions`) — decision was to link
|
|
537
|
+
the existing pages, not build in-app ones. `RichText.tsx` `MarkupText` renders real
|
|
538
|
+
`http(s)` hrefs as live anchors (`target=_blank`, `rel="noopener noreferrer"`, aria-label
|
|
539
|
+
"... (opens in new tab)"); bare `#hash` hrefs keep the mockup's inert `preventDefault`. A
|
|
540
|
+
regression test guards against dead anchors returning.
|
|
524
541
|
12. **Share button is a mock (§9.12).** The Success summary-card Share popover validates
|
|
525
542
|
the email and shows "Sent" but **sends nothing** (the Copy button IS a real clipboard
|
|
526
543
|
write). A functional version needs an **email-send route** AND the **real call
|
|
@@ -573,6 +590,14 @@ so fidelity should be near-exact.
|
|
|
573
590
|
- [ ] Pixel-perfect parity pass against the mockup screens + screenshots.
|
|
574
591
|
|
|
575
592
|
## Change history
|
|
593
|
+
- 2026-07-16 — §9 resolutions mirrored from `BDR/PLAN.md`. §9.10 RESOLVED by dissolution
|
|
594
|
+
(decision by Rohan, HubSpot instance owner): HubSpot holds no marketing content — content
|
|
595
|
+
is in-repo (`default.ts`), `hubspotProvider.ts` deleted, static `provider.ts` permanent;
|
|
596
|
+
§9.2 re-resolved accordingly (content store is the repo, not HubSpot) and §5.2 marked
|
|
597
|
+
SUPERSEDED. §9.11 RESOLVED: consent links → togatech.com privacy/terms pages via live
|
|
598
|
+
`RichText` anchors. Execution-status banner updated: Phase 6 was mostly already built
|
|
599
|
+
(commit `34a3910`); session added phone prefill + mask, `submitOk` on Success, GA4. Built
|
|
600
|
+
detail in `web-funnel-app.md`; content model in `web-funnel-content-model.md`. (tcox)
|
|
576
601
|
- 2026-07-15 — First UI design review: appended three future items to §9 open questions,
|
|
577
602
|
mirroring `BDR/PLAN.md` §9.11–§9.13. §9.11 real Privacy/Terms pages (consent footnotes
|
|
578
603
|
currently link to dead `#privacy`/`#terms` anchors; 2-line content fix). §9.12 Share
|
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-16
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
@@ -15,9 +15,14 @@ files:
|
|
|
15
15
|
- bdr/amplify.yml
|
|
16
16
|
- bdr/src/app
|
|
17
17
|
- bdr/src/app/layout.tsx
|
|
18
|
+
- bdr/src/app/page.tsx
|
|
18
19
|
- bdr/src/app/studio.css
|
|
19
20
|
- bdr/src/lib/resolveCampaignId.ts
|
|
20
21
|
- bdr/src/lib/scheduleSlots.ts
|
|
22
|
+
- bdr/src/lib/formatPhone.ts
|
|
23
|
+
- bdr/src/lib/analytics.ts
|
|
24
|
+
- bdr/src/components/GoogleAnalytics.tsx
|
|
25
|
+
- bdr/src/components/RichText.tsx
|
|
21
26
|
- bdr/src/content
|
|
22
27
|
- bdr/src/components
|
|
23
28
|
- bdr/src/flow
|
|
@@ -41,11 +46,15 @@ plan lives in `bdr-web-funnel-plan.md`; the config-driven content contract lives
|
|
|
41
46
|
stack, the directory map, what was ported vs. parked, the test suite, and the
|
|
42
47
|
decisions that are load-bearing for anyone touching the repo.**
|
|
43
48
|
|
|
44
|
-
As of this writing **PLAN Phases 0–5 are BUILT and QA'd
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
As of this writing **PLAN Phases 0–5 are BUILT and QA'd**, and the Phase 6 backend
|
|
50
|
+
work turned out to be **mostly already built** (the `info` URL/backend contract was
|
|
51
|
+
fully implemented at commit `34a3910`: `page.tsx primeLead` reads `?hsContactId=` +
|
|
52
|
+
`?hsCampaignId=` → server-side HubSpot `getContact` → Toga `upsertContactByEmail` with
|
|
53
|
+
campaign attribution → only `togaUuid` crosses to the client). The 2026-07-16 session
|
|
54
|
+
added the deltas below (phone prefill + live input mask, `submitOk` on Success, GA4
|
|
55
|
+
wiring) and resolved §9.10 (content mechanism — see `web-funnel-content-model.md`) and
|
|
56
|
+
§9.11 (consent links). Developer QA verdict: the UI is "very close, minimal adjustments
|
|
57
|
+
to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `next build`.
|
|
49
58
|
|
|
50
59
|
- **Repo:** `agilantsolutions/BDR` on GitHub, working branch `BDR-Development`. The
|
|
51
60
|
machine-local `.claude/` harness is **gitignored** — committing it would snapshot
|
|
@@ -66,7 +75,8 @@ is **decision-gated, not code** (see "What is parked / not built").
|
|
|
66
75
|
- **ESLint flat config** carries a **no-em-dash rule** on shipped strings
|
|
67
76
|
(`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
|
|
68
77
|
rule on in-repo strings (`eslint.config.mjs`).
|
|
69
|
-
- **Vitest** test suite: **
|
|
78
|
+
- **Vitest** test suite: **79 tests, all green** (13 added in the 2026-07-16 session:
|
|
79
|
+
phone format/prefill/mask, live-anchor consent links, `submitOk` race). Every phase was
|
|
70
80
|
verified with `tsc` + lint + build + a live SSR `curl` before commit.
|
|
71
81
|
- **Deploy target:** AWS Amplify (SSR), `amplify.yml` present (Phase 8 not done).
|
|
72
82
|
|
|
@@ -116,24 +126,70 @@ is **decision-gated, not code** (see "What is parked / not built").
|
|
|
116
126
|
(`src/server/toga.ts`) converts the picked slot to `America/Chicago` for Toga. If
|
|
117
127
|
`Intl` is unavailable the note simply is not rendered (graceful, no crash). A design
|
|
118
128
|
request to "make tz calculated" was already satisfied — do not re-implement.
|
|
129
|
+
- **Phone prefill (known leads).** `page.tsx` primeLead now also carries the contact's
|
|
130
|
+
phone: `ClientLeadRef` gained a `phone` field — the **one deliberate PII exception**
|
|
131
|
+
that crosses to the client (the visitor's own number, shown back to its owner, never
|
|
132
|
+
logged). `prefillPhone()` (`src/lib/formatPhone.ts`) normalizes CRM values, drops a US
|
|
133
|
+
`1` prefix, and returns `""` for junk / over-long / non-`+1`-country values (a `+45`
|
|
134
|
+
number next to the fixed `+1` country-code field would dial the wrong country). CallNow
|
|
135
|
+
+ Schedule init their number fields from it. (The old `info` site exposed name + email +
|
|
136
|
+
phone the same way; BDR reduces the client-crossing PII to **phone only**.)
|
|
137
|
+
- **Live phone input mask.** `formatPhoneInput()` (`src/lib/formatPhone.ts`) renders a
|
|
138
|
+
progressive `(555) 012-3456` as the user types: hard 10-digit cap (`maxLength=14` on the
|
|
139
|
+
inputs), strips non-digits on paste, idempotent, and backspace-safe by design (formatting
|
|
140
|
+
is always re-derived from the digits, with no trailing punctuation after the last digit,
|
|
141
|
+
so deleting a `)` or `-` never fights the formatter). Matches the bundle placeholder.
|
|
142
|
+
- **`submitOk` surfaced on Success (known leads only).** When a KNOWN lead's
|
|
143
|
+
fire-and-forget backend submit resolves `false`, a **pre-mounted** `role="status"` live
|
|
144
|
+
region (mounted empty so screen readers reliably announce the change) shows
|
|
145
|
+
`success.submitFailed.{calling,scheduled}`. Anonymous visits always pass `null` and never
|
|
146
|
+
see it. A submission-generation counter (a ref in `useAgentFlow`) prevents a stale,
|
|
147
|
+
late-resolving submit from an abandoned attempt overwriting the current result.
|
|
148
|
+
- **GA4 analytics (`react-ga4`).** `src/lib/analytics.ts` + `src/components/GoogleAnalytics.tsx`
|
|
149
|
+
(mounted in `layout.tsx`), env-driven via `NEXT_PUBLIC_GA_MEASUREMENT_ID` — a silent
|
|
150
|
+
no-op when unset (dev-only console warning). Typed event map: `screen_viewed`,
|
|
151
|
+
`build_started`, `early_access_opened`, `call_now_submitted{success}`,
|
|
152
|
+
`call_later_submitted{success}`. No PII in any param. See the page_location leak gotcha.
|
|
153
|
+
- **Consent footnote links are live (§9.11 resolved).** They point at
|
|
154
|
+
`https://togatech.com/privacy-policy` and `.../terms-and-conditions` (decision: link the
|
|
155
|
+
existing togatech pages, not in-app pages). `RichText.tsx` `MarkupText` renders real
|
|
156
|
+
`http(s)` hrefs as live anchors (`target=_blank`, `rel="noopener noreferrer"`, aria-label
|
|
157
|
+
"... (opens in new tab)"); bare `#hash` hrefs keep the mockup's inert `preventDefault`.
|
|
119
158
|
|
|
120
159
|
## What is parked / not built
|
|
121
160
|
|
|
122
161
|
- **Creator screen + `TweaksPanel` — deliberately NOT ported** (parked per plan
|
|
123
162
|
§9.3 / §9.7).
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
- **Content is in-repo, not HubSpot (§9.10 RESOLVED by dissolution, 2026-07-16).** The
|
|
164
|
+
throwing `hubspotProvider.ts` was **DELETED**; `src/content/provider.ts`'s static
|
|
165
|
+
provider is the **permanent** implementation, and `DEFAULT_BUNDLE` carries the real
|
|
166
|
+
shipped copy (not interim). HubSpot is contact-lookup only; campaign records live in Toga.
|
|
167
|
+
Full detail in `web-funnel-content-model.md`. Do not reintroduce a remote content source.
|
|
128
168
|
- **`info`'s dead 501 `visit` route was dropped** on purpose (attack surface, no
|
|
129
169
|
caller).
|
|
130
|
-
- **Remaining work is decision-gated, not code:**
|
|
131
|
-
(
|
|
132
|
-
|
|
133
|
-
`togatech` integration.
|
|
170
|
+
- **Remaining work is decision-gated, not code:** leadership's first-campaign copy;
|
|
171
|
+
Phase 8 Amplify SSR (incl. a **per-IP WAF rate rule** — see the enumeration gotcha) +
|
|
172
|
+
`togatech` integration. GA4 (Phase 7) is now wired; §9.10 and §9.11 are resolved.
|
|
134
173
|
|
|
135
174
|
## Gotchas
|
|
136
175
|
|
|
176
|
+
- **CRITICAL — GA4 leaks the entry identifiers unless you scrub `page_location`.** `gtag`
|
|
177
|
+
stamps `page_location = window.location.href` (which **includes** `?hsContactId=` &
|
|
178
|
+
`?hsCampaignId=`) on the auto `page_view` **and on every subsequent event** — so the
|
|
179
|
+
entry identifiers would ship to Google on every hit. Mitigation (in place): initialize
|
|
180
|
+
with `gtagOptions {send_page_view:false}` and pin a **query-scrubbed** `page_location`
|
|
181
|
+
via `ReactGA.set` before each pageview. Never send raw `location.href` to analytics here.
|
|
182
|
+
- **HIGH / OPEN (security) — `?hsContactId=` is an enumerable capability token.** The entry
|
|
183
|
+
page is public and HubSpot contact ids are small integers; a bot walking ids can (a)
|
|
184
|
+
scrape each contact's phone from the server-rendered prefill, and (b) drive the page-prime
|
|
185
|
+
Toga upsert, polluting contacts/attribution. **Inherited from `info`'s CRM-link URL
|
|
186
|
+
contract — cannot be changed unilaterally.** Mitigation path before public launch: a
|
|
187
|
+
**per-IP WAF rate rule** on the Amplify distribution (Phase 8), and raise **opaque/signed
|
|
188
|
+
link tokens** with the CRM owner. Tracked in `BDR/PLAN.md` §10.
|
|
189
|
+
- **Standing rule: never add more PII fields to `ClientLeadRef`.** Phone is the one
|
|
190
|
+
deliberate exception (visitor's own number, shown back to them, never logged). Every field
|
|
191
|
+
on `ClientLeadRef` crosses to the public client and is exposed to the enumeration risk
|
|
192
|
+
above — do not widen it.
|
|
137
193
|
- **The mockup's `mockup/styles.css` contains literal `\n` and `\"` escape sequences**
|
|
138
194
|
(JS-string generation artifacts, around lines ~759 / ~840) that break real CSS
|
|
139
195
|
parsers. This was **fixed in the ported `src/app/studio.css`**, but
|
|
@@ -162,6 +218,22 @@ is **decision-gated, not code** (see "What is parked / not built").
|
|
|
162
218
|
harness; pull it with `npx toga-ai`.
|
|
163
219
|
|
|
164
220
|
## Change history
|
|
221
|
+
- 2026-07-16 — Phase 6 delta + §9.10/§9.11 resolutions. DISCOVERED the `info` URL/backend
|
|
222
|
+
contract was already fully built at commit `34a3910` (primeLead: `?hsContactId=` +
|
|
223
|
+
`?hsCampaignId=` → HubSpot `getContact` → Toga upsert + attribution → only `togaUuid` to
|
|
224
|
+
client). BUILT: phone prefill (`ClientLeadRef.phone`, the one deliberate client-crossing
|
|
225
|
+
PII field; `prefillPhone()` normalizes CRM values, drops US `1`, rejects non-`+1`-country
|
|
226
|
+
numbers); live phone input mask (`formatPhoneInput()`, 10-digit cap, paste-safe,
|
|
227
|
+
backspace-safe); `submitOk` surfaced on Success via a pre-mounted `role="status"` live
|
|
228
|
+
region for KNOWN leads, guarded by a submission-generation counter against stale late
|
|
229
|
+
resolves; GA4 via `react-ga4` (env-driven, silent no-op when unset). RESOLVED §9.10 (content
|
|
230
|
+
is in-repo, `hubspotProvider.ts` DELETED, static `provider.ts` permanent — see
|
|
231
|
+
`web-funnel-content-model.md`) and §9.11 (consent links now live togatech.com anchors via
|
|
232
|
+
`RichText`). Added CRITICAL GA4 `page_location` leak gotcha (scrub `?hsContactId=`/
|
|
233
|
+
`?hsCampaignId=` before every hit), HIGH/OPEN `?hsContactId=` enumeration gotcha (WAF rate
|
|
234
|
+
rule + signed tokens before launch), and the "never widen `ClientLeadRef` PII" standing
|
|
235
|
+
rule. 13 new tests (79 total); tsc/eslint/build all green. Work uncommitted at capture
|
|
236
|
+
time (branch `BDR-Phase-2`). (tcox)
|
|
165
237
|
- 2026-07-15 — First UI design review pass. FIXED: site-wide font silently fell back to
|
|
166
238
|
`system-ui` (mockup's `<head>` Google-Fonts link was never ported) — now Plus Jakarta
|
|
167
239
|
Sans via `next/font/google` in `layout.tsx`, exposed as `--font-jakarta`, resolved
|
|
@@ -5,12 +5,12 @@ repo: ai-bdr
|
|
|
5
5
|
project: AI-BDR
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
|
-
status:
|
|
9
|
-
updated: 2026-07-
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-16
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/content/schema.ts
|
|
13
|
-
- bdr/src/content/
|
|
13
|
+
- bdr/src/content/provider.ts
|
|
14
14
|
- bdr/src/content/useCampaign.ts
|
|
15
15
|
- bdr/src/content/default.ts
|
|
16
16
|
- bdr/src/lib/resolveCampaignId.ts
|
|
@@ -25,11 +25,14 @@ related:
|
|
|
25
25
|
- web-funnel-app.md
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
> **Status note:** the content **contract** below (schema,
|
|
29
|
-
> is
|
|
30
|
-
> how
|
|
31
|
-
>
|
|
32
|
-
>
|
|
28
|
+
> **Status note (RESOLVED 2026-07-16):** the content **contract** below (schema,
|
|
29
|
+
> provider seam, adapters) is BUILT and QA'd (PLAN Phases 0–5). The last open item —
|
|
30
|
+
> *how* campaign content is sourced (§9.10) — is now **resolved by dissolution**
|
|
31
|
+
> (decision by Rohan, the HubSpot instance owner): **HubSpot never holds marketing
|
|
32
|
+
> content for this product.** Campaign content lives **in the repo** (`src/content/default.ts`
|
|
33
|
+
> is the authoring surface); the static `src/content/provider.ts` is the **permanent**
|
|
34
|
+
> implementation and the throwing `hubspotProvider.ts` stub was **deleted**. HubSpot is
|
|
35
|
+
> contact-lookup only (see "How campaigns actually work" below). This doc is now `active`.
|
|
33
36
|
|
|
34
37
|
## What this is
|
|
35
38
|
|
|
@@ -55,41 +58,42 @@ routes through `var(--accent)` + derived `--accent-fill`; never hardcode an acce
|
|
|
55
58
|
|
|
56
59
|
Full implementation plan: `BDR/PLAN.md`.
|
|
57
60
|
|
|
58
|
-
##
|
|
61
|
+
## How campaigns actually work (content in-repo; HubSpot is contact-lookup only)
|
|
62
|
+
|
|
63
|
+
> **This section supersedes the earlier "HubSpot-owned content" model.** The prior plan
|
|
64
|
+
> assumption — that marketing would author campaign content in HubSpot and the app would
|
|
65
|
+
> fetch it by slug — was a **misreading** of how the team uses HubSpot. The HubSpot
|
|
66
|
+
> instance owner (Rohan) confirmed on 2026-07-16 that **HubSpot holds no marketing content
|
|
67
|
+
> and never will** for this product. §9.10 is resolved by dissolution.
|
|
59
68
|
|
|
60
69
|
Every campaign is a **serializable data bundle** (`CampaignBundle`) keyed by a stable
|
|
61
|
-
`slug`. **Campaign content is authored
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
purpose-built content CMS, so richly nested content and non-dev editor UX are weaker than
|
|
75
|
-
Contentful — acceptable here because the content shape is **stable / not frequently
|
|
76
|
-
restructured** (a headless CMS would only pay off if the model were rich and churning).
|
|
70
|
+
`slug`. **Campaign content is authored in the repo** — `src/content/default.ts` is the
|
|
71
|
+
authoring surface, and **every slug renders `DEFAULT`.** There is no remote content
|
|
72
|
+
source. What HubSpot *does* provide is unchanged from the old `info` site: a **contact
|
|
73
|
+
lookup** (`?hsContactId=` → server-side `getContact`), nothing more.
|
|
74
|
+
|
|
75
|
+
**Campaign records are added MANUALLY to the Toga database** and reach the app only as the
|
|
76
|
+
`?hsCampaignId=` attribution UUID on entry URLs (the exact same URL contract as `info`).
|
|
77
|
+
The UUID is passed through to Toga (`addContactToCampaign`) for attribution; it does **not**
|
|
78
|
+
select or fetch content.
|
|
79
|
+
|
|
80
|
+
The `CampaignContentProvider` **seam is retained** (so a hypothetical future remote source
|
|
81
|
+
could slot in), but the static `src/content/provider.ts` is the **permanent** implementation
|
|
82
|
+
— not an interim. The throwing `hubspotProvider.ts` stub was **deleted**.
|
|
77
83
|
|
|
78
84
|
Config-driven, per TOGA precedent (`toga25-supply`'s `useClientFields`,
|
|
79
85
|
`toga2-commerce`'s Cart C1–C7): components are **pure renderers of a bundle**; there is
|
|
80
|
-
**no `if (campaign === 'x')` branching** anywhere in the screens. The repo holds
|
|
81
|
-
**schema** (`CampaignBundle` types), the **
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- **Provider seam.** `CampaignContentProvider.resolve(slug)` returns a `CampaignBundle
|
|
85
|
-
`
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
components receive the resolved bundle as props / via `useCampaign()` and never see
|
|
89
|
-
HubSpot.
|
|
86
|
+
**no `if (campaign === 'x')` branching** anywhere in the screens. The repo holds the
|
|
87
|
+
**schema** (`CampaignBundle` types), the **static provider**, and the **`DEFAULT`** bundle
|
|
88
|
+
that carries the real shipped content.
|
|
89
|
+
|
|
90
|
+
- **Provider seam.** `CampaignContentProvider.resolve(slug)` returns a `CampaignBundle`.
|
|
91
|
+
The permanent `provider.ts` implementation returns `DEFAULT` for every slug (server-side,
|
|
92
|
+
during render). Client components receive the resolved bundle as props / via
|
|
93
|
+
`useCampaign()`.
|
|
90
94
|
- **Serializable-only bundles.** Non-serializable behavior (a CTA action, an icon) is
|
|
91
95
|
referenced by an **enum key** and hydrated at the view-model layer through a
|
|
92
|
-
`Record<key, fn|Component>` registry —
|
|
96
|
+
`Record<key, fn|Component>` registry — the bundle stays pure strings/values.
|
|
93
97
|
- **Campaign selection is URL-driven, per-request.** `resolveCampaignId()`, first match
|
|
94
98
|
wins: `?campaign=<slug>` → mapped `?hsCampaignId=<id>` (id→slug lookup so existing
|
|
95
99
|
outreach/CRM links keep working) → `DEFAULT`. Query-param entry ships first (matches how
|
|
@@ -122,10 +126,10 @@ returns the `DEFAULT` bundle, so the funnel always renders. Entry resolution liv
|
|
|
122
126
|
`src/lib/resolveCampaignId.ts` (`?campaign=<slug>` → mapped `?hsCampaignId=` →
|
|
123
127
|
`DEFAULT`; slug regex `^[a-z0-9][a-z0-9-]{0,63}$`).
|
|
124
128
|
|
|
125
|
-
**`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
**`DEFAULT_BUNDLE` (`src/content/default.ts`) is the shipped content**, not interim dev
|
|
130
|
+
copy — it carries the mockup's real copy and is what every slug renders. The throwing
|
|
131
|
+
`hubspotProvider.ts` stub was **deleted** (§9.10 dissolution); do not reintroduce a remote
|
|
132
|
+
provider without a new decision.
|
|
129
133
|
|
|
130
134
|
## Backend reuse (from the `info` repo; existing AI-BDR calls)
|
|
131
135
|
|
|
@@ -157,10 +161,10 @@ Vitest tests. See `web-funnel-app.md` for the full built structure.
|
|
|
157
161
|
|
|
158
162
|
Note on "campaign": `info` uses HubSpot **only to fetch the contact**; the URL's
|
|
159
163
|
`hsCampaignId` is passed to Toga as the attribution campaign UUID
|
|
160
|
-
(`addContactToCampaign`) — attribution-only
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
right campaign for logging.
|
|
164
|
+
(`addContactToCampaign`) — attribution-only. **BDR keeps this exact contract** — HubSpot
|
|
165
|
+
is contact-lookup only, campaign records are added manually to Toga, and the app never
|
|
166
|
+
fetches marketing content from anywhere (see "How campaigns actually work" above). The
|
|
167
|
+
`CampaignBundle` still carries the UUID so a slug maps to the right campaign for logging.
|
|
164
168
|
|
|
165
169
|
### Environment (values live in env / Amplify config, never in the repo)
|
|
166
170
|
|
|
@@ -170,13 +174,11 @@ right campaign for logging.
|
|
|
170
174
|
|
|
171
175
|
## Gotchas
|
|
172
176
|
|
|
173
|
-
- **
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
flat columns can't hold cleanly, so it needs child rows or a structured (JSON) field.
|
|
179
|
-
Settle with the HubSpot owner before building the content layer.
|
|
177
|
+
- **Do NOT reintroduce a HubSpot (or any remote) content source.** §9.10 is resolved by
|
|
178
|
+
dissolution (2026-07-16, HubSpot owner): HubSpot holds no marketing content for this
|
|
179
|
+
product. Content lives in `src/content/default.ts`; the static `provider.ts` is permanent.
|
|
180
|
+
The `CampaignContentProvider` seam is kept only for a *hypothetical* future remote source
|
|
181
|
+
and needs a fresh decision before anyone wires one.
|
|
180
182
|
- **Keep secrets server-side.** `TOGA_CLIENT_API_SECRET` / `HUBSPOT_ACCESS_TOKEN` and the
|
|
181
183
|
token exchange must live in server components / route handlers, never in a `"use client"`
|
|
182
184
|
component or a `NEXT_PUBLIC_` env var (those ship to the browser). The interactive Agent
|
|
@@ -184,17 +186,26 @@ right campaign for logging.
|
|
|
184
186
|
- **Do NOT port `info`'s debug `console.log`s** — several log PII (email/phone). Log
|
|
185
187
|
identifiers only.
|
|
186
188
|
- **Do not port `info`'s hero/curiosity marketing UI** — it is a different, older funnel.
|
|
187
|
-
- **`DEFAULT`
|
|
188
|
-
|
|
189
|
-
- **
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
copy, so enforce via marketing author guidance + the normalization pass; in-repo strings
|
|
193
|
-
get an ESLint no-em-dash check.
|
|
189
|
+
- **`DEFAULT` is what always renders.** Every slug resolves to the in-repo `DEFAULT`
|
|
190
|
+
bundle — an unknown/absent slug is not a special case, it's the norm; never a 404 funnel.
|
|
191
|
+
- **No em dashes in any shipped copy** (mockup rule). Because content is now in-repo, this
|
|
192
|
+
is fully enforced by the ESLint no-em-dash check on in-repo strings — no author-guidance
|
|
193
|
+
or normalization-pass gap.
|
|
194
194
|
- **Single accent source** (mockup rule): every accent-tinted element routes through
|
|
195
195
|
`var(--accent)` (+ derived `--accent-fill`); never hardcode an accent value.
|
|
196
196
|
|
|
197
197
|
## Change history
|
|
198
|
+
- 2026-07-16 — §9.10 RESOLVED by dissolution (decision by Rohan, HubSpot instance owner):
|
|
199
|
+
HubSpot holds NO marketing content for this product and never will — it is contact-lookup
|
|
200
|
+
only (`?hsContactId=`, exactly as `info`). Campaign content lives in-repo (`default.ts` is
|
|
201
|
+
the authoring surface; every slug renders `DEFAULT`); campaign records are added MANUALLY
|
|
202
|
+
to Toga and reach the app only as the `?hsCampaignId=` attribution UUID. Rewrote the
|
|
203
|
+
content section (was "HubSpot-owned, fetched by slug"), removed the HubSpot-over-Contentful
|
|
204
|
+
framing, the throwing `hubspotProvider.ts` was DELETED, and the static `provider.ts` is now
|
|
205
|
+
the permanent implementation (the `CampaignContentProvider` seam is kept only for a
|
|
206
|
+
hypothetical future remote source). Dropped the now-moot runtime-fetch latency / HubSpot
|
|
207
|
+
content-mechanism gotchas. Flipped `draft`→`active` (the reason it was draft is resolved).
|
|
208
|
+
(tcox)
|
|
198
209
|
- 2026-07-14 — Content contract BUILT + QA'd (PLAN Phases 0–5): schema calibrated by
|
|
199
210
|
building the real screens (`src/content/schema.ts` is now THE contract — chrome,
|
|
200
211
|
capabilities eyebrows/qbank/howLink, `connectNote` {label,value}, earlyAccess/phone/
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 33 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 11 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -14,11 +14,12 @@ project: _Underscore
|
|
|
14
14
|
client: compass-canada
|
|
15
15
|
type: profile
|
|
16
16
|
status: active
|
|
17
|
-
updated: 2026-
|
|
17
|
+
updated: 2026-07-16
|
|
18
18
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
19
19
|
files: []
|
|
20
20
|
related:
|
|
21
21
|
- ../compass-usa/profile.md
|
|
22
|
+
- ../compass-usa/features/approval-decision-flow.md
|
|
22
23
|
- ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
|
|
23
24
|
- ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
24
25
|
- ../../2.0/apps/_underscore/features/surface-resolver.md
|
|
@@ -59,6 +60,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
59
60
|
Compass USA's 1/3/4. See [Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
|
|
60
61
|
- Customer language preference: `UserGlobalSettings.settingId = 2` (`en` / `fr-CA`); customer-
|
|
61
62
|
facing emails are sent in EN or FR accordingly.
|
|
63
|
+
- **Approval decisions use the shared Compass parent.** `_Model_Compass_Canada_ApprovalDecision`
|
|
64
|
+
is an empty subclass of `_Model_Compass_ApprovalDecision`; the entire approval/notification/
|
|
65
|
+
manager-reassignment/VIP-auto-approve flow lives in the parent and branches at runtime on
|
|
66
|
+
`clientIdentifier === 'Compass_Canada'` (email-template UUID + EN/FR localization). Fixes to the
|
|
67
|
+
parent cover Canada automatically — no per-tenant change. See
|
|
68
|
+
[Approval-Decision Flow](../compass-usa/features/approval-decision-flow.md).
|
|
62
69
|
- Assortment (product-grouping) names are served in fr-CA via the `AssortmentTranslations` sidecar
|
|
63
70
|
(16 rows seeded). French was extracted from the old bilingual `"English/French"` `Assortments.name`
|
|
64
71
|
values, which were then cleaned to English-only. See
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php |
|
|
5
6
|
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
6
7
|
| [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
|
|
7
8
|
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass Approval-Decision Flow (Notifications & Manager Reassignment)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: compass-usa
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-16
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Compass/ApprovalDecision.php
|
|
13
|
+
- _underscore/Model/Compass/Usa/ApprovalDecision.php
|
|
14
|
+
- _underscore/Model/Compass/Canada/ApprovalDecision.php
|
|
15
|
+
related:
|
|
16
|
+
- mr-ma-order-approval-and-status.md
|
|
17
|
+
- ../../compass-canada/profile.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/email-template-sending.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**,
|
|
23
|
+
VIP auto-approve, and EN/FR localization — lives **entirely in the shared parent
|
|
24
|
+
`_Model_Compass_ApprovalDecision`**. Both tenant subclasses,
|
|
25
|
+
`_Model_Compass_Usa_ApprovalDecision` and `_Model_Compass_Canada_ApprovalDecision`, are **empty**
|
|
26
|
+
(`extends _Model_Compass_ApprovalDecision {}`, no overrides). Client- and language-specific
|
|
27
|
+
behavior is **not** subclassed; it branches at runtime inside the parent on
|
|
28
|
+
`$api->client->clientIdentifier` (`Compass_Usa` vs `Compass_Canada`). Practical consequence: a fix
|
|
29
|
+
to a parent method applies to **both** Compass US and Compass Canada automatically — there is no
|
|
30
|
+
per-client code change to make.
|
|
31
|
+
|
|
32
|
+
This doc covers the **approval-decision / notification** half of Compass approvals. The separate
|
|
33
|
+
auto-approval + `_status` gating on the order itself is in
|
|
34
|
+
[Compass MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md).
|
|
35
|
+
|
|
36
|
+
## Key files / entry points
|
|
37
|
+
- **`_underscore/Model/Compass/ApprovalDecision.php`** — the shared parent that owns the whole
|
|
38
|
+
flow (notification-list maintenance, `_swapManagerEmailAddress()`, VIP auto-approve, email-template
|
|
39
|
+
UUID resolution, localization).
|
|
40
|
+
- **`_underscore/Model/Compass/Usa/ApprovalDecision.php`** — empty subclass, no overrides.
|
|
41
|
+
- **`_underscore/Model/Compass/Canada/ApprovalDecision.php`** — empty subclass, no overrides.
|
|
42
|
+
|
|
43
|
+
## How it works
|
|
44
|
+
### One shared parent, runtime client branch (not subclass overrides)
|
|
45
|
+
Compass US and Compass Canada resolve to different model classes at request time
|
|
46
|
+
(`_Model_Compass_Usa_ApprovalDecision` / `_Model_Compass_Canada_ApprovalDecision`), but both are
|
|
47
|
+
empty shells extending `_Model_Compass_ApprovalDecision`. All behavior is inherited. Where the two
|
|
48
|
+
tenants differ, the **parent** branches on `$api->client->clientIdentifier === 'Compass_Canada'`:
|
|
49
|
+
- **Email-template resolution** — `resolveEmailTemplateUuid()` picks the tenant's template UUID.
|
|
50
|
+
- **EN/FR localization** — user language is read from `UserGlobalSettings` (`settingId = 2`;
|
|
51
|
+
`en` / `fr-CA`) so Canadian notifications go out in the recipient's language.
|
|
52
|
+
|
|
53
|
+
Because the divergence is a runtime branch and not an override, do **not** add per-client logic to
|
|
54
|
+
the empty subclasses — extend the shared parent and branch there if a genuine tenant difference is
|
|
55
|
+
needed.
|
|
56
|
+
|
|
57
|
+
### Manager reassignment and the notification list
|
|
58
|
+
When a step-2 (Manager) approval decision is **reassigned** to a new manager,
|
|
59
|
+
`_swapManagerEmailAddress()` updates the order's CC/notification list in `SalesOrderEmailAddresses`:
|
|
60
|
+
it removes the outgoing manager's email and adds the incoming manager's. The one email that must
|
|
61
|
+
**never** be dropped is the **order requester's** — the person the order is for, resolved via
|
|
62
|
+
`SalesOrders.contactId → Users` (the requester's email is looked up with a parameterized
|
|
63
|
+
`SELECT Users.email FROM SalesOrders INNER JOIN Users ON Users.contactId = SalesOrders.contactId
|
|
64
|
+
WHERE SalesOrders.id = ?`). Before deleting the old-manager email, the method compares it against
|
|
65
|
+
both the new-manager email and the requester email and **skips the delete when it matches the
|
|
66
|
+
requester**. All of these comparisons are case-insensitive (`strcasecmp()`), matching
|
|
67
|
+
`getFilteredCcEmails()`.
|
|
68
|
+
|
|
69
|
+
## Gotchas / known issues
|
|
70
|
+
- **Never drop the order requester from notifications on manager reassignment.** If the outgoing
|
|
71
|
+
manager happens to also be the order requester (same email), deleting the old-manager address
|
|
72
|
+
from `SalesOrderEmailAddresses` would silently remove the requester from all order notifications.
|
|
73
|
+
`_swapManagerEmailAddress()` guards against this by comparing the old-manager email to the
|
|
74
|
+
requester's email (looked up via `SalesOrders.contactId → Users`) and skipping the delete when
|
|
75
|
+
they match. This was a live bug (fixed 2026-07-16).
|
|
76
|
+
- **Email comparisons must be case-insensitive.** Use `strcasecmp()`, not `!==` — the notification
|
|
77
|
+
list can hold the same address in different casing, and `getFilteredCcEmails()` already compares
|
|
78
|
+
case-insensitively. A strict `!==` comparison here previously let a casing mismatch defeat the
|
|
79
|
+
requester/new-manager guards.
|
|
80
|
+
- **Don't put tenant behavior in the empty subclasses.** `Usa`/`Canada` `ApprovalDecision` are
|
|
81
|
+
intentionally empty; the tenant branch lives in the parent on `clientIdentifier`.
|
|
82
|
+
|
|
83
|
+
## Change history
|
|
84
|
+
- 2026-07-16 — Fixed `_swapManagerEmailAddress()` dropping the order requester from the order's
|
|
85
|
+
notification list (`SalesOrderEmailAddresses`) when a step-2 manager was reassigned and the
|
|
86
|
+
outgoing manager's email also belonged to the requester: now looks up the requester email via
|
|
87
|
+
`SalesOrders.contactId → Users` and skips the delete when they match; also switched the
|
|
88
|
+
old-vs-new and old-vs-requester comparisons to case-insensitive `strcasecmp()` (was `!==`) to
|
|
89
|
+
match `getFilteredCcEmails()`. Documented that the flow is the shared parent
|
|
90
|
+
`_Model_Compass_ApprovalDecision` (empty `Usa`/`Canada` subclasses, runtime `clientIdentifier`
|
|
91
|
+
branch), so the fix covers Compass Canada automatically. Shipped on branch TRUE-80244 (pushed;
|
|
92
|
+
not yet merged to `_beta`). (apeterson)
|
|
93
|
+
|
|
94
|
+
## Related docs
|
|
95
|
+
- [Compass MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md) — the
|
|
96
|
+
auto-approval + `_status` gating half of Compass approvals (on `_Model_Compass_SalesOrder`).
|
|
97
|
+
- [Compass Canada](../../compass-canada/profile.md) — uses this identical flow; the runtime
|
|
98
|
+
`Compass_Canada` branch drives its EN/FR notification localization.
|
|
99
|
+
- [Email Template Sending](../../../2.0/apps/_underscore/features/email-template-sending.md) — the
|
|
100
|
+
shared engine the resolved approval-notification templates are sent through.
|
|
@@ -15,12 +15,13 @@ project: _Underscore
|
|
|
15
15
|
client: compass-usa
|
|
16
16
|
type: profile
|
|
17
17
|
status: active
|
|
18
|
-
updated: 2026-07-
|
|
18
|
+
updated: 2026-07-16
|
|
19
19
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
20
20
|
files: []
|
|
21
21
|
related:
|
|
22
22
|
- features/asn-to-item-fulfillment.md
|
|
23
23
|
- features/cost-centers.md
|
|
24
|
+
- features/approval-decision-flow.md
|
|
24
25
|
- workflows/cross-kit-bundle-corruption.md
|
|
25
26
|
- ../../2.0/apps/worker2/features/compass-vip-support-importer.md
|
|
26
27
|
- ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
|
|
@@ -75,6 +76,10 @@ separate, related client (see its own profile).
|
|
|
75
76
|
- [VIP Support Importer (worker2)](../../2.0/apps/worker2/features/compass-vip-support-importer.md)
|
|
76
77
|
— manual (Postman) worker2 action that reads Compass's quarterly VIP spreadsheet and sets
|
|
77
78
|
`Users.c_supportedByUserId` (assigned support tech) per VIP.
|
|
79
|
+
- [Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) —
|
|
80
|
+
the shared `_Model_Compass_ApprovalDecision` parent (empty `Usa`/`Canada` subclasses; runtime
|
|
81
|
+
`clientIdentifier` branch) that owns approval notifications, manager reassignment, VIP
|
|
82
|
+
auto-approve, and EN/FR localization.
|
|
78
83
|
|
|
79
84
|
## Notes
|
|
80
85
|
- **Order status is shipped-only (2026-06-30).** Compass imports all IF stages
|
package/package.json
CHANGED