toga-ai 1.0.322 → 1.0.324
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/INDEX.md +2 -2
- package/knowledge/2.0/apps/ai-bdr/architecture.md +7 -2
- package/knowledge/2.0/apps/ai-bdr/features/bdr-web-funnel-plan.md +81 -59
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-content-model.md +30 -7
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/mits-po-to-so-item-linking.md +26 -2
- package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +124 -0
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [AI-BDR Architecture](architecture.md) | **AI-BDR** is TOGA's outbound AI sales-development representative. | ai-bdr/README.md, ai-bdr/requirements.txt, ai-bdr/.env.example, ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/vapi/templates/assistant.template.json, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py |
|
|
6
|
-
| [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Status:**
|
|
6
|
+
| [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Status:** Draft 2 (needs one more review). | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
|
|
7
7
|
| [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
|
|
8
8
|
| [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
|
|
9
|
-
| [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | The **public-facing web funnel** is the new UI / lead-capture front door of the **same AI-BDR product** documented in `../architecture.md
|
|
9
|
+
| [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | The **public-facing web funnel** is the new UI / lead-capture front door of the **same AI-BDR product** documented in `../architecture.md` — it is the front doo | bdr/src/content/schema.ts, bdr/src/content/hubspotProvider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts |
|
|
10
10
|
| [New-Campaign Onboarding (6-phase runbook)](workflows/new-campaign-onboarding.md) | The end-to-end procedure for launching a new outbound BDR campaign (industry, offer, target persona, geography). | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/calcom-new-campaign-event-guide.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py |
|
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-13
|
|
10
10
|
owners: [akhokhani, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- ai-bdr/README.md
|
|
@@ -184,7 +184,10 @@ this system's existing calls** (HubSpot contact fetch, Toga upsert + campaign li
|
|
|
184
184
|
`requestCall` callback — see *End-to-end call flow* and `features/call-orchestration.md`),
|
|
185
185
|
ported from the external `info` repo behind `LeadSink` / `CallbackService` adapters so all
|
|
186
186
|
secret-bearing calls stay server-side. On top of that reuse it adds a **HubSpot-owned,
|
|
187
|
-
config-driven campaign content layer
|
|
187
|
+
config-driven campaign content layer** (HubSpot chosen over Contentful — marketing already
|
|
188
|
+
operates there, the campaign model already exists, and the read is a trivial server-side
|
|
189
|
+
fetch; the stable content shape makes a dedicated headless CMS unnecessary): marketing
|
|
190
|
+
authors campaign content in HubSpot,
|
|
188
191
|
fetched by slug at runtime and mapped to a serializable `CampaignBundle` (config-driven
|
|
189
192
|
components, `DEFAULT` fallback — no `if(campaign===x)`; a copy change ships with no
|
|
190
193
|
redeploy). Full detail: **`features/web-funnel-content-model.md`**.
|
|
@@ -213,3 +216,5 @@ redeploy). Full detail: **`features/web-funnel-content-model.md`**.
|
|
|
213
216
|
product's UI / lead-capture front door — reuses this system's existing HubSpot/Toga/callback
|
|
214
217
|
calls and adds a HubSpot-owned, config-driven campaign content layer. See
|
|
215
218
|
`features/web-funnel-content-model.md`. (tcox)
|
|
219
|
+
- 2026-07-13 — Additive: recorded the HubSpot-over-Contentful content-store decision for the
|
|
220
|
+
web funnel. (tcox)
|
|
@@ -6,7 +6,7 @@ project: "AI-BDR"
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: draft
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-13
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
@@ -20,8 +20,8 @@ related:
|
|
|
20
20
|
|
|
21
21
|
# BDR: Multi-Campaign "AI BDR / Agent Studio" Web App — Implementation Plan
|
|
22
22
|
|
|
23
|
-
> **Status:**
|
|
24
|
-
>
|
|
23
|
+
> **Status:** Draft 2 (needs one more review). Open items tracked in §9; final campaign direction /
|
|
24
|
+
> verbiage still pending from leadership.
|
|
25
25
|
> **Author:** tcox · **Stack:** TOGA 2.0 front-end · Next.js (App Router) · **Client scope:** shared / internal (TOGA)
|
|
26
26
|
> **Repo (new):** `bdr` · **Two source inputs:** the **mockup** (real UI) + **`info`** (backend to reuse).
|
|
27
27
|
>
|
|
@@ -65,22 +65,23 @@ mockup folder, and the `info` repo.
|
|
|
65
65
|
TypeScript, matching `info`). Next.js is the chosen framework because the site is **highly public**,
|
|
66
66
|
so server-side rendering + SEO matter, and its server (server components + route handlers) is where
|
|
67
67
|
the secret-bearing HubSpot/Toga/content calls run natively (§5.6). Every campaign is defined by
|
|
68
|
-
content authored in HubSpot
|
|
69
|
-
run side by side, and the real HubSpot /
|
|
68
|
+
**content authored by marketing in HubSpot** and fetched **by slug at runtime**, so adding or changing
|
|
69
|
+
a campaign needs no code and no redeploy. Multiple campaigns run side by side, and the real HubSpot /
|
|
70
|
+
Toga / callback backend from `info` is reused directly.
|
|
70
71
|
|
|
71
72
|
**Done when:**
|
|
72
73
|
1. The full flow (Welcome, Build, Capabilities, Call Now, Schedule, Success) renders in Next.js
|
|
73
74
|
with visual parity to the mockup (pixel-perfect pass against the mockup screens).
|
|
74
75
|
2. Two distinct campaigns (for example a generic **BDR-as-a-service** and a **healthcare** variant),
|
|
75
|
-
**each authored in HubSpot**, render differing only in data (copy, Q&A proof points, agent
|
|
76
|
-
accent), with zero component-code differences. Adding a campaign
|
|
77
|
-
code, no redeploy).
|
|
78
|
-
3. No user-facing string is
|
|
76
|
+
**each authored in HubSpot**, render differing only in **data** (copy, Q&A proof points, agent
|
|
77
|
+
identity, accent), with zero component-code differences. Adding/changing a campaign is a **HubSpot
|
|
78
|
+
edit** (no code, no redeploy).
|
|
79
|
+
3. No user-facing string is inline in a component (grep-verified); all copy comes from the HubSpot
|
|
79
80
|
campaign content, fetched by slug at runtime and resolved through one hook.
|
|
80
81
|
4. The simulated call/schedule actions are wired to the **real backend** (Toga `requestCall`,
|
|
81
82
|
HubSpot/Toga contact upsert) behind a `CallbackService` / `LeadSink` adapter ported from `info`.
|
|
82
|
-
5.
|
|
83
|
-
`
|
|
83
|
+
5. Content is fetched from **HubSpot at runtime** through the **`CampaignContentProvider` seam** (§5.2),
|
|
84
|
+
with a `DEFAULT` safety-net fallback, so marketing edits copy without a developer or a redeploy.
|
|
84
85
|
6. Copy honors the mockup rules: no em dashes; a single `--accent` source drives all accents.
|
|
85
86
|
7. No PII in logs; the `info` debug logging is not carried over.
|
|
86
87
|
8. Deploys (AWS Amplify) with documented env vars.
|
|
@@ -197,7 +198,7 @@ the bundle carries the UUID so a slug maps to the right campaign for logging.
|
|
|
197
198
|
| Framework | **Next.js (App Router) + TypeScript** | Chosen because the site is highly public: SSR + SEO, and a built-in server for the secret-bearing HubSpot/Toga/content calls. Matches `info`, so its patterns port directly. |
|
|
198
199
|
| UI source | **The mockup** | Rebuild its Agent Studio flow faithfully (pixel-perfect). The mockup is React, so components port cleanly into Next.js client components. |
|
|
199
200
|
| Backend source | **`info`** | Reuse HubSpot/Toga/callback logic directly (same framework). |
|
|
200
|
-
| Content model | **Runtime, HubSpot-owned** | Campaign content is authored by
|
|
201
|
+
| Content model | **Runtime, HubSpot-owned** | Content store = **HubSpot** (decided over Contentful: the site's content shape is stable / not frequently restructured, so HubSpot's flat-modeling limits are acceptable, and the API integration is trivial — confirmed in `info`, §5.2). Campaign content is authored by marketing in HubSpot and fetched **by slug at runtime**; **no campaign content in the repo, no redeploy to change copy**. Served behind the `CampaignContentProvider` seam with a `DEFAULT` fallback. See §5.2 / §5.5. |
|
|
201
202
|
| Backend relationship | **Same product; reuses the AI-BDR backend** | This funnel is the new UI / front door of TOGA's existing **AI-BDR** product; it reuses that system's HubSpot/Toga/callback calls (documented at `2.0/apps/ai-bdr/`). Lead/callback handoff still goes behind `LeadSink`/`CallbackService` adapters for a clean seam. |
|
|
202
203
|
| Campaign entry | **Plan recommends** | See §5.5. |
|
|
203
204
|
| Copy rules | **No em dashes; one accent source** | Accent enforced in code. No-em-dash cannot be linted on HubSpot-authored copy: enforce via author guidance + a normalization pass in the backend mapping. |
|
|
@@ -230,31 +231,52 @@ Port approach:
|
|
|
230
231
|
> `NEXT_PUBLIC_` env.
|
|
231
232
|
|
|
232
233
|
### 5.2 Content provider — HubSpot is the source of truth
|
|
233
|
-
**
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
**Decision:** the content store is **HubSpot** (chosen over Contentful). The site's content shape is
|
|
235
|
+
stable / not frequently restructured, so HubSpot's flatter modeling is acceptable, and a dedicated
|
|
236
|
+
headless CMS (Contentful) would only pay off if the content model were rich and frequently
|
|
237
|
+
restructured, which it is not.
|
|
238
|
+
|
|
239
|
+
**Why HubSpot (rationale for the team):**
|
|
240
|
+
- **Marketing already operates there.** The contact database, campaigns, outreach, lists, and workflows
|
|
241
|
+
all live in HubSpot. Content sits with the team that authors it, and we reuse the existing
|
|
242
|
+
go-to-market engine instead of standing up a parallel system.
|
|
243
|
+
- **The campaign model already exists.** The old BDR site (`info`) already uses HubSpot for lead
|
|
244
|
+
sourcing, known-visitor personalization, and **campaign attribution** (`hsContactId` / `hsCampaignId`
|
|
245
|
+
→ Toga). Campaign content keys naturally off that same campaign concept, and content + attribution
|
|
246
|
+
stay in one system rather than split across two.
|
|
247
|
+
- **The API call is trivial.** Reading from HubSpot is a ~15-line server-side fetch proxied by a Next
|
|
248
|
+
route handler (proven in `info`; the token never reaches the browser). The same pattern serves any
|
|
249
|
+
HubSpot read, HubDB included. No new infrastructure.
|
|
250
|
+
- **No new vendor.** HubSpot is already in the stack and paid for; Contentful would add a SaaS contract
|
|
251
|
+
and a second integration with no offsetting benefit given the stable content shape.
|
|
252
|
+
- **Trade-off we are accepting:** HubSpot is not a purpose-built structured-content CMS, so richly
|
|
253
|
+
nested content and non-dev editor UX are weaker than Contentful — acceptable here because the content
|
|
254
|
+
shape is stable (§9.10 tracks how we model it).
|
|
255
|
+
|
|
256
|
+
**Campaign content lives in HubSpot, authored by marketing, and is fetched by slug at runtime** — not
|
|
257
|
+
in the repo, no redeploy to change copy. One seam makes this true:
|
|
236
258
|
```ts
|
|
237
259
|
interface CampaignContentProvider {
|
|
238
|
-
resolve(slug: string | undefined): Promise<CampaignBundle>; // returns
|
|
260
|
+
resolve(slug: string | undefined): Promise<CampaignBundle>; // always returns a valid bundle (DEFAULT if unknown)
|
|
239
261
|
list?(): Promise<CampaignSummary[]>;
|
|
240
262
|
}
|
|
241
|
-
// Primary: HubSpotContentProvider — runs server-side (
|
|
242
|
-
//
|
|
243
|
-
//
|
|
263
|
+
// Primary: HubSpotContentProvider — runs server-side (page server component / route handler); reads
|
|
264
|
+
// content from HubSpot (§9.10 mechanism), maps it to a CampaignBundle, caches it. Secrets
|
|
265
|
+
// stay server-side.
|
|
244
266
|
// Fallback: a minimal built-in DEFAULT bundle (engineering safety net only, NOT authored marketing
|
|
245
267
|
// copy) so the funnel still renders if a slug is unknown or HubSpot is briefly unreachable.
|
|
246
268
|
```
|
|
247
|
-
`resolve(slug)` runs on the server during render (SSR), so the
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
269
|
+
`resolve(slug)` runs on the server during render (SSR), so the fetch + mapping to the `CampaignBundle`
|
|
270
|
+
shape (§6) happens before the page is sent, with Next caching / revalidation so marketing edits appear
|
|
271
|
+
quickly without a deploy (and the page stays SEO-friendly). Components consume the resolved bundle via
|
|
272
|
+
`useCampaign()`; they never see HubSpot or the provider.
|
|
251
273
|
|
|
252
274
|
> **Key sub-decision (open, §9.10):** *how* campaign content is represented in HubSpot. HubSpot's
|
|
253
|
-
> "Campaigns" tool is built for attribution, not rich structured copy, so this
|
|
254
|
-
>
|
|
255
|
-
>
|
|
256
|
-
>
|
|
257
|
-
>
|
|
275
|
+
> "Campaigns" tool is built for attribution, not rich structured copy, so this content (headlines, a
|
|
276
|
+
> ~10-entry Q&A pool, agent identity, call summaries) needs a deliberate home. Likely **HubDB** (a
|
|
277
|
+
> CMS-Hub table: one row per campaign, a `slug` column + content columns, API-fetchable) or a **custom
|
|
278
|
+
> object**. This drives the fetch + the `CampaignBundle` mapping and needs confirmation with whoever
|
|
279
|
+
> owns the HubSpot instance before the content layer (Phase 1) is built.
|
|
258
280
|
|
|
259
281
|
### 5.3 Campaign as data, components as renderers
|
|
260
282
|
Follow the TOGA config-driven precedents (`toga25-supply` `useClientFields`; `toga2-commerce` Cart
|
|
@@ -289,13 +311,11 @@ safety-net `DEFAULT`.
|
|
|
289
311
|
(id -> slug lookup, so CRM links keep working) -> `DEFAULT`. An unknown or missing slug always falls
|
|
290
312
|
back to the `DEFAULT` bundle, so the funnel never fails to render.
|
|
291
313
|
|
|
292
|
-
**4. The slug drives a runtime fetch of the content from HubSpot
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
change copy. If the slug is unknown or HubSpot is briefly unreachable, the minimal `DEFAULT` safety net
|
|
298
|
-
renders so the funnel never breaks.
|
|
314
|
+
**4. The slug drives a runtime fetch of the content from HubSpot:** the page's server component calls
|
|
315
|
+
`provider.resolve(slug)`, which reads that campaign's content from **HubSpot** (§9.10), maps it to a
|
|
316
|
+
`CampaignBundle`, and caches it (Next revalidation). Because this is server-side, the fully-populated
|
|
317
|
+
page is rendered and sent (SEO-friendly), and marketing edits appear on the next revalidation with no
|
|
318
|
+
redeploy. An unknown/missing slug falls back to the `DEFAULT` bundle so the funnel never fails to render.
|
|
299
319
|
|
|
300
320
|
**5. Multiple campaigns run simultaneously by construction.** Selection is per-visitor, derived from
|
|
301
321
|
their URL each load; there is no global "current campaign" state. Any number of campaigns are live at
|
|
@@ -343,11 +363,10 @@ never blank. Prune unused slots per the campaigns actually shipped (the default
|
|
|
343
363
|
---
|
|
344
364
|
|
|
345
365
|
## 6. Campaign bundle schema (concrete sketch)
|
|
346
|
-
This is the shape the app consumes
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
key. Covers the real content surfaces from §2.4.
|
|
366
|
+
This is the shape the app consumes, and **the stable contract between HubSpot and the components** —
|
|
367
|
+
components only ever read this. The server maps HubSpot fields into this shape (that mapping layer is
|
|
368
|
+
where the no-em-dash normalization runs). Serializable data only; behavior/icons by key. Covers the
|
|
369
|
+
real content surfaces from §2.4.
|
|
351
370
|
|
|
352
371
|
> The richer fields here (the nested `pool: QA[]` + `sets`, `callSummaries`, `RichLine` accent spans)
|
|
353
372
|
> are the parts hardest to represent in a marketing-friendly way in HubSpot — a direct input to the
|
|
@@ -403,7 +422,7 @@ bdr/
|
|
|
403
422
|
c/[slug]/page.tsx # optional path-based campaign entry (later)
|
|
404
423
|
api/{lead,call-now,call-later}/route.ts # SERVER: secret-bearing Toga/HubSpot calls (ported from info)
|
|
405
424
|
content/
|
|
406
|
-
schema.ts # CampaignBundle types (the contract)
|
|
425
|
+
schema.ts # CampaignBundle types (the stable contract)
|
|
407
426
|
hubspotProvider.ts # SERVER: reads campaign content from HubSpot + maps to bundle (§5.2/§6)
|
|
408
427
|
useCampaign.ts # client hook to read the resolved bundle
|
|
409
428
|
default.ts # minimal safety-net fallback ONLY (not authored copy)
|
|
@@ -431,11 +450,11 @@ so fidelity should be near-exact.
|
|
|
431
450
|
Amplify SSR hosting). Port `styles.css` and the CSS-variable accent system. Set up campaign routing
|
|
432
451
|
(query param now, `/c/[slug]` later) and ESLint incl. a no-em-dash check on in-repo strings. Read
|
|
433
452
|
`node_modules/next/dist/docs/` first (Next breaking changes per `info/AGENTS.md`).
|
|
434
|
-
- **Phase 1 — Content layer.** Depends on the §9.10 HubSpot-modeling decision. Define `schema.ts`
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
453
|
+
- **Phase 1 — Content layer.** Depends on the §9.10 HubSpot-modeling decision. Define `schema.ts` (the
|
|
454
|
+
`CampaignBundle` contract), the server-side `hubspotProvider` (`resolve(slug)` reads HubSpot + maps to
|
|
455
|
+
a bundle, with caching/revalidation + no-em-dash normalization), the `useCampaign` hook, and a minimal
|
|
456
|
+
safety-net `DEFAULT`. Seed HubSpot with one real campaign (extract the mockup's current copy into
|
|
457
|
+
HubSpot, not the repo) to develop against. Unit-test mapping + fallback.
|
|
439
458
|
- **Phase 2 — Port the component library.** Bring `components.jsx` across as real modules (SwapFade,
|
|
440
459
|
Particles, AgentArt + data, VoiceDial, ColorSelector, SynapseButton, StepRail, Wordmark, Ico,
|
|
441
460
|
asset/preload). Migrate assets to `public/`. This unblocks every screen.
|
|
@@ -443,7 +462,8 @@ so fidelity should be near-exact.
|
|
|
443
462
|
(skills + Q&A bank + CTAs), so building it first calibrates the real estimate and proves the bundle
|
|
444
463
|
schema before the remaining screens.
|
|
445
464
|
- **Phase 4 — Remaining screens + modals from config.** AutoBuild, Landing (behind a feature flag),
|
|
446
|
-
Call Now, Schedule, Success, Welcome/WelcomeStep2/EarlyAccess, StepRail.
|
|
465
|
+
Call Now, Schedule, Success, Welcome/WelcomeStep2/EarlyAccess, StepRail. All copy comes from the
|
|
466
|
+
HubSpot-resolved bundle (no inline strings in components).
|
|
447
467
|
- **Phase 5 — Wire the real backend.** Port `info`'s `lib/toga.ts` / `lib/hubspot.ts` + its call
|
|
448
468
|
route handlers into BDR's `app/api/*` (server-side); wire the `LeadSink` / `CallbackService`
|
|
449
469
|
adapters; replace the `Success` simulation with real immediate/scheduled `requestCall`; remove PII
|
|
@@ -459,12 +479,13 @@ so fidelity should be near-exact.
|
|
|
459
479
|
|
|
460
480
|
---
|
|
461
481
|
|
|
462
|
-
## 9. Open questions (leadership + HubSpot owner). Most do not block early work; **§9.10 gates
|
|
482
|
+
## 9. Open questions (leadership + HubSpot owner). Most do not block early work; **§9.10 gates the content layer**.
|
|
463
483
|
1. **Backend relationship:** RESOLVED — `bdr` is the new UI / front door of TOGA's existing **AI-BDR**
|
|
464
484
|
product (not standalone); it reuses that system's backend calls, and its knowledge is filed under
|
|
465
485
|
`2.0/apps/ai-bdr/`. `CallbackService` uses that system's callback path.
|
|
466
|
-
2. **
|
|
467
|
-
|
|
486
|
+
2. **Content store:** RESOLVED — **HubSpot** (over Contentful): content shape is stable, so HubSpot is
|
|
487
|
+
acceptable and the integration is trivial (§5.2). Content is HubSpot-owned from the start (no
|
|
488
|
+
hardcoding); the remaining detail is the HubSpot mechanism (§9.10).
|
|
468
489
|
3. **Agent choice:** Stay with a campaign-fixed agent (auto-build to a preset), or ship the manual
|
|
469
490
|
Creator (currently parked / "coming soon")? Determines whether `Creator` + full slot art ship now.
|
|
470
491
|
4. **Service picker:** Is the `Landing` service-selection screen (AI BDR + "Talos coming soon") in
|
|
@@ -478,15 +499,15 @@ so fidelity should be near-exact.
|
|
|
478
499
|
9. **Backend hosting:** RESOLVED by the Next.js choice — the secret-bearing HubSpot/Toga calls run in
|
|
479
500
|
BDR's own server components / route handlers; no separate backend to host. Only remaining item is
|
|
480
501
|
confirming the Amplify **SSR** hosting target (not static) at deploy.
|
|
481
|
-
10. **HubSpot content mechanism (
|
|
482
|
-
HubSpot so it is marketing-editable and fetchable by slug
|
|
483
|
-
contacts). Options: **HubDB** (CMS-Hub table, one row per campaign, `slug`
|
|
484
|
-
API-fetchable — the likely fit) vs. a **custom object** vs. properties on
|
|
485
|
-
determines the fetch + the HubSpot->`CampaignBundle` mapping. Sub-parts to
|
|
486
|
-
owner: which mechanism; how
|
|
487
|
-
`RichLine`s) is represented (flat columns can't hold arrays cleanly — child rows or
|
|
488
|
-
who authors/owns the schema; and how the slug relates to the existing
|
|
489
|
-
UUID used for attribution.
|
|
502
|
+
10. **HubSpot content mechanism (gates the content layer, Phase 1) — the big one.** *How* campaign
|
|
503
|
+
content is stored in HubSpot so it is marketing-editable and fetchable by slug. `info` gives no
|
|
504
|
+
precedent (it only reads contacts). Options: **HubDB** (CMS-Hub table, one row per campaign, `slug`
|
|
505
|
+
column + content columns, API-fetchable — the likely fit) vs. a **custom object** vs. properties on
|
|
506
|
+
the Campaigns object. This determines the fetch + the HubSpot->`CampaignBundle` mapping. Sub-parts to
|
|
507
|
+
settle with the HubSpot owner: which mechanism; how **nested content** (Q&A pool + rotation sets, call
|
|
508
|
+
summaries, accent `RichLine`s) is represented (flat columns can't hold arrays cleanly — child rows or
|
|
509
|
+
a JSON field); who authors/owns the schema; and how the slug relates to the existing
|
|
510
|
+
`hsCampaignId`/Toga campaign UUID used for attribution.
|
|
490
511
|
|
|
491
512
|
---
|
|
492
513
|
|
|
@@ -530,4 +551,5 @@ so fidelity should be near-exact.
|
|
|
530
551
|
- [ ] Pixel-perfect parity pass against the mockup screens + screenshots.
|
|
531
552
|
|
|
532
553
|
## Change history
|
|
554
|
+
- 2026-07-13 — Draft 2 resync: re-mirrored the body from the current `BDR/PLAN.md`. Framework locked to Next.js App Router (earlier React/Vite SPA idea dropped); content store decided HubSpot over Contentful (rationale in §4/§5.2); no-hardcoding (HubSpot-owned, fetched by slug from the start); same-product framing confirmed (front door of the existing AI-BDR backend, "backend relationship" open question resolved); meeting-specific dates/cadence removed. Still a draft. (tcox)
|
|
533
555
|
- 2026-07-08 — Captured the full BDR web-funnel implementation plan verbatim into the knowledge repo as a companion to the distilled `web-funnel-content-model.md` feature doc (redundancy intentional). (tcox)
|
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: draft
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-13
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/content/schema.ts
|
|
@@ -25,11 +25,16 @@ related:
|
|
|
25
25
|
## What this is
|
|
26
26
|
|
|
27
27
|
The **public-facing web funnel** is the new UI / lead-capture front door of the **same
|
|
28
|
-
AI-BDR product** documented in `../architecture.md
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
system's existing backend calls** (see "Backend reuse" below); the API/backend
|
|
32
|
-
are already documented and are **not** restated here.
|
|
28
|
+
AI-BDR product** documented in `../architecture.md` — it is the front door of that existing
|
|
29
|
+
product, not a standalone system. It is a **Next.js (App Router) + TypeScript** app living
|
|
30
|
+
in its own code repo, `bdr`. Only the UI/content layer is new — the funnel **reuses this
|
|
31
|
+
AI-BDR system's existing backend calls** (see "Backend reuse" below); the API/backend
|
|
32
|
+
contracts are already documented and are **not** restated here.
|
|
33
|
+
|
|
34
|
+
**Framework:** Next.js (App Router) + TypeScript is the locked choice because the site is
|
|
35
|
+
highly public — SSR + SEO matter, and Next's server (server components + route handlers)
|
|
36
|
+
natively hosts the secret-bearing HubSpot/Toga calls (an earlier React/Vite SPA idea was
|
|
37
|
+
dropped). It also matches the `info` repo, so `info`'s patterns port directly.
|
|
33
38
|
|
|
34
39
|
The UI is built pixel-perfect from the mockup at `BDR/mockup`: a **7-screen "Agent Studio"
|
|
35
40
|
flow** — Welcome → AutoBuild → Capabilities → Call Now / Schedule → Success. The
|
|
@@ -47,7 +52,19 @@ Every campaign is a **serializable data bundle** (`CampaignBundle`) keyed by a s
|
|
|
47
52
|
`slug`. **Campaign content is authored by the marketing team in HubSpot** and fetched **by
|
|
48
53
|
slug at runtime, server-side**, then mapped to a `CampaignBundle`. Content is **not** stored
|
|
49
54
|
in the repo — a new campaign or a copy edit ships with **no developer and no redeploy**,
|
|
50
|
-
only a HubSpot edit.
|
|
55
|
+
only a HubSpot edit. There is **no hardcoded/in-code interim**: content is HubSpot-owned and
|
|
56
|
+
fetched by slug from the start (an in-code interim was considered and rejected).
|
|
57
|
+
|
|
58
|
+
**Content store decided: HubSpot (over Contentful).** Rationale: (a) marketing already
|
|
59
|
+
operates in HubSpot; (b) the campaign model already exists there — the old BDR site (`info`)
|
|
60
|
+
uses HubSpot for lead sourcing, personalization, and campaign **attribution**
|
|
61
|
+
(`hsContactId` / `hsCampaignId` → Toga), so content keys off that same campaign concept and
|
|
62
|
+
content + attribution stay in one system; (c) the API integration is trivial — a ~15-line
|
|
63
|
+
server-side fetch proxied by a Next route handler (proven in `info`; same pattern serves
|
|
64
|
+
HubDB); (d) no new vendor to contract. **Trade-off accepted:** HubSpot is not a
|
|
65
|
+
purpose-built content CMS, so richly nested content and non-dev editor UX are weaker than
|
|
66
|
+
Contentful — acceptable here because the content shape is **stable / not frequently
|
|
67
|
+
restructured** (a headless CMS would only pay off if the model were rich and churning).
|
|
51
68
|
|
|
52
69
|
Config-driven, per TOGA precedent (`toga25-supply`'s `useClientFields`,
|
|
53
70
|
`toga2-commerce`'s Cart C1–C7): components are **pure renderers of a bundle**; there is
|
|
@@ -141,6 +158,12 @@ right campaign for logging.
|
|
|
141
158
|
`var(--accent)` (+ derived `--accent-fill`); never hardcode an accent value.
|
|
142
159
|
|
|
143
160
|
## Change history
|
|
161
|
+
- 2026-07-13 — Draft 2 resync to the current plan: framework locked to Next.js App Router
|
|
162
|
+
(earlier React/Vite SPA idea dropped); recorded the **HubSpot-over-Contentful** content-store
|
|
163
|
+
decision + rationale (marketing already in HubSpot, campaign model already exists, trivial
|
|
164
|
+
API, no new vendor; stable-content-shape trade-off accepted); reinforced no-hardcoding
|
|
165
|
+
(HubSpot-owned, fetched by slug from the start); confirmed same-product / front-door framing
|
|
166
|
+
(not standalone). Still a draft. (tcox)
|
|
144
167
|
- 2026-07-08 — Initial doc: Next.js App Router web funnel as the AI-BDR product's UI /
|
|
145
168
|
lead-capture front door; config-driven campaign bundle authored in HubSpot and fetched by
|
|
146
169
|
slug server-side at runtime (`DEFAULT` fallback, URL-driven per-request selection); backend
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| [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 |
|
|
8
8
|
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
9
9
|
| [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
|
|
10
|
+
| [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
10
11
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
11
12
|
| [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 |
|
|
12
13
|
| [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/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, library/app/client/compass.php |
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-07-13
|
|
10
|
+
owners: ["jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/PurchaseOrder.php
|
|
13
13
|
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
@@ -40,6 +40,15 @@ production data-integrity bug.
|
|
|
40
40
|
`createdFromSalesOrderItem`, and deliberately **aligns** SO and PO `lineNumber`s. It does
|
|
41
41
|
NOT build the item bridge — so the `postPost` lineNumber-match pass is the **only** (and
|
|
42
42
|
correct) MR linking mechanism.
|
|
43
|
+
- **MA orders — item population in the ODP PO import cron:** the 3a cron
|
|
44
|
+
(`worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`) parses ODP
|
|
45
|
+
EDI 850 POs. Its "Non-SA order handling" block creates the Compass **MA** sales order. Line
|
|
46
|
+
items are attached to the Compass SO **only in the new-SO branch**, from `$purchaseOrder['items']`
|
|
47
|
+
(`partNumber` + `UUID_ITEM_CATALOG` Compass catalog + `qtyOrdered` + `unitPrice`), mirroring the
|
|
48
|
+
ODP-facing SO item payload built lower in the same cron. The item-linking block that maps ODP
|
|
49
|
+
items back to the Compass side runs only `if (… == 'SA' || … == 'MR')` — MA is excluded there
|
|
50
|
+
because SA/MR originate in Compass (their Compass SO+items pre-exist) whereas MA is
|
|
51
|
+
ODP-originated, so nothing else would populate it.
|
|
43
52
|
|
|
44
53
|
## Data model
|
|
45
54
|
- `SalesOrderItems_PurchaseOrderItems` (`Client_Compass`): `uuid`, `salesOrderItemId`,
|
|
@@ -115,7 +124,22 @@ USA and Canada share the parent handler unchanged.
|
|
|
115
124
|
off-by-one bridge fingerprint (a `SalesOrderItems_PurchaseOrderItems` PO item linked to two
|
|
116
125
|
adjacent SO items). Only SA132763 was remediated; a broader reviewed backfill is pending.
|
|
117
126
|
|
|
127
|
+
- **Empty MA orders (fixed 2026-07-13 — forward only).** The 3a cron's new-SO branch created MA
|
|
128
|
+
Compass sales orders with **no** `salesOrderItems`, because item population lived only on the
|
|
129
|
+
ODP-facing SO and the back-linking block excludes MA (`if (… == 'SA' || … == 'MR')`). Empty MA
|
|
130
|
+
(and MR) shells then stick at "Pending Initial Approval" because Compass `_status` gates on
|
|
131
|
+
approvals first — see
|
|
132
|
+
[MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md). **Fix:** build a
|
|
133
|
+
`salesOrderItems` array from `$purchaseOrder['items']` in the new-SO branch (partNumber +
|
|
134
|
+
`UUID_ITEM_CATALOG` + qtyOrdered + unitPrice), mirroring the ODP SO item payload. Only the
|
|
135
|
+
new-SO branch changed; the SO-already-exists branch was intentionally left alone. `php -l`
|
|
136
|
+
passes. This stops NEW empty MA orders; it does **not** repair the 192 historical empties (which
|
|
137
|
+
need item backfill from the sibling ODP SO first).
|
|
138
|
+
|
|
118
139
|
## Change history
|
|
140
|
+
- 2026-07-13 — Fixed empty MA orders: the 3a ODP-PO-import cron's new-SO branch now builds
|
|
141
|
+
`salesOrderItems` from the 850 PO items (the MA back-linking block excludes MA by design).
|
|
142
|
+
Forward-only; 192 historical empties still need item backfill. (rgirish)
|
|
119
143
|
- 2026-06-16 — Found and fixed a SECOND source of cross-part bridge links: the Office Depot
|
|
120
144
|
worker cron `3a_import_office_depot_purchase_orders.php` paired API-returned PO items to SO
|
|
121
145
|
items by array index; now pairs by lineNumber. Remediated SA132763's spurious bridge +
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass MR/MA Order Auto-Approval & Status Gate
|
|
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-13
|
|
10
|
+
owners: ["rgirish"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Compass/SalesOrder.php
|
|
13
|
+
- _underscore/Model/Compass/PurchaseOrder.php
|
|
14
|
+
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
15
|
+
related:
|
|
16
|
+
- mits-po-to-so-item-linking.md
|
|
17
|
+
- ../workflows/odp-order-pipeline-to-netsuite.md
|
|
18
|
+
- ../workflows/order-lifecycle-and-data-integrity.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI
|
|
24
|
+
pipeline (they do not originate as user-entered SA orders) and must **skip human approval
|
|
25
|
+
entirely**. Compass's `_status` calculated field gates on **approvals FIRST**: if any stage of
|
|
26
|
+
the active `ApprovalTemplate` has no `ApprovalDecision`, `_status` returns the pending-stage slug
|
|
27
|
+
(`pendingInitialApproval`) *before* it ever evaluates fulfillment. So an MR/MA order that has an
|
|
28
|
+
`Approval` row but no decisions is stuck at "Pending Initial Approval" regardless of shipment.
|
|
29
|
+
|
|
30
|
+
Two independent root causes produced the stuck-order population, and both are addressed at the
|
|
31
|
+
source (auto-approval + item population), **not** in the status SQL.
|
|
32
|
+
|
|
33
|
+
## Key files / entry points
|
|
34
|
+
- **`_underscore/Model/Compass/SalesOrder.php`**
|
|
35
|
+
- `_status` calculated field — approval gate runs before the shipped-only fulfillment machine
|
|
36
|
+
(see [IF stage lifecycle & order status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md)
|
|
37
|
+
for the fulfillment half).
|
|
38
|
+
- `postPost` — the `if ($isMrOrder)` block loops `foreach ([1,2] as $step)` and issues two
|
|
39
|
+
`internalApiRequest('POST', '/approval-decisions', …)` with `isApproved=true`,
|
|
40
|
+
note `'Auto-approved: MR order'`. **Went live 2026-06-03.**
|
|
41
|
+
- **`_underscore/Model/Compass/PurchaseOrder.php`** — MR handling (`handleMrOrder`); MR/MA order
|
|
42
|
+
detection.
|
|
43
|
+
- **`worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`** — the
|
|
44
|
+
hourly ODP 850 import that creates MA orders (see the item-population fix in
|
|
45
|
+
[MITS PO → SO Item Linking](mits-po-to-so-item-linking.md)).
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
### The approval gate (why empty + un-decided orders stick)
|
|
49
|
+
Prod `Client_Compass` has one active `ApprovalTemplate` (id 1, `salesOrders`) with two stages —
|
|
50
|
+
stage 1 = `step1` = `pendingInitialApproval`, stage 2 = `step2` = `pendingApproval`;
|
|
51
|
+
`APPROVAL_DECISION_TYPE = 1`. The base `_Model_Client_SalesOrder::postPost` creates an `Approval`
|
|
52
|
+
row on **any** order whenever an active template exists. Compass `_status` then requires an
|
|
53
|
+
`ApprovalDecision` for every active-template stage before it will look at fulfillment. No
|
|
54
|
+
decision on a stage ⇒ `_status` short-circuits to that stage's slug.
|
|
55
|
+
|
|
56
|
+
### Auto-approval (the intended MR path)
|
|
57
|
+
`_Model_Compass_SalesOrder::postPost` auto-approves MR orders by POSTing an approval decision for
|
|
58
|
+
each of the two stages (`isApproved=true`). `_status` only checks `isApproved` on the decision —
|
|
59
|
+
never the `assignedToUserId` / `decidedByUserId` columns — so a system decision with NULL user
|
|
60
|
+
columns is fully valid.
|
|
61
|
+
|
|
62
|
+
## Client variations
|
|
63
|
+
- **MR** orders originate in Compass; their Compass SO + items pre-exist. Auto-approval covers them
|
|
64
|
+
from 2026-06-03 onward.
|
|
65
|
+
- **MA** orders are **Office-Depot-originated** (created by the 3a cron) and were **never**
|
|
66
|
+
auto-approved — the `isMrOrder` check only matches numbers starting `MR`, so MA falls through
|
|
67
|
+
the gate. MA orders were additionally created as **empty shells** (see below).
|
|
68
|
+
|
|
69
|
+
## Gotchas / known issues
|
|
70
|
+
- **The stuck orders are EMPTY (zero `SalesOrderItems`).** All stuck MR+MA orders (and their linked
|
|
71
|
+
Compass PO) have no line items — the real line items live on a sibling **Office-Depot-facing SO**
|
|
72
|
+
(`customerId = OfficeDepot`) reachable via `SalesOrders_PurchaseOrders → PurchaseOrders_SalesOrders`.
|
|
73
|
+
A fulfilled-short-circuit in `_status` therefore does **nothing** for them (nothing to fulfill).
|
|
74
|
+
- **Do NOT "fix" this in `_status`.** A speculative fulfilled-short-circuit (plus an extracted
|
|
75
|
+
`_isFullyFulfilledSql` helper) was added and then **reverted** this session: because the stuck
|
|
76
|
+
orders are empty, the short-circuit never fires. The real fix is item population + approval
|
|
77
|
+
decisions at the source.
|
|
78
|
+
- **MA is still creating stuck orders until the item-population cron fix ships** — see
|
|
79
|
+
[MITS PO → SO Item Linking](mits-po-to-so-item-linking.md). Auto-approval for MA is not yet
|
|
80
|
+
implemented (the `isMrOrder` check does not match `MA`).
|
|
81
|
+
|
|
82
|
+
## Prod status buckets (2026-07-13)
|
|
83
|
+
| Type | NO_APPROVAL (correct — pre-template, gate falls through) | APPROVAL_NO_DECISION (STUCK) | FULLY_APPROVED |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| MR | 5922 (Sep 2025–Jan 2026) | 2927 | 583 |
|
|
86
|
+
| MA | 176 | 192 | 0 |
|
|
87
|
+
|
|
88
|
+
- The 5922 MR + 176 MA `NO_APPROVAL` orders pre-date the active template; they have **no**
|
|
89
|
+
`Approval` row, so `_status` skips the gate and reads correctly. They must **NOT** get an
|
|
90
|
+
`Approval` created.
|
|
91
|
+
- Total stuck: **3119** (2927 MR + 192 MA). A CSV of all 3119 (type, orderNumber, orderUuid,
|
|
92
|
+
dateCreated, compassPurchaseOrder, officeDepotSalesOrder_sourceOfItems) was handed to
|
|
93
|
+
ops/integration for remediation.
|
|
94
|
+
|
|
95
|
+
## Historical MR remediation (approval-decision backfill)
|
|
96
|
+
A one-off, idempotent, transaction-wrapped SQL backfill unsticks the 2927 historical MR orders by
|
|
97
|
+
inserting the two auto-approval `ApprovalDecisions` (one per stage, mirroring `postPost`'s
|
|
98
|
+
`foreach([1,2])`). Each row: `approvalTemplateStageId` 1/2, `approvalDecisionTypeId=1`,
|
|
99
|
+
`isApproved=1`, `assignedToUserId`/`decidedByUserId` NULL, note
|
|
100
|
+
`'Auto-approved: MR order (historical backfill)'`, `dtDecision NOW()`.
|
|
101
|
+
|
|
102
|
+
Safe scoping baked in — **decisions only, never Approvals**:
|
|
103
|
+
- **`INNER JOIN Approvals` (`recordId=14`)** so it can only add decisions to orders that *already*
|
|
104
|
+
have an `Approval`. Structurally it cannot touch the `NO_APPROVAL` orders (which are already
|
|
105
|
+
correct and must not get an Approval).
|
|
106
|
+
- **Per-stage `NOT EXISTS`** ⇒ idempotent / re-runnable.
|
|
107
|
+
- Includes a PREVIEW count query (prod: 2927 orders → 2927 step-1 + 2927 step-2 rows). Workflow:
|
|
108
|
+
run on beta, verify, COMMIT; then production.
|
|
109
|
+
- Does **not** create Approvals; does **not** handle MA (MA orders are empty and need item backfill
|
|
110
|
+
from the sibling ODP SO first).
|
|
111
|
+
|
|
112
|
+
## Change history
|
|
113
|
+
- 2026-07-13 — Diagnosed the MR/MA "Pending Initial Approval" stuck-order bug: `_status` gates on
|
|
114
|
+
approvals before fulfillment; MR orders created 2026-02-04..2026-06-02 got an Approval but no
|
|
115
|
+
decisions (auto-approval only live from 2026-06-03), and MA orders were never auto-approved and
|
|
116
|
+
are empty shells. Reverted a speculative `_status` fulfilled-short-circuit (stuck orders are
|
|
117
|
+
empty, so it never fires). Built an idempotent, decisions-only MR approval-decision backfill
|
|
118
|
+
(2927 orders) scoped by `INNER JOIN Approvals` so it can never create an Approval, and a CSV of
|
|
119
|
+
all 3119 stuck orders for ops. MA item population fixed separately at the 3a cron. (rgirish)
|
|
120
|
+
|
|
121
|
+
## Related docs
|
|
122
|
+
- [Compass MITS PO → SO Item Linking](mits-po-to-so-item-linking.md) — the 3a cron item-population fix that stops NEW empty MA orders.
|
|
123
|
+
- [Compass ODP Order Pipeline to NetSuite](../workflows/odp-order-pipeline-to-netsuite.md)
|
|
124
|
+
- [IF Stage Lifecycle & Order Status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md) — the shipped-only fulfillment half of Compass `_status`.
|
package/package.json
CHANGED