# Deal Intelligence

> Track which objections lead to wins vs losses

**Created:** Jan 20, 2026
**Status:** Complete ✅
**Built:** Jan 21, 2026
**Effort:** 8-12 hours
**Ongoing Cost:** $0 (uses existing data)

---

## Problem

We don't know:
- Which objections are deal-killers vs speed bumps?
- What responses actually lead to conversions?
- Why do some deals stall while others close fast?
- What's the winning pattern for each trade/lead type?

---

## Solution

Track the full deal journey and analyze patterns that lead to wins.

### Deal Journey Tracking

```
Lead Created → First Contact → Discovery → Objections → Proposal → Close/Lost
     ↓              ↓             ↓           ↓           ↓
   Source       Response      Pain Points  Responses   Outcome
   Score        Time          Needs        Used        Reason
```

### Intelligence Outputs

1. **Objection Impact Analysis**
   - "Budget" objection: 45% still convert (speed bump)
   - "Using competitor" objection: 12% convert (deal killer)

2. **Winning Response Patterns**
   - When they say "too expensive" → leads who heard "exclusive leads save you time" converted 3x more

3. **Deal Velocity Insights**
   - Average time to close: 4.2 days
   - Deals that close in <2 days have 80% higher value

4. **Stall Predictors**
   - 3+ touches without response = 70% chance of loss
   - No callback scheduled = 60% likely to stall

---

## Database Schema

```sql
-- Deal stages tracking
CREATE TABLE IF NOT EXISTS deal_stages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  lead_id UUID REFERENCES leads(id) ON DELETE CASCADE,
  stage VARCHAR(50) NOT NULL,
  -- 'new', 'contacted', 'discovery', 'proposal', 'negotiation', 'won', 'lost'
  entered_at TIMESTAMPTZ DEFAULT NOW(),
  exited_at TIMESTAMPTZ,
  duration_hours INTEGER,
  notes TEXT,
  metadata JSONB
);

-- Objection outcomes (extends existing objection_responses)
ALTER TABLE objection_responses ADD COLUMN IF NOT EXISTS
  led_to_conversion BOOLEAN;
ALTER TABLE objection_responses ADD COLUMN IF NOT EXISTS
  lead_final_outcome VARCHAR(50);
ALTER TABLE objection_responses ADD COLUMN IF NOT EXISTS
  days_to_outcome INTEGER;

-- Win/Loss reasons
CREATE TABLE IF NOT EXISTS deal_outcomes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  lead_id UUID REFERENCES leads(id) ON DELETE CASCADE UNIQUE,
  outcome VARCHAR(20) NOT NULL, -- 'won', 'lost', 'stalled'
  primary_reason VARCHAR(100),
  secondary_reasons TEXT[],
  competitor_lost_to VARCHAR(100),
  deal_value DECIMAL(10,2),
  time_to_close_days INTEGER,
  touchpoints_count INTEGER,
  objections_encountered TEXT[],
  winning_response TEXT,
  rep_notes TEXT,
  recorded_at TIMESTAMPTZ DEFAULT NOW(),
  recorded_by UUID REFERENCES auth.users(id)
);

-- Indexes
CREATE INDEX idx_deal_stages_lead ON deal_stages(lead_id);
CREATE INDEX idx_deal_outcomes_outcome ON deal_outcomes(outcome);
CREATE INDEX idx_deal_outcomes_reason ON deal_outcomes(primary_reason);
```

---

## Win/Loss Capture Flow

When lead status changes to 'converted' or 'lost':

```javascript
// Modal prompts rep for outcome details
const WinLossModal = {
  won: {
    questions: [
      "What was the deciding factor?",
      "Which objection response worked best?",
      "Deal value estimate?"
    ]
  },
  lost: {
    questions: [
      "Primary reason for loss?",
      "Lost to competitor?",
      "Could we have done something differently?"
    ],
    reasons: [
      "Price/Budget",
      "Chose Competitor",
      "Timing Not Right",
      "No Response",
      "Not a Fit",
      "Internal Decision",
      "Other"
    ]
  }
};
```

---

## Analysis Engine

