# Sales Intelligence Dashboard — Architecture & Data Flows

> Status: as-built. Implements the business goals of the *Sales Intelligence* spec using
> repo-native patterns (not the spec's AWS-specific tech plan).
> Companion docs: [architecture-rag-system.md](architecture-rag-system.md) (the chat/KB base this
> sits on) and the Chinese implementation plan [sales-intelligence-plan.md](sales-intelligence-plan.md).

## 1. What this system is

A read-only intelligence surface for the Head of Sales (CEO/COO read-only) that fuses data
scattered across **HubSpot / Fathom / Fireflies / Redmine / Gmail** into:

1. **Daily Pipeline Report** — a browser dashboard with 6 sections (Executive Summary, Rep
   Activity, Top-5 Action List, Deal Risk Radar, Deal Pulse, Pipeline vs Target, Upsell).
2. **Conversational Sales AI** — embedded chat grounded in the real numbers (system-of-record).
3. **@SilkSales Slack bot** — mid-day proactive push of critical signals + on-demand DM Q&A.
4. **Demo mode** — a client-side redaction toggle that masks real company/customer/rep names and
   amounts for external demos.

Design stance: **deterministic aggregation first, LLM second.** Numbers come from read-time
queries over mirror tables; LLMs only *narrate, rank, and classify* on top — never invent figures.

## 2. Relationship to the RAG base

The Sales system reuses the RAG infrastructure rather than duplicating it:

| Need | Reused from RAG base |
|---|---|
| HubSpot/Fathom/Fireflies/Redmine/Gmail data | The same ingest crons + Postgres mirror tables (§3 of the RAG doc) |
| Conversational answers | `chat/service.ts` with `surface: 'sales'` |
| Proactive bot | A second Slack bot instance (`salesSlackService`) over the same `slack/service.ts` |
| Inference | The same Bedrock Converse path (Nova Pro default) used by narrative/eval |

What's **new** for Sales is a deterministic analytics layer + an LLM agent layer + a separate
auth portal, all under `api/src/core/{pipeline,customers,salesreport,salesanalytics,sales,...}`.

## 3. Architecture

```
   HubSpot ─┐  Fathom ─┐  Fireflies ─┐  Redmine ─┐  Gmail ─┐   (RAG ingest crons)
            ▼          ▼             ▼           ▼         ▼
        ┌──────────────────────── PostgreSQL mirror ───────────────────────────┐
        │ hubspot_deals/companies/contacts + _deal_snapshots                    │
        │ redmine_projects (+ hubspotCompanyId/Deal, parentId, customerMatch)   │
        │ fathom_meetings / fireflies_meetings (attendeeEmails, externalDomains) │
        │ email_messages · reps · sales_settings · sales_reports                 │
        │ risk_flags · exec_actions · upsell_cards                               │
        └───────────────┬───────────────────────────────────────┬──────────────┘
   customer linking ────┘                  deterministic reads   │
   (customers/service autoMatch)                                 ▼
                                          ┌────────────────────────────────────┐
                                          │ pipeline/  salesanalytics/  facts   │
                                          │ (read-time aggregation, no LLM)     │
                                          └───────┬───────────────────┬─────────┘
                          report runner fans out  │                   │ buildSalesFacts()
                                                  ▼                   ▼
                                      ┌───────────────────┐   ┌──────────────────┐
                                      │ salesreport/agents│   │  Sales Chat       │
                                      │ dealRisk·execAction│  │ (surface:'sales') │
                                      │ upsell·narrative   │  └──────────────────┘
                                      └─────────┬──────────┘
                                  persist → sales_reports + risk/exec/upsell tables
                                                │
                  ┌─────────────────────────────┼──────────────────────────────┐
                  ▼                              ▼                              ▼
            /sales Daily Report          @SilkSales push (mid-day)       @SilkSales DM Q&A
            (React, _sales/ui)           salesreport/push.ts             slack sales bot
```

## 4. Data foundation

### 4.1 Customer linking (M1) — `customers/`
The join between CRM and delivery. `redmine_projects` carries `hubspotCompanyId` /
`hubspotDealId` / `customerMatch` / `customerMatchedAt` / `parentId`.

- **`autoMatch`** matches a HubSpot company to a Redmine project by name (exact + suffix-stripped
  "normalized"/fuzzy), and walks the project hierarchy so a subproject inherits its parent's
  company (`MatchType = exact | normalized | inherited | manual`). Runs automatically after every
  HubSpot/Redmine sync (write-only-on-change, so it's cheap per tick).
- **Match durability convention** (`customerMatch`):
  - `'manual'` + company set → pinned link (authoritative; auto-match never overrides).
  - `'manual'` + company `null` → durable "not a customer" exclude.
  - `null` → auto (the matcher decides).
- **Review gate:** fuzzy (`normalized`) matches surface in a "Needs review" filter on the HubSpot
  Companies tab (admin-only); `POST /customers/confirm` pins a page of them to `manual`.

### 4.2 Snapshots — `hubspot_deal_snapshots`
The HubSpot ingest upserts one row per `(account, deal, day)`. This is the time-series substrate:
deal history timelines, stage-change detection, and the **velocity / movement** analytics. Value
accrues as snapshots accumulate (metrics degrade gracefully via a `coverage` flag + "still
accumulating" banner until there's enough history).

### 4.3 Cross-source correlation
- Meetings store `attendeeEmails[]` / `externalDomains[]` / `organizerEmail`; HubSpot deals store
  `ownerEmail`. This enables **rep calls-this-week** (owner email ↔ meeting attendees) and
  **customer recent calls** (company domain ↔ meeting `externalDomains`) — deterministically.
  (New columns backfill on the next full sync.)

### 4.4 Roster & settings
- **`reps`** — the active-AE roster (CRUD at `/sales/reps` + HubSpot import). Drives scoping:
  open deals are filtered to roster owners; the rep strip is roster-driven; the Head of Sales is
  excluded from the strip. Graceful fallback: no roster seeded → don't owner-filter.
- **`sales_settings`** — JSON settings incl. quota targets (`TargetSetting`:
  `quarterTarget`/`annualTarget`/`perRep[]`). Read via `getTargetSetting()` (also feeds the
  pipeline-vs-target card — replaced the old hardcoded $2M/$8M). Primed on boot before crons read it.

## 5. Deterministic analytics layer (no LLM)

### 5.1 `pipeline/service.ts` — the live snapshot
Aggregates `hubspot_deals` + customer links into the `PipelineSummary`: open/won/lost, by-stage,
by-owner, existing-vs-prospect split, by-close-quarter (with slippage), **Deal Pulse** table
(filterable; `isStale` ≥30d untouched, `isOverdue` past close date, `daysSinceUpdate`),
**rep activity / health** (Active ≤2d / Quiet 3-4d / Dark ≥5d, "last activity" = max(deal edit,
last call) over 60d; carries `tier`/`callsThisWeek`), **pipeline vs target**, and a **weighted
forecast** with a Fathom-confidence haircut derived from the latest `risk_flags`. Open deals
dormant ≥60d and non-roster owners are excluded from every headline.

### 5.2 `salesanalytics/service.ts` — historical analytics
Owner-scoped (leadership sees all). Endpoints `/sales/analytics/*`:
- **win-loss** (win rate overall + by rep/size-band/existing-vs-prospect, loss-by-furthest-stage),
- **velocity** (conversion funnel + avg time-in-stage + sales cycle, from snapshots),
- **movement** (snapshot-diff feed: won/lost/advanced/slipped/new/close_pushed over a window),
- **trend** (time series from `sales_reports` payloads — leadership only),
- **relationships** (single-thread risk: contacts per deal vs contacts seen on calls),
- **engagement** (per-customer inbound emails + calls, last-touch status — leadership),
- **quota** (current-quarter won vs personal quota — leadership).

## 6. LLM agent layer — `salesreport/`

The **daily report runner** (mirrors the `eval` module) writes one `sales_reports` row per run:
the full deterministic `PipelineSummary` as `payload` JSON + denormalized headlines + deltas vs
the previous SUCCESS report. Cadence: **daily cron** (`SALES_REPORT_CRON=off` to disable) +
manual `POST /sales/report/run` + stale-recovery on boot.

On each run it fans out to `salesreport/agents/`:
- **dealRisk** — classifies the 5 spec risk types (push_risk / competitive / stalled /
  quarter_slip / se_gap), incl. a prospect "gone quiet on calls" stalled signal; writes `risk_flags`.
- **upsell** — `upsell_cards`, incl. `launch_approaching` (Redmine `nextLaunchDate`, or a
  Fathom-extracted verbally-confirmed launch date when Redmine has none).
- **execAction** — deterministic candidate actions → LLM chief-of-staff re-rank/reword (identity
  pinned to candidate index, never fabricated); deterministic top-5 fallback. Writes `exec_actions`.
- **narrative** — Bedrock Converse 2-3 sentence executive summary; **best-effort** (null on
  failure, report still SUCCESS).

All LLM calls go through the same `bedrockRuntimeClient` Converse path; model via
`SALES_NARRATIVE_MODEL_ID` / `SALES_AGENT_MODEL_ID` (default Nova Pro, switch to Claude when unlocked).

## 7. Conversational Sales AI — `surface: 'sales'`

The dashboard chat calls `chat/service.ts` with `surface: 'sales'`, which:
1. Pins the dedicated `@SilkSales` agent (`SALES_AGENT_INTENT`), and
2. Injects `buildSalesFacts()` ([salesreport/facts.ts](../api/src/core/salesreport/facts.ts)) — a
   compact daily snapshot (pipeline, weighted forecast vs target, rep activity, ranked exec
   actions, risk, upsell) as grounding, plus a relevance directive.

So numeric/strategic answers come from the system-of-record, not just KB recall, and the bot
offers the closest relevant info instead of a flat "no records" refusal. Runs in `stream` mode so
the sales instruction is the authoritative system prompt.

## 8. @SilkSales Slack bot

A **separate Slack app/bot** from `@SilkAI` (own token + signing secret:
`SLACK_SALES_BOT_TOKEN` / `SLACK_SALES_SIGNING_SECRET`; webhook `…/api/slack/sales/events`).
`SlackService` is config-driven; the sales instance runs in "sales mode" and answers **everything**
(DM / mention / thread) via the `@SilkSales` agent + daily-snapshot grounding (no classifier).

**Mid-day push** ([salesreport/push.ts](../api/src/core/salesreport/push.ts)) — its own cron —
fires 4 triggers to `SILKSALES_PUSH_CHANNEL`: `quarter_slip`, `blocker` (≥$100K),
`critical_health` (≥$45K call-classified), `exec_flag` (newly in exec_actions vs prior report).
No-ops until the `SLACK_SALES_*` env is set, so existing deploys are unaffected.

## 9. Frontend (`app/src/pages`)

Separate **sales portal** behind Google-SSO (`salesAuth` guard, `sales_users`) — distinct from the
admin app. Shared UI kit `app/src/pages/_sales/ui.tsx` (`PageHero` violet gradient, `Kpi`, `Pill`,
`COLORS`) gives the dashboard its cohesive violet look. Key pages:

| Route | Page | Source |
|---|---|---|
| `/sales` | **Daily Report** (6-section rail) | `Sales/` + `_sales/*` |
| `/sales/pipeline` | Pipeline deep-dive (filterable Deal Pulse) | `Pipeline/` |
| `/sales/analytics` | Win&Loss / Velocity / Trends / Movement / Relationships / Targets | `Analytics/` |
| `/sales/customers` | Existing customers + open pipeline rollup | `Customers/` |
| `/sales/report` | Report history + Generate / Run-push buttons | `SalesReport/` |
| `/sales/reps`, `/sales/settings/targets` | Roster + quota admin | `SalesReps/`, `SalesSettings/` |
| (chat) | Embedded Sales AI | `SalesChat/` |

> Frontend uses **hand-rolled API clients** (`app/src/libs/api/{pipeline,customers,hubspot,
> sales-analytics,salesreport}.ts`) instead of Eden Treaty here, because the full sales surface
> hits Eden's type-instantiation-depth limit. They `unwrapQuery` so both `useListAPI` and direct
> `useAPI` work. After editing API types the frontend consumes via Eden, run `cd api && bunx tsc -b`
> to refresh `api/dist` or the app typecheck sees a stale union.

## 10. Data model (Sales-specific tables)
`sales_users`, `sales_settings`, `reps`, `sales_reports`, `risk_flags`, `exec_actions`,
`upsell_cards`, `hubspot_deal_snapshots`, plus the `redmine_projects` link columns
(`hubspotCompanyId/Deal`, `customerMatch`, `parentId`, `nextLaunchDate`) and the meeting/deal
correlation columns (`attendeeEmails`, `externalDomains`, `ownerEmail`).

## 11. Operational notes
- **HubSpot credential:** requires a **Private App access token** (`pat-…`, scopes
  `crm.objects.{deals,companies,contacts}.read`) — a bare Developer API Key / legacy hapikey does
  **not** work. Until set, the whole intelligence layer runs on empty data.
- Needs `prisma db push` for the sales tables/columns; `prisma generate` after pulling (the
  committed client can be stale). Verify commands: see the `toolchain-verify-commands` memo.
- Models default to Nova Pro; set `SALES_*_MODEL_ID` / `CHAT_MODEL_ID` to a Claude id once the
  Bedrock account has Anthropic unlocked, for best narrative/agent quality.
- New correlation columns (`attendeeEmails`, `ownerEmail`, …) populate on the **next** full sync —
  run a manual Fathom/Fireflies + HubSpot sync to backfill existing rows.

## 12. End-to-end example (a daily report)
1. Daily cron fires `salesReportService` → `pipelineService.summary()` builds the deterministic
   snapshot from `hubspot_deals` + links + meetings (roster-scoped, ≥60d-dormant excluded).
2. Runner fans out Deal Risk + Upsell in parallel, then Exec Action (LLM re-rank), then narrative;
   persists `sales_reports` + `risk_flags`/`exec_actions`/`upsell_cards` with deltas vs the last run.
3. `/sales` renders the 6 sections from that report + live `pipeline/deals` for Deal Pulse.
4. The mid-day push cron compares the new report to the prior one and DMs `@SilkSales` alerts for
   any of the 4 triggers.
5. A rep asks the embedded chat "how far are we from quarter target?" → `surface:'sales'` injects
   `buildSalesFacts()`, and the answer cites the exact weighted forecast vs target from the snapshot.
