# Call List Smart Prioritization - 0.1% Plan

## Overview
Transform the call list from static tier-based sorting to AI-powered, urgency-aware ranking with clear "why call this lead NOW" context on every card.

**Status:** COMPLETE
**Priority:** #1
**Target:** Basic → 0.1%

---

## 0.1% Vision

Every lead in the call list answers: **"Why should I call THIS person RIGHT NOW?"**

Examples:
- "Sarah replied 12 mins ago - strike while hot"
- "Best time to call Mike is NOW (2pm - his conversion hour)"
- "Tony hasn't been contacted in 52 hours - going cold!"
- "Pattern match: Electricians convert 73% - prioritize"
- "3 inbound calls from Lisa - she's chasing YOU"

---

## The Smart Ranking Algorithm

### Priority Score Formula

Each lead gets a `priority_score` (0-1000) computed from weighted factors:

```
priority_score =
  (urgency_score × 0.35) +      // How urgent is action needed?
  (intent_score × 0.25) +        // How interested are they?
  (timing_score × 0.20) +        // Is now the right time?
  (pattern_score × 0.15) +       // Does this match winning patterns?
  (base_score × 0.05)            // Underlying lead score
```

### Factor Breakdowns

#### 1. Urgency Score (0-350 points)

| Condition | Points | Why Badge |
|-----------|--------|-----------|
| Inbound SMS < 15 mins | 350 | "Replied 12 mins ago - strike now!" |
| Inbound SMS 15-60 mins | 300 | "Replied 45 mins ago - still hot" |
| Inbound SMS 1-4 hrs | 250 | "Replied 2 hrs ago - follow up" |
| Inbound SMS 4-24 hrs | 150 | "Replied yesterday - needs response" |
| Inbound call (any) | 300 | "Called you back - high intent!" |
| Multiple inbound calls | 350 | "Called you 3 times - wants you!" |
| Callback overdue | 280 | "Callback 2 hrs overdue" |
| Callback due today | 200 | "Callback scheduled for today" |
| Hot lead stale > 24hrs | 250 | "Hot lead going cold - 36 hrs!" |
| Hot lead stale > 48hrs | 320 | "URGENT: Hot lead 52 hrs stale!" |

#### 2. Intent Score (0-250 points)

| Condition | Points | Why Badge |
|-----------|--------|-----------|
| is_high_intent = true | 250 | "High intent - called you 3+ times" |
| Positive SMS reply | 200 | "Positive reply - interested" |
| Question SMS reply | 150 | "Asked a question - engaged" |
| Platform job posted | 180 | "Posted a job on RateRight" |
| Platform login < 7 days | 100 | "Active on platform" |
| Inbound call count >= 2 | 200 | "Called back twice" |

#### 3. Timing Score (0-200 points)

| Condition | Points | Why Badge |
|-----------|--------|-----------|
| Current hour = best conversion hour | 200 | "Best time to call - 2pm slot!" |
| Current hour = 2nd best hour | 150 | "Good calling window" |
| Current = their conversion day | 100 | "Converts on Tuesdays" |
| Morning (9-11am) | 80 | (bonus for prime calling) |
| Afternoon (2-4pm) | 80 | (bonus for prime calling) |

#### 4. Pattern Score (0-150 points)

| Condition | Points | Why Badge |
|-----------|--------|-----------|
| Trade matches winning pattern | 150 | "Electricians convert 73%" |
| Source matches winning pattern | 100 | "Referrals convert best" |
| Similar to recent conversion | 80 | "Similar to last week's win" |

#### 5. Base Score (0-50 points)

| Lead Score | Points |
|------------|--------|
| 90-100 | 50 |
| 70-89 | 40 |
| 50-69 | 30 |
| 20-49 | 15 |
| 0-19 | 5 |

### Staleness Penalty

Applied AFTER base calculation:

| Condition | Penalty | Warning |
|-----------|---------|---------|
| Hot lead, no contact > 24hrs | -50 + urgency boost | Yellow warning |
| Hot lead, no contact > 48hrs | -100 + urgency boost | Red warning |
| Any lead, no contact > 7 days | -30 | "Getting stale" |
| Any lead, no contact > 14 days | -60 | "Gone cold" |

The urgency boost counteracts the penalty but adds the warning badge.

---

## Database Changes

### New Field on Leads Table

