Skip to content

Repository files navigation

RevOps Forecast

CRM-agnostic revenue forecasting on a normalized pipeline schema.

Most revenue teams can't answer "what will we actually close this quarter?" without trusting a single CRM's built-in roll-up — and the moment data lives in two systems (a Salesforce org here, a HubSpot portal from an acquisition there, a partner's CSV exports), there is no shared way to forecast revenue or judge pipeline health at all. Forecast logic gets welded to one vendor's field names and becomes unportable.

This project solves that by splitting the problem in two:

  1. A normalized opportunity schema that any CRM's export or API maps onto with a small, declarative field-mapping config — Salesforce and HubSpot mappings ship as working examples.
  2. A forecasting engine of pure functions that only ever sees the normalized schema: three forecasting methodologies plus a blended ensemble, pipeline-health metrics (coverage, slip, conversion, win rates), and a data hygiene score.

An animated dashboard (anime.js v4) sits on top so the numbers read like a product, not a notebook.

Dashboard, light theme Forecast methods with confidence band, dark theme

Data & Privacy — every record in this repository is synthetic, generated by a seeded script (scripts/generate_sample_data.py). No customer, employer, or otherwise proprietary data appears anywhere. Company names are invented; resemblance to real organizations is coincidental.

Quickstart

One command (frontend build + API + sample data included):

docker compose up --build
# → http://localhost:8000

Local development:

# backend (Python 3.12+, uv)
cd backend
uv sync
REVOPS_AS_OF=2026-07-23 REVOPS_AUTOLOAD_SAMPLE=1 uv run uvicorn app.main:app --reload

# frontend (Node 22) — proxies /api to :8000
cd frontend
npm install
npm run dev

# tests
cd backend && uv run pytest

REVOPS_AS_OF pins "today" so the bundled dataset (generated around 2026-07-23) demos meaningfully at any date; drop it for live use.

The normalized schema

Every importer produces Opportunity records in this shape — the common denominator across major CRMs, not any one vendor's field list:

Field Notes
opportunity_id, account_name identity
deal_amount, currency currency is carried, never converted
stage normalized 7-stage funnel: Prospecting → Qualification → Proposal → Negotiation → Commit → Closed Won / Closed Lost
forecast_category Pipeline / Best Case / Commit ("rep has a verified close plan and date — not a hope, a plan") / Closed
close_date, created_date, last_stage_change_date ISO dates; MM/DD/YYYY accepted on import
original_close_date (optional) first committed close date — powers slip-rate
probability 0–1 (0–100 inputs normalized); falls back to stage defaults
owner_id, rep_name, team, region ownership
segment SMB / Mid-Market / Enterprise
deal_type New Business / Renewal / Expansion
next_step (optional) absence is a hygiene flag

Forecasting methods

All methods are pure functions in backend/forecasting/ — stdlib only, no framework imports, unit-tested against hand-computed golden values.

1. Weighted pipeline — the expected value of the open pipeline due in the period, plus what already closed-won inside it:

forecast = closed_won + Σ (deal_amount × probability)   over open deals due in period

2. Forecast category rollup — the classic sales-leadership call, weighting rep-committed categories (defaults: Closed ×1.0, Commit ×0.9, Best Case ×0.5, Pipeline ×0):

forecast = closed_won + 0.9·commit + 0.5·best_case

3. Historical trend / regression — what history says, independent of rep judgment: a least-squares trend over monthly closed-won revenue projected through the quarter (pro-rated for the elapsed month), averaged with a win-rate estimate (due pipeline × trailing win rate + closed-won QTD). Degrades gracefully: with under 3 months of history it falls back to the win-rate estimate, and with no history at all to the weighted forecast — with an explicit warning either way.

4. Ensemble — a weighted blend (0.35 / 0.40 / 0.25, renormalized if trend is degraded) with a confidence band spanning the methods' disagreement ±7.5%. Methods agreeing tightly ⇒ narrow band.

Supporting metrics

GET /api/metrics exposes the standard RevOps dashboard set: coverage ratio (open pipeline due ÷ remaining target), slip rate (share of open deals whose close date pushed past the original), stage-to-stage conversion (snapshot estimate over open + won deals — flat exports don't say where lost deals died, so this is documented as an optimistic approximation), win rate by segment and by rep (count- and amount-based), and average sales cycle.

Data hygiene score

Each open deal starts at 100 and loses points for standard pipeline-hygiene violations: stale stage (>45 days without movement, >30 for Negotiation/Commit, −30), overdue close date (−30), missing next step (−20), slipped close date (−10), probability far from what the stage implies (−10). The dashboard surfaces the aggregate score, flag counts, and the worst deals.

How to connect your real CRM

The app never talks to a CRM directly — it consumes exports/API payloads through a mapping config (mappings/*.json|yaml) that declares how native fields translate:

{
  "name": "salesforce",
  "field_map":   { "StageName": "stage", "Amount": "deal_amount", "CloseDate": "close_date" },
  "stage_map":   { "Proposal/Price Quote": "Proposal", "Negotiation/Review": "Negotiation" },
  "category_map":{ "Omitted": "Pipeline" },
  "defaults":    { "currency": "USD" }
}
  • Salesforce: export an Opportunity report (or query the REST API) with the columns in mappings/salesforce.json, then POST /api/data/upload with mapping=salesforce. Probabilities arrive 0–100 and are normalized; Omitted forecast category folds into Pipeline.
  • HubSpot: export deal properties (or pull the CRM API) per mappings/hubspot.json — internal dealstage codes map to the normalized funnel; swap in your pipeline's stage IDs.
  • Anything else: copy mappings/generic_csv.yaml and fill in your field names. Bad rows are reported per-row, never fatal.

A mock CRM API client (backend/app/mock_crm.py) demonstrates the API-pull integration shape — paginated fetches feeding the same importer. A real connector replaces one class; nothing downstream changes. The CRM Mapping tab animates exactly how each native field lands in the normalized schema, and can import tiny demo exports through each mapping live.

Architecture

flowchart LR
    subgraph sources [Data sources]
        CSV[CSV / JSON export]
        MOCK[Mock CRM API client]
    end
    subgraph backend [FastAPI backend]
        MAP[mapping configs] --> IMP[importer]
        CSV --> IMP
        MOCK --> IMP
        IMP --> STORE[(in-memory store)]
        STORE --> ENG[forecasting/ - pure functions]
        ENG --> API[REST endpoints /api/*]
    end
    API --> UI[Vite + anime.js dashboard]
Loading
  • backend/forecasting/ — schema + engine, zero web-framework imports
  • backend/app/ — FastAPI layer: importer, store, mock CRM, routers
  • frontend/ — vanilla JS + Vite; all motion honors prefers-reduced-motion, animates only transform/opacity, and imports just the anime.js modules used
  • Storage is deliberately in-memory: this is an analysis tool, not a system of record — data reloads from upload/sample/sync on demand

API reference

Endpoint Purpose
POST /api/data/sample load bundled synthetic dataset
POST /api/data/upload multipart CSV/JSON + mapping form field
POST /api/data/crm-sync pull via the mock CRM client
GET /api/data/opportunities list (filter by stage, segment)
GET /api/data/hygiene hygiene score + flagged deals
GET /api/mappings, GET /api/mappings/{name} mapping configs
GET /api/forecast, GET /api/forecast/{weighted|category|trend|ensemble} forecasts
GET /api/metrics coverage, slip, conversion, win rates
GET/PUT /api/targets revenue targets per quarter
GET /api/meta as-of date, data source, deal count

Interactive docs at /docs (FastAPI/OpenAPI).

What this demonstrates

  • RevOps data modeling — a normalized schema designed from the common denominator of real CRMs, with the judgment calls (slip-rate needs original_close_date; conversion from snapshots is an approximation) documented rather than hidden.
  • Forecasting methodology — three independent methods with different failure modes, composed into an ensemble with an honest uncertainty band and explicit degradation when history is thin.
  • CRM integration design — declarative field mappings + one importer, so adding a CRM is config, not code.
  • Full-stack engineering — a tested pure-function engine (47 pytest tests), a thin typed API, a hand-built animated frontend with a validated accessible palette, one-command Docker deployment.

License

MIT — see LICENSE.

About

CRM-agnostic revenue forecasting: normalized pipeline schema, multi-method forecast engine, animated RevOps dashboard (FastAPI + anime.js)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages