# Voicemail Drop - Plan

## Problem
When sales reps hit voicemail, they have to:
1. Wait for the greeting to end
2. Leave a live message (awkward, inconsistent, time-consuming)
3. Say "um" and "uh" because they're thinking on the spot

This wastes 30-60 seconds per voicemail and results in inconsistent messages.

## Solution
Pre-record professional voicemails. When hitting VM, rep taps "Drop VM" → pre-recorded message plays → call ends automatically.

**Result:** Perfect voicemails in 5 seconds instead of 60.

---

## Implementation Approach

### Phase 1: Manual Drop (Build First)
- Rep recognizes they hit voicemail (hears greeting/beep)
- Taps "Drop VM" button
- Backend plays pre-recorded audio via Twilio
- Call auto-hangs up after audio completes

**Pros:** Simple, works immediately, no extra Twilio costs
**Cons:** Rep has to recognize voicemail manually

### Phase 2: AMD Auto-Detection (Future)
- Twilio AMD detects voicemail vs human
- Webhook notifies frontend
- Auto-drop or prompt to drop
- Cost: ~$0.0075/call

---

## Database Schema

```sql
-- Pre-recorded voicemail messages
CREATE TABLE IF NOT EXISTS voicemail_recordings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users(id), -- Owner (null = shared)
  name VARCHAR(100) NOT NULL, -- "Standard Intro", "Follow-up VM"
  description TEXT,
  recording_url TEXT NOT NULL, -- Twilio Media URL or Supabase storage
  recording_sid VARCHAR(50), -- Twilio Recording SID if uploaded there
  duration_seconds INTEGER,
  is_default BOOLEAN DEFAULT FALSE, -- Default VM for this user
  is_shared BOOLEAN DEFAULT FALSE, -- Available to whole team
  usage_count INTEGER DEFAULT 0, -- Track which VMs work best
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for quick lookup
CREATE INDEX idx_vm_recordings_user ON voicemail_recordings(user_id);
CREATE INDEX idx_vm_recordings_shared ON voicemail_recordings(is_shared) WHERE is_shared = TRUE;

-- Track voicemail drops for analytics
CREATE TABLE IF NOT EXISTS voicemail_drops (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  recording_id UUID REFERENCES voicemail_recordings(id),
  lead_id UUID REFERENCES leads(id),
  user_id UUID REFERENCES auth.users(id),
  call_sid VARCHAR(50),
  dropped_at TIMESTAMPTZ DEFAULT NOW(),
  -- Track effectiveness
  lead_called_back BOOLEAN,
  callback_within_hours INTEGER
);
```

---

## API Endpoints

### Recording Management

```
GET /api/voicemail/recordings
  - List user's recordings + shared recordings
  - Returns: [{id, name, description, duration_seconds, is_default, usage_count}]

POST /api/voicemail/recordings
  - Create new recording
  - Body: multipart/form-data with audio file
  - Or: {recordingUrl} if already hosted
  - Returns: {id, name, recording_url}

DELETE /api/voicemail/recordings/:id
  - Delete a recording (only owner)

PATCH /api/voicemail/recordings/:id/default
  - Set as default voicemail

GET /api/voicemail/recordings/:id/audio
  - Stream the audio file (for preview)
```

### Drop Voicemail

```
POST /api/voicemail/drop
  - Drop a voicemail on active call
  - Body: {callSid, recordingId, leadId}
  - Backend uses Twilio API to:
    1. Play the recording
    2. Hang up after completion
  - Returns: {success, message}
```

---

## Backend Implementation

### 1. Recording Upload (src/routes/voicemail.js)