```javascript
// src/services/dealIntelligence.js

async function analyzeObjectionImpact() {
  // Query all objections and their final outcomes
  const objections = await supabase
    .from('objection_responses')
    .select('objection_type, response_used, led_to_conversion')
    .not('led_to_conversion', 'is', null);

  // Calculate conversion rate per objection type
  const impact = {};
  for (const obj of objections) {
    if (!impact[obj.objection_type]) {
      impact[obj.objection_type] = { total: 0, converted: 0 };
    }
    impact[obj.objection_type].total++;
    if (obj.led_to_conversion) {
      impact[obj.objection_type].converted++;
    }
  }

  // Sort by conversion rate
  return Object.entries(impact)
    .map(([type, data]) => ({
      objection: type,
      conversionRate: data.converted / data.total,
      total: data.total,
      classification: data.converted / data.total > 0.3 ? 'speed_bump' : 'deal_killer'
    }))
    .sort((a, b) => b.conversionRate - a.conversionRate);
}

async function findWinningResponses(objectionType) {
  const responses = await supabase
    .from('objection_responses')
    .select('response_used, led_to_conversion')
    .eq('objection_type', objectionType)
    .eq('led_to_conversion', true);

  // Group and count
  const winning = {};
  for (const r of responses) {
    winning[r.response_used] = (winning[r.response_used] || 0) + 1;
  }

  return Object.entries(winning)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5);
}

async function predictStall(leadId) {
  const lead = await getLead(leadId);
  const comms = await getCommunications(leadId);

  const signals = [];

  // No response in 3+ days
  const daysSinceResponse = daysSince(lead.last_inbound_at);
  if (daysSinceResponse > 3) {
    signals.push({ risk: 'high', reason: `No response in ${daysSinceResponse} days` });
  }

  // No callback scheduled
  const hasCallback = await hasActiveCallback(leadId);
  if (!hasCallback) {
    signals.push({ risk: 'medium', reason: 'No callback scheduled' });
  }

  // Multiple unanswered outreach
  const unanswered = comms.filter(c => c.direction === 'outbound' && !c.response_id);
  if (unanswered.length >= 3) {
    signals.push({ risk: 'high', reason: `${unanswered.length} unanswered messages` });
  }

  return {
    stallProbability: calculateProbability(signals),
    signals,
    recommendation: getRecommendation(signals)
  };
}
```

---

## API Endpoints

```
GET  /api/deals/intelligence/objections    # Objection impact analysis
GET  /api/deals/intelligence/responses     # Winning responses
GET  /api/deals/intelligence/velocity      # Deal velocity stats
GET  /api/deals/intelligence/stall/:leadId # Stall prediction
GET  /api/deals/outcomes                   # Win/loss summary
POST /api/deals/outcomes                   # Record outcome
GET  /api/deals/outcomes/reasons           # Loss reason breakdown
GET  /api/deals/outcomes/patterns          # Winning patterns
```

---

## UI Components

### DealIntelDashboard.jsx
- Objection impact chart (bar chart)
- Win/Loss funnel visualization
- Average deal velocity
- Top winning responses

### StallWarning.jsx
- Shows on Lead Profile when stall detected
- Amber/Red warning based on probability
- Suggested actions

### WinLossModal.jsx
- Triggered on status change to converted/lost
- Captures outcome details
- Quick select for common reasons

### ObjectionInsightCard.jsx
- Shows in LiveCopilot when objection detected
- "This objection converts 45% of the time"
- "Best response: ..."

---

## Implementation Steps

### Phase 1: Database + Tracking (3 hours)
1. [x] Create migrations
2. [x] Add stage tracking triggers
3. [x] Link objections to outcomes

### Phase 2: Capture Flow (2-3 hours)
1. [x] Create WinLossModal
2. [ ] Hook into status change (frontend integration)
3. [x] Store outcome details

### Phase 3: Analysis Engine (3-4 hours)
1. [x] Implement objection analysis
2. [x] Implement winning response finder
3. [x] Implement stall predictor

### Phase 4: Frontend (2-3 hours)
1. [x] Create DealIntelDashboard
2. [x] Create StallWarning component
3. [x] Create ObjectionInsightCard

---

## Weekly Insights

Auto-generate weekly deal intelligence report:
- Top 3 objections this week
- Response that worked best
- Deals at risk of stalling
- Win rate by lead source

---

## Success Metrics

- Win/loss recorded on 90% of closed deals
- Objection response effectiveness tracked
- Stall predictions accurate >70%
- Win rate improves 10% with insights