```sql
ALTER TABLE leads ADD COLUMN IF NOT EXISTS call_list_score INTEGER DEFAULT 0;
ALTER TABLE leads ADD COLUMN IF NOT EXISTS call_list_reason TEXT;
ALTER TABLE leads ADD COLUMN IF NOT EXISTS call_list_updated_at TIMESTAMPTZ;
ALTER TABLE leads ADD COLUMN IF NOT EXISTS call_list_warning TEXT;

CREATE INDEX IF NOT EXISTS idx_leads_call_list_score ON leads(call_list_score DESC);
```

### Pre-computation Strategy

- **On-demand refresh**: When user loads call list, check if scores are stale (> 5 mins)
- **Event-triggered**: Refresh affected lead's score on: SMS inbound, call inbound, callback created
- **Batch refresh**: Cron job every 15 mins refreshes all active leads

---

## API Changes

### GET /api/call-list (Enhanced)

**Response:**
```json
{
  "callList": [
    {
      "id": "uuid",
      "first_name": "Sarah",
      "last_name": "K",
      "company": "BuildRight",
      "score": 75,
      "phone": "+61412345678",

      // NEW: Smart ranking fields
      "priority_score": 847,
      "priority_reason": "Replied 12 mins ago - strike now!",
      "priority_type": "urgency",
      "priority_warning": null,
      "priority_factors": {
        "urgency": 350,
        "intent": 200,
        "timing": 150,
        "pattern": 100,
        "base": 47
      },

      // Existing
      "tier": "hot",
      "patternBoost": { "type": "trade", "description": "..." }
    }
  ],
  "stats": { ... },
  "computedAt": "2026-01-16T14:30:00Z"
}
```

### New Fields Explained

| Field | Purpose |
|-------|---------|
| `priority_score` | Computed 0-1000 score for sorting |
| `priority_reason` | Human-readable "why" badge text |
| `priority_type` | Category: urgency, intent, timing, pattern, score |
| `priority_warning` | Optional warning: "going_cold", "callback_overdue" |
| `priority_factors` | Breakdown for debugging/transparency |

---

## Backend Implementation

### New Service: `src/services/callListRanker.js`

```javascript
/**
 * Call List Smart Ranker
 * Computes priority scores with explainable reasons
 */

const WEIGHTS = {
  urgency: 0.35,
  intent: 0.25,
  timing: 0.20,
  pattern: 0.15,
  base: 0.05
};

async function computeLeadPriority(lead, context) {
  // context = { winningPatterns, bestHours, currentHour }

  const factors = {
    urgency: computeUrgencyScore(lead),
    intent: computeIntentScore(lead),
    timing: computeTimingScore(lead, context),
    pattern: computePatternScore(lead, context),
    base: computeBaseScore(lead)
  };

  const rawScore = Object.entries(factors).reduce((sum, [key, val]) => {
    return sum + (val * WEIGHTS[key] / WEIGHTS[key]); // Normalize
  }, 0);

  const { penalty, warning } = computeStaleness(lead);
  const finalScore = Math.max(0, rawScore + penalty);

  const { reason, type } = generateReason(lead, factors);

  return {
    priority_score: Math.round(finalScore),
    priority_reason: reason,
    priority_type: type,
    priority_warning: warning,
    priority_factors: factors
  };
}
```

### Key Functions

#### computeUrgencyScore(lead)
- Check `last_inbound_at` for SMS/call recency
- Check `callbacks` for overdue/due today
- Check `last_contact_at` for staleness on hot leads

#### computeIntentScore(lead)
- Check `is_high_intent` flag
- Check `metadata.inbound_call_count`
- Check last SMS intent (positive/question)
- Check platform activity

#### computeTimingScore(lead, context)
- Compare current hour to `context.bestHours`
- Check if current day matches conversion patterns

#### computePatternScore(lead, context)
- Match trade against `context.winningPatterns`
- Match source against patterns

#### generateReason(lead, factors)
- Find highest-scoring factor
- Generate appropriate human-readable message
- Return type for UI styling

---

## Frontend Changes

### CallList.jsx Updates

Minimal changes - add "why" badge to existing card:

```jsx
// After lead name, before score badge
{currentLead.priority_reason && (
  <div className={`priority-badge ${getPriorityBadgeStyle(currentLead.priority_type, currentLead.priority_warning)}`}>
    {getPriorityIcon(currentLead.priority_type)}
    <span>{currentLead.priority_reason}</span>
  </div>
)}

// Warning badge if present
{currentLead.priority_warning && (
  <div className="warning-badge">
    <AlertTriangle className="w-4 h-4" />
    {getWarningText(currentLead.priority_warning)}
  </div>
)}
```