```javascript
const express = require('express');
const router = express.Router();
const multer = require('multer');
const { supabase } = require('../config/database');
const twilio = require('twilio');

const upload = multer({ storage: multer.memoryStorage() });

// Upload recording to Supabase Storage
router.post('/recordings', upload.single('audio'), async (req, res) => {
  const { name, description } = req.body;
  const userId = req.user?.id;

  if (!req.file) {
    return res.status(400).json({ error: 'Audio file required' });
  }

  // Upload to Supabase Storage
  const fileName = `vm_${Date.now()}_${userId}.mp3`;
  const { data: uploadData, error: uploadError } = await supabase.storage
    .from('voicemail-recordings')
    .upload(fileName, req.file.buffer, {
      contentType: req.file.mimetype,
      upsert: false
    });

  if (uploadError) {
    return res.status(500).json({ error: 'Failed to upload' });
  }

  // Get public URL
  const { data: urlData } = supabase.storage
    .from('voicemail-recordings')
    .getPublicUrl(fileName);

  // Save to database
  const { data, error } = await supabase
    .from('voicemail_recordings')
    .insert({
      user_id: userId,
      name,
      description,
      recording_url: urlData.publicUrl,
      duration_seconds: req.body.duration || null
    })
    .select()
    .single();

  if (error) {
    return res.status(500).json({ error: error.message });
  }

  res.json(data);
});
```

### 2. Drop Voicemail (src/routes/voicemail.js)

```javascript
// Drop a voicemail on an active call
router.post('/drop', async (req, res) => {
  const { callSid, recordingId, leadId } = req.body;
  const userId = req.user?.id;

  if (!callSid || !recordingId) {
    return res.status(400).json({ error: 'callSid and recordingId required' });
  }

  // Get recording URL
  const { data: recording, error } = await supabase
    .from('voicemail_recordings')
    .select('recording_url, name')
    .eq('id', recordingId)
    .single();

  if (error || !recording) {
    return res.status(404).json({ error: 'Recording not found' });
  }

  // Use Twilio to modify the call - play audio then hang up
  const client = twilio(
    process.env.TWILIO_ACCOUNT_SID,
    process.env.TWILIO_AUTH_TOKEN
  );

  try {
    // Update the call to play the recording
    await client.calls(callSid).update({
      twiml: `<Response>
        <Play>${recording.recording_url}</Play>
        <Hangup/>
      </Response>`
    });

    // Log the drop
    await supabase.from('voicemail_drops').insert({
      recording_id: recordingId,
      lead_id: leadId,
      user_id: userId,
      call_sid: callSid
    });

    // Increment usage count
    await supabase.rpc('increment_vm_usage', { recording_id: recordingId });

    res.json({ success: true, message: 'Voicemail dropped' });

  } catch (err) {
    console.error('VM Drop error:', err);
    res.status(500).json({ error: 'Failed to drop voicemail' });
  }
});
```

---

## Frontend Implementation

### 1. Recording Management (VoicemailSettings.jsx)

Add to Settings or Scripts page:

```jsx
function VoicemailRecordings() {
  const [recordings, setRecordings] = useState([]);
  const [isRecording, setIsRecording] = useState(false);
  const mediaRecorderRef = useRef(null);

  // Record new voicemail
  const startRecording = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const mediaRecorder = new MediaRecorder(stream);
    const chunks = [];

    mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
    mediaRecorder.onstop = () => {
      const blob = new Blob(chunks, { type: 'audio/mp3' });
      uploadRecording(blob);
    };

    mediaRecorder.start();
    mediaRecorderRef.current = mediaRecorder;
    setIsRecording(true);
  };

  const stopRecording = () => {
    mediaRecorderRef.current?.stop();
    setIsRecording(false);
  };

  return (
    <div>
      <h3>Your Voicemails</h3>
      {recordings.map(r => (
        <RecordingCard key={r.id} recording={r} />
      ))}
      <button onClick={isRecording ? stopRecording : startRecording}>
        {isRecording ? 'Stop Recording' : 'Record New VM'}
      </button>
    </div>
  );
}
```

### 2. Drop VM Button (TwilioCall.jsx)

Add to active call UI:

```jsx
// In TwilioCall.jsx, add to the call controls
const [showVMPicker, setShowVMPicker] = useState(false);
const [vmRecordings, setVmRecordings] = useState([]);

// Load recordings when component mounts
useEffect(() => {
  voicemailApi.getRecordings().then(setVmRecordings);
}, []);

// Drop VM function
const dropVoicemail = async (recordingId) => {
  const callSid = callRef.current?.parameters?.CallSid;
  if (!callSid) return;

  await voicemailApi.drop({
    callSid,
    recordingId,
    leadId: lead?.id
  });

  // Call will auto-end after VM plays
  setStatus('ended');
  onCallEnd?.(duration);
};

// In render, add button:
{status === 'in-progress' && (
  <button
    onClick={() => setShowVMPicker(true)}
    className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg"
  >
    <Voicemail className="w-5 h-5" />
    Drop VM
  </button>
)}

// VM Picker Modal
{showVMPicker && (
  <VMPickerModal
    recordings={vmRecordings}
    onSelect={(id) => {
      dropVoicemail(id);
      setShowVMPicker(false);
    }}
    onClose={() => setShowVMPicker(false)}
  />
)}
```

---

## Seed Voicemails

Create 3 default voicemails for the team:

### 1. Standard Introduction
> "G'day, this is [Name] from the RateRight team. Just giving you a quick call about finding workers for your projects. If you've got a minute, give us a ring back on this number or shoot us a text. Cheers!"

### 2. Follow-up
> "Hey, it's [Name] from RateRight again. Just following up on my earlier message. When you get a chance, give me a call back or flick me a text. Talk soon!"

### 3. Hot Lead Re-engagement
> "G'day, [Name] from RateRight here. I noticed you were looking at our platform recently. We've actually got some great workers available in your area right now. Call me back when you can - I'd love to help you out. Cheers!"

---

## User Flow

### Setting Up Voicemails
1. Go to Settings > Voicemails
2. Click "Record New" or upload audio file
3. Name it (e.g., "Standard Intro")
4. Set as default (optional)

### Using During Calls
1. Call lead from Call List
2. Lead doesn't answer, goes to voicemail
3. Wait for beep
4. Tap "Drop VM" button
5. Select voicemail (or uses default with long-press)
6. Pre-recorded message plays
7. Call auto-ends

---

## Implementation Order

1. [x] Database: Create voicemail_recordings and voicemail_drops tables
   - Schema: `supabase/voicemail-drop-schema.sql`
   - Migration pending in: `supabase/PENDING_MIGRATIONS.md`
2. [x] Backend: Recording CRUD endpoints (upload, list, delete)
   - Route: `src/routes/voicemail.js`
   - Endpoints: GET/POST /recordings, DELETE /recordings/:id, PATCH /recordings/:id/default
3. [x] Backend: Drop endpoint with Twilio call modification
   - POST /api/voicemail/drop - plays audio via TwiML then hangs up
4. [ ] Supabase Storage: Create voicemail-recordings bucket (user task)
5. [x] Frontend: VoicemailSettings page for recording management
   - Component: `admin/src/components/VoicemailSettings.jsx`
   - Accessible from: More → Settings → Voicemail Recordings
6. [x] Frontend: Drop VM button in TwilioCall.jsx
   - Amber button next to Hang Up
   - Shows picker modal when tapped
7. [x] Frontend: VM picker modal in TwilioCall.jsx
   - Lists recordings, shows loader, handles drop
8. [x] Seed: 3 default team voicemails in schema (URL pending)
9. [ ] Test end-to-end (after migration + bucket creation)

---

## Success Metrics

- VMs dropped per day
- Average call duration when VM dropped vs live VM
- Callback rate from dropped VMs vs live
- Time saved per rep per day

---

## Estimated Effort

| Phase | Effort |
|-------|--------|
| Database + Backend | 1 day |
| Frontend (settings) | 0.5 day |
| Frontend (drop button) | 0.5 day |
| Testing + Polish | 0.5 day |
| **Total** | **2.5 days** |

---

*Created: Jan 18, 2026*