### Badge Styles by Type

| Type | Color | Icon |
|------|-------|------|
| urgency | Red/Orange gradient | Clock |
| intent | Green | Heart/Flame |
| timing | Blue | Clock with check |
| pattern | Purple | Brain/Sparkles |
| score | Slate | Star |

### Warning Styles

| Warning | Color | Text |
|---------|-------|------|
| going_cold | Amber | "Going cold!" |
| callback_overdue | Red | "Callback overdue!" |
| stale_48hrs | Red pulsing | "52 hrs without contact!" |

---

## Implementation Steps

### Step 1: Database Migration
- [x] Add `call_list_score`, `call_list_reason`, `call_list_warning`, `call_list_updated_at` to leads
- [x] Create index on `call_list_score`

### Step 2: Create Ranker Service
- [x] Create `src/services/callListRanker.js`
- [x] Implement `computeUrgencyScore()`
- [x] Implement `computeIntentScore()`
- [x] Implement `computeTimingScore()`
- [x] Implement `computePatternScore()`
- [x] Implement `generateReason()`
- [x] Implement `computeLeadPriority()`

### Step 3: Update Call List API
- [x] Import ranker service
- [x] Fetch winning patterns and best hours once
- [x] Compute priority for each lead
- [x] Sort by `priority_score` DESC
- [x] Return enhanced response

### Step 4: Add Event Triggers
- [x] On SMS inbound → refresh lead's priority
- [x] On call inbound → refresh lead's priority
- [x] On callback created → refresh lead's priority

### Step 5: Frontend Badge
- [x] Add priority badge component to CallList.jsx
- [x] Style by priority_type
- [x] Add warning badge when present
- [x] Test all badge types

### Step 6: Test & Verify
- [x] Test urgency scoring (recent SMS jumps to top)
- [x] Test timing scoring (best hour leads surface)
- [x] Test staleness warnings (48hr hot lead shows warning)
- [x] Test pattern matching (winning trades boosted)
- [x] Verify performance (< 500ms response)

### Step 7: Documentation
- [x] Update SYSTEM-INTEL.md
- [x] Update BUILD-GUIDE.md
- [x] Mark plan steps complete

---

## Performance Targets

| Metric | Target |
|--------|--------|
| Call list load time | < 500ms |
| Priority computation per lead | < 10ms |
| Full list (50 leads) computation | < 200ms |
| Database query time | < 100ms |

---

## Testing Scenarios

| Scenario | Expected Result |
|----------|-----------------|
| SMS received 5 mins ago | Lead jumps to #1, "Replied 5 mins ago" badge |
| Hot lead, 50 hrs no contact | Red warning, "Hot lead 50 hrs stale!" |
| Callback 2 hrs overdue | Top 5, "Callback 2 hrs overdue" badge |
| Current hour = best conversion hour | Timing badge, boosted position |
| Trade matches 73% pattern | Purple pattern badge |
| 3 inbound calls from lead | "Called you 3 times!" + high intent boost |

---

## Files to Create

| File | Purpose |
|------|---------|
| `src/services/callListRanker.js` | Smart ranking algorithm |
| `supabase/call-list-migration.sql` | New fields + index |

## Files to Modify

| File | Changes |
|------|---------|
| `src/routes/callList.js` | Use ranker, return enhanced data |
| `src/routes/webhooks.js` | Trigger refresh on SMS inbound |
| `src/routes/voice.js` | Trigger refresh on call inbound |
| `admin/src/pages/CallList.jsx` | Add priority badge + warning |

---

## Success Criteria

1. **User sees WHY** - Every lead has a clear reason badge
2. **Recency matters** - 12-min-old reply beats 2-day-old reply
3. **Time-aware** - Best calling hour leads surface at that hour
4. **Staleness visible** - Hot leads going cold show urgent warnings
5. **Fast** - List loads in < 500ms
6. **Explainable** - User can understand why any lead is ranked where it is

---

**Ready for Phase 3: Execute**

Build order:
1. Database migration
2. Ranker service (core algorithm)
3. Update call list API
4. Frontend badge
5. Event triggers
6. Test & document
