Browse Source

feat: add VAPI call integration #104

- Add database migration for VAPI-specific fields in call_logs table
- Create vapi-webhook Edge Function to receive end-of-call-report webhooks
- Implement call-logs API endpoint to fetch and transform call data
- Update CallDetailsModal to display VAPI conversation messages and recordings
- Add comprehensive documentation in VAPI_INTEGRATION.md

Features:
- Store VAPI messages, analysis, and recording URLs
- Display formatted transcripts with customer/assistant messages
- Show call summary and success evaluation
- Provide links to mono/stereo recordings and call logs
- Map VAPI data to existing call_logs schema

VAPI webhook URL: https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/vapi-webhook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 4 months ago
parent
commit
c9bee8ecda

+ 331 - 0
VAPI_INTEGRATION.md

@@ -0,0 +1,331 @@
+# VAPI Integration Documentation
+
+## Overview
+
+ShopCall.ai integrates with VAPI (Voice AI Platform) to receive and store call data from AI-powered phone conversations. This integration allows the system to track call logs, transcripts, recordings, and analytics from VAPI calls.
+
+## Architecture
+
+### Components
+
+1. **VAPI Webhook Endpoint** (`/functions/v1/vapi-webhook`)
+   - Receives POST requests from VAPI
+   - Validates and processes end-of-call reports
+   - Stores call data in the database
+
+2. **Database Schema** (`call_logs` table)
+   - Stores call metadata, transcripts, and analytics
+   - Includes VAPI-specific fields for messages, analysis, and recording URLs
+
+3. **API Endpoint** (`/api/call-logs`)
+   - Retrieves call logs for authenticated users
+   - Transforms data for frontend display
+
+4. **WebUI** (`/call-logs` page)
+   - Displays call logs in a table format
+   - Shows detailed call information including transcripts and recordings
+
+## Database Schema
+
+### New Fields in `call_logs` Table
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `vapi_call_id` | TEXT | Unique identifier from VAPI for this call |
+| `messages` | JSONB | Array of conversation messages (bot/user/system/tool_calls) |
+| `analysis` | JSONB | VAPI analysis object (summary, successEvaluation) |
+| `stereo_recording_url` | TEXT | URL to stereo recording from VAPI |
+| `pcap_url` | TEXT | URL to SIP packet capture file |
+| `log_url` | TEXT | URL to VAPI call logs |
+| `vapi_timestamp` | BIGINT | VAPI event timestamp in milliseconds |
+
+### Indexes
+
+- `idx_call_logs_vapi_call_id` - Fast lookups by VAPI call ID
+- `idx_call_logs_vapi_timestamp` - Time-based queries
+
+## VAPI Webhook Configuration
+
+### Webhook URL
+
+```
+https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/vapi-webhook
+```
+
+### Webhook Events
+
+The integration listens for `end-of-call-report` events from VAPI.
+
+### Expected Payload Structure
+
+```json
+{
+  "message": {
+    "timestamp": 1763633517714,
+    "type": "end-of-call-report",
+    "analysis": {
+      "summary": "Call summary text",
+      "successEvaluation": "true"
+    },
+    "artifact": {
+      "messages": [
+        {
+          "role": "bot|user|system|tool_calls|tool_call_result",
+          "message": "Message text",
+          "time": 1763633305414,
+          "endTime": 1763633327672,
+          "secondsFromStart": 0.486,
+          "duration": 270
+        }
+      ],
+      "transcript": "Full conversation transcript",
+      "recordingUrl": "https://storage.vapi.ai/...",
+      "stereoRecordingUrl": "https://storage.vapi.ai/...",
+      "pcapUrl": "https://storage.vapi.ai/...",
+      "logUrl": "https://calllogs.vapi.ai/..."
+    },
+    "call": {
+      "id": "unique-call-id",
+      "assistantId": "assistant-id",
+      "phoneNumberId": "phone-number-id",
+      "customerId": "customer-phone-number",
+      "costBreakdown": {
+        "stt": 0.0012,
+        "llm": 0.0034,
+        "tts": 0.0023,
+        "twilio": 0.0050,
+        "total": 0.0119
+      }
+    }
+  }
+}
+```
+
+## Data Mapping
+
+### VAPI → call_logs
+
+| VAPI Field | call_logs Field |
+|------------|-----------------|
+| `message.call.id` | `vapi_call_id` |
+| `message.timestamp` | `vapi_timestamp`, `started_at`, `ended_at` |
+| `message.artifact.messages` | `messages` |
+| `message.analysis` | `analysis` |
+| `message.analysis.summary` | `summary` |
+| `message.artifact.transcript` | `transcript` |
+| `message.artifact.recordingUrl` | `recording_url` |
+| `message.artifact.stereoRecordingUrl` | `stereo_recording_url` |
+| `message.artifact.pcapUrl` | `pcap_url` |
+| `message.artifact.logUrl` | `log_url` |
+| `message.call.assistantId` | `assistant_id` |
+| `message.call.phoneNumberId` | `phone_number_id` |
+| `message.call.customerId` | `customer_number` |
+| `message.call.costBreakdown.*` | `cost_stt`, `cost_llm`, `cost_tts`, `cost_twilio`, `cost_total` |
+
+### Call Outcome Mapping
+
+- `analysis.successEvaluation = "true"` → `call_outcome = "resolved"`
+- `analysis.successEvaluation = "false"` → `call_outcome = "not_interested"`
+- No evaluation → `call_outcome = "pending"`
+
+### Duration Calculation
+
+Duration is calculated from the last message's `secondsFromStart` field.
+
+## WebUI Features
+
+### Call Logs Table
+
+Displays:
+- Call time
+- Customer phone number (masked for privacy)
+- Intent/Summary
+- Outcome (color-coded)
+- Duration
+- Sentiment (based on successEvaluation)
+- Cost
+
+### Call Details Modal
+
+Shows:
+- Full conversation transcript with timestamps
+- Individual messages (customer/assistant)
+- Call summary and success evaluation
+- Recording playback links (mono and stereo)
+- Call analytics and metadata
+- Link to VAPI call logs
+
+## API Endpoints
+
+### POST /functions/v1/vapi-webhook
+
+**Purpose**: Receive VAPI webhooks
+
+**Authentication**: None (service-level endpoint)
+
+**Request Body**: VAPI webhook payload (JSON)
+
+**Response**:
+```json
+{
+  "status": "success",
+  "call_log_id": "uuid",
+  "message": "Call log stored successfully"
+}
+```
+
+### GET /api/call-logs
+
+**Purpose**: Retrieve call logs for authenticated user
+
+**Authentication**: Bearer token (required)
+
+**Response**:
+```json
+{
+  "success": true,
+  "call_logs": [
+    {
+      "id": "uuid",
+      "time": "2025-11-20 10:30:45",
+      "customer": "...xxx-1234",
+      "intent": "Customer inquiry",
+      "outcome": "resolved",
+      "duration": "3:25",
+      "sentiment": "Positive",
+      "cost": "$0.0119",
+      "outcomeColor": "text-green-500",
+      "sentimentColor": "text-green-500",
+      "fullData": { /* complete call log object */ }
+    }
+  ]
+}
+```
+
+## Security
+
+### Webhook Security
+
+Currently, the webhook endpoint accepts all POST requests. For production, consider:
+
+1. **VAPI Signature Verification**: Validate VAPI webhook signatures
+2. **IP Allowlisting**: Restrict to VAPI IP addresses
+3. **Rate Limiting**: Prevent abuse
+
+### Data Privacy
+
+- Customer phone numbers are masked in the UI (`...xxx-1234`)
+- Full phone numbers stored in database for analytics
+- Call recordings stored externally (VAPI)
+- Transcripts contain PII - handle according to GDPR
+
+## Deployment
+
+### Database Migration
+
+```bash
+# Migration is already applied
+# File: /supabase/migrations/20251120_vapi_integration.sql
+```
+
+### Edge Functions
+
+```bash
+# Deploy VAPI webhook handler
+supabase functions deploy vapi-webhook --project-ref ztklqodcdjeqpsvhlpud
+
+# Deploy updated API endpoint
+supabase functions deploy api --project-ref ztklqodcdjeqpsvhlpud
+```
+
+### Environment Variables
+
+Required in Supabase Edge Functions:
+- `SUPABASE_URL` - Supabase project URL
+- `SUPABASE_SERVICE_ROLE_KEY` - Service role key for database access
+
+## Testing
+
+### Manual Testing
+
+1. **Test Webhook Endpoint**:
+```bash
+curl -X POST https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/vapi-webhook \
+  -H "Content-Type: application/json" \
+  -d @test-payload.json
+```
+
+2. **Test API Endpoint**:
+```bash
+curl https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/api/call-logs \
+  -H "Authorization: Bearer YOUR_TOKEN"
+```
+
+3. **Test WebUI**:
+   - Navigate to `/call-logs`
+   - Verify calls are displayed
+   - Click "Details" to view full call information
+
+### Sample Test Payload
+
+See `message` object in the VAPI webhook payload example above.
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Webhook not receiving data**
+   - Verify VAPI webhook URL configuration
+   - Check Supabase Edge Function logs
+   - Ensure `end-of-call-report` event is enabled
+
+2. **Call logs not appearing in UI**
+   - Check browser console for errors
+   - Verify API endpoint returns data
+   - Ensure user authentication is valid
+
+3. **Missing transcript or messages**
+   - Check VAPI payload structure
+   - Verify `artifact.messages` array exists
+   - Confirm transcript generation in VAPI
+
+### Logs
+
+View Edge Function logs:
+```bash
+# Via Supabase Dashboard
+https://supabase.com/dashboard/project/ztklqodcdjeqpsvhlpud/functions
+
+# Via CLI
+supabase functions logs vapi-webhook
+```
+
+## Future Enhancements
+
+### Planned Features
+
+1. **Real-time Updates**: WebSocket integration for live call updates
+2. **Search and Filtering**: Advanced search by customer, outcome, date range
+3. **Analytics Dashboard**: Call statistics, sentiment analysis, trends
+4. **Export Functionality**: CSV/PDF export of call logs
+5. **Webhook Signature Verification**: Enhanced security
+6. **User Assignment**: Link calls to specific user_id
+7. **Call Tagging**: Custom tags for call categorization
+
+### Optional Improvements
+
+1. **Audio Playback**: In-browser audio player for recordings
+2. **Sentiment Analysis**: AI-powered sentiment detection
+3. **Call Scoring**: Automated quality scoring
+4. **Integration with CRM**: Sync call data to external systems
+5. **Notification System**: Alerts for important calls
+
+## Related Issues
+
+- Issue #104: Initial VAPI integration implementation
+
+## References
+
+- [VAPI Documentation](https://vapi.ai/docs)
+- [Supabase Edge Functions](https://supabase.com/docs/guides/functions)
+- [Supabase Database](https://supabase.com/docs/guides/database)

+ 79 - 31
shopcall.ai-main/src/components/CallDetailsModal.tsx

@@ -10,6 +10,16 @@ interface CallDetailsModalProps {
   call: any;
 }
 
+interface VAPIMessage {
+  role: string;
+  message?: string;
+  content?: string;
+  time: number;
+  endTime?: number;
+  secondsFromStart: number;
+  duration?: number;
+}
+
 export function CallDetailsModal({ isOpen, onClose, call }: CallDetailsModalProps) {
   const [isPlaying, setIsPlaying] = useState(false);
 
@@ -113,8 +123,25 @@ export function CallDetailsModal({ isOpen, onClose, call }: CallDetailsModalProp
 
                 <div>
                   <h4 className="text-white font-medium mb-2">Transcript</h4>
-                  <div className="text-slate-300 text-sm leading-relaxed">
-                    Customer called regarding order status. The AI assistant handled the inquiry professionally and provided appropriate assistance.
+                  <div className="text-slate-300 text-sm leading-relaxed max-h-64 overflow-y-auto">
+                    {call.fullData?.messages && call.fullData.messages.length > 0 ? (
+                      <div className="space-y-3">
+                        {call.fullData.messages
+                          .filter((msg: VAPIMessage) => msg.role === 'user' || msg.role === 'bot' || msg.role === 'assistant')
+                          .map((msg: VAPIMessage, idx: number) => (
+                            <div key={idx} className={`p-2 rounded ${msg.role === 'user' ? 'bg-slate-600/30' : 'bg-slate-700/30'}`}>
+                              <div className="text-xs text-slate-400 mb-1">
+                                {msg.role === 'user' ? 'Customer' : 'Assistant'} • {Math.floor(msg.secondsFromStart)}s
+                              </div>
+                              <div className="text-sm">{msg.message || msg.content || ''}</div>
+                            </div>
+                          ))}
+                      </div>
+                    ) : call.fullData?.transcript ? (
+                      <div className="whitespace-pre-wrap">{call.fullData.transcript}</div>
+                    ) : (
+                      <div className="text-slate-400 italic">No transcript available</div>
+                    )}
                   </div>
                 </div>
 
@@ -122,8 +149,16 @@ export function CallDetailsModal({ isOpen, onClose, call }: CallDetailsModalProp
                   <h4 className="text-white font-medium mb-2">Summary</h4>
                   <div className="bg-slate-600/50 rounded-lg p-3">
                     <p className="text-slate-300 text-sm">
-                      Call successfully handled. Customer inquiry about order status was resolved efficiently. Sentiment: {call.sentiment} 😊
+                      {call.fullData?.analysis?.summary || call.intent || 'No summary available'}
                     </p>
+                    {call.fullData?.analysis?.successEvaluation && (
+                      <div className="mt-2 text-xs">
+                        <span className="text-slate-400">Success: </span>
+                        <span className={call.fullData.analysis.successEvaluation === 'true' ? 'text-green-500' : 'text-red-500'}>
+                          {call.fullData.analysis.successEvaluation === 'true' ? '✓ Yes' : '✗ No'}
+                        </span>
+                      </div>
+                    )}
                   </div>
                 </div>
 
@@ -138,38 +173,51 @@ export function CallDetailsModal({ isOpen, onClose, call }: CallDetailsModalProp
                   </div>
                 </div>
 
-                <div>
-                  <h4 className="text-white font-medium mb-3">Call Recording</h4>
-                  <div className="bg-slate-600/50 rounded-lg p-4">
-                    <div className="flex items-center gap-4">
-                      <Button
-                        variant="ghost"
-                        size="sm"
-                        onClick={handlePlayPause}
-                        className="w-12 h-12 rounded-full bg-slate-700 hover:bg-slate-600 text-white border-2 border-slate-500"
-                      >
-                        <Play className="w-5 h-5" />
-                      </Button>
-                      
-                      <div className="flex-1">
-                        <div className="flex items-center justify-between text-sm text-slate-400 mb-1">
-                          <span>0:00</span>
-                          <span>{call.duration}</span>
+                {(call.fullData?.recording_url || call.fullData?.stereo_recording_url) && (
+                  <div>
+                    <h4 className="text-white font-medium mb-3">Call Recording</h4>
+                    <div className="bg-slate-600/50 rounded-lg p-4">
+                      {call.fullData.recording_url && (
+                        <div className="mb-2">
+                          <a
+                            href={call.fullData.recording_url}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="text-cyan-400 hover:text-cyan-300 text-sm flex items-center gap-2"
+                          >
+                            <Play className="w-4 h-4" />
+                            Play Mono Recording
+                          </a>
                         </div>
-                        <div className="w-full bg-slate-700 rounded-full h-2">
-                          <div className="bg-cyan-500 h-2 rounded-full w-0"></div>
+                      )}
+                      {call.fullData.stereo_recording_url && (
+                        <div className="mb-2">
+                          <a
+                            href={call.fullData.stereo_recording_url}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="text-cyan-400 hover:text-cyan-300 text-sm flex items-center gap-2"
+                          >
+                            <Play className="w-4 h-4" />
+                            Play Stereo Recording
+                          </a>
                         </div>
-                      </div>
-                      
-                      <div className="flex items-center gap-2">
-                        <Volume2 className="w-4 h-4 text-slate-400" />
-                        <Button variant="ghost" size="sm" className="text-slate-400">
-                          <MoreHorizontal className="w-4 h-4" />
-                        </Button>
-                      </div>
+                      )}
+                      {call.fullData.log_url && (
+                        <div className="mt-3 pt-3 border-t border-slate-700">
+                          <a
+                            href={call.fullData.log_url}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="text-slate-400 hover:text-slate-300 text-xs flex items-center gap-2"
+                          >
+                            View Call Logs
+                          </a>
+                        </div>
+                      )}
                     </div>
                   </div>
-                </div>
+                )}
               </div>
             </div>
           </div>

+ 81 - 9
supabase/functions/api/index.ts

@@ -1656,15 +1656,87 @@ serve(async (req) => {
 
     // GET /api/call-logs - Get call logs
     if (path === 'call-logs' && req.method === 'GET') {
-      // TODO: Implement real call logs retrieval from database
-      // For now, returning empty array to unblock the frontend
-      return new Response(
-        JSON.stringify({
-          success: true,
-          call_logs: []
-        }),
-        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-      )
+      try {
+        // Fetch call logs from database
+        const { data: callLogs, error: callLogsError } = await supabase
+          .from('call_logs')
+          .select('*')
+          .eq('user_id', user.id)
+          .order('created_at', { ascending: false })
+
+        if (callLogsError) {
+          console.error('Error fetching call logs:', callLogsError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch call logs' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Transform call logs to match frontend format
+        const transformedCallLogs = (callLogs || []).map(log => {
+          // Format duration
+          const duration = log.duration_seconds
+            ? `${Math.floor(log.duration_seconds / 60)}:${String(log.duration_seconds % 60).padStart(2, '0')}`
+            : '0:00'
+
+          // Determine outcome color
+          let outcomeColor = 'text-slate-300'
+          if (log.call_outcome === 'resolved' || log.call_outcome === 'interested') {
+            outcomeColor = 'text-green-500'
+          } else if (log.call_outcome === 'not_interested' || log.call_outcome === 'failed') {
+            outcomeColor = 'text-red-500'
+          } else if (log.call_outcome === 'pending') {
+            outcomeColor = 'text-yellow-500'
+          }
+
+          // Determine sentiment from analysis or summary
+          let sentiment = 'Neutral'
+          let sentimentColor = 'text-slate-300'
+
+          if (log.analysis?.successEvaluation === 'true' || log.analysis?.successEvaluation === true) {
+            sentiment = 'Positive'
+            sentimentColor = 'text-green-500'
+          } else if (log.analysis?.successEvaluation === 'false' || log.analysis?.successEvaluation === false) {
+            sentiment = 'Negative'
+            sentimentColor = 'text-red-500'
+          }
+
+          // Format cost
+          const cost = log.cost_total ? `$${Number(log.cost_total).toFixed(4)}` : '$0.00'
+
+          // Format time
+          const time = new Date(log.created_at).toLocaleString()
+
+          return {
+            id: log.id,
+            time,
+            customer: log.customer_number || 'Unknown',
+            intent: log.summary || 'N/A',
+            outcome: log.call_outcome || 'pending',
+            duration,
+            sentiment,
+            cost,
+            outcomeColor,
+            sentimentColor,
+            // Include full log data for details modal
+            fullData: log
+          }
+        })
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            call_logs: transformedCallLogs
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in call-logs endpoint:', error)
+        return new Response(
+          JSON.stringify({ error: 'Internal server error' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
     }
 
     // GET /api/phone-numbers - Get available phone numbers for user's country

+ 216 - 0
supabase/functions/vapi-webhook/index.ts

@@ -0,0 +1,216 @@
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+  'Access-Control-Allow-Methods': 'POST, OPTIONS',
+}
+
+interface VAPIMessage {
+  role: string
+  message?: string
+  content?: string
+  time: number
+  endTime?: number
+  secondsFromStart: number
+  duration?: number
+  source?: string
+  metadata?: any
+  toolCalls?: any[]
+  tool_calls?: any[]
+  toolCallId?: string
+  tool_call_id?: string
+  name?: string
+  result?: string
+}
+
+interface VAPIAnalysis {
+  summary?: string
+  successEvaluation?: string
+}
+
+interface VAPIArtifact {
+  messages: VAPIMessage[]
+  messagesOpenAIFormatted?: VAPIMessage[]
+  transcript?: string
+  recordingUrl?: string
+  stereoRecordingUrl?: string
+  pcapUrl?: string
+  logUrl?: string
+}
+
+interface VAPIWebhookPayload {
+  message: {
+    timestamp: number
+    type: string
+    analysis?: VAPIAnalysis
+    artifact?: VAPIArtifact
+    call?: {
+      id?: string
+      phoneNumberId?: string
+      customerId?: string
+      assistantId?: string
+      cost?: number
+      costBreakdown?: {
+        stt?: number
+        llm?: number
+        tts?: number
+        twilio?: number
+        total?: number
+      }
+    }
+  }
+}
+
+serve(async (req) => {
+  // Handle CORS preflight
+  if (req.method === 'OPTIONS') {
+    return new Response('ok', { headers: corsHeaders })
+  }
+
+  try {
+    // Only accept POST requests
+    if (req.method !== 'POST') {
+      return new Response(
+        JSON.stringify({ error: 'Method not allowed' }),
+        { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // Parse the webhook payload
+    const payload: VAPIWebhookPayload = await req.json()
+    console.log('Received VAPI webhook:', JSON.stringify(payload, null, 2))
+
+    // Validate payload structure
+    if (!payload.message || payload.message.type !== 'end-of-call-report') {
+      console.log('Ignoring non-end-of-call-report webhook:', payload.message?.type)
+      return new Response(
+        JSON.stringify({ status: 'ignored', reason: 'Not an end-of-call-report' }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    const { message } = payload
+    const { timestamp, analysis, artifact, call } = message
+
+    // Create Supabase client with service role key (bypasses RLS for webhook)
+    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+    const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+    const supabase = createClient(supabaseUrl, supabaseKey)
+
+    // Extract transcript from messages
+    let transcript = artifact?.transcript || ''
+    if (!transcript && artifact?.messages) {
+      // Build transcript from messages
+      const transcriptParts = artifact.messages
+        .filter(msg => msg.role === 'user' || msg.role === 'bot' || msg.role === 'assistant')
+        .map(msg => {
+          const speaker = msg.role === 'user' ? 'Customer' : 'Assistant'
+          const text = msg.message || msg.content || ''
+          return `[${speaker}]: ${text}`
+        })
+      transcript = transcriptParts.join('\n\n')
+    }
+
+    // Calculate call duration from messages
+    let durationSeconds = 0
+    if (artifact?.messages && artifact.messages.length > 0) {
+      const lastMessage = artifact.messages[artifact.messages.length - 1]
+      durationSeconds = Math.round(lastMessage.secondsFromStart)
+    }
+
+    // Determine call outcome based on analysis
+    let callOutcome = 'pending'
+    if (analysis?.successEvaluation === 'true' || analysis?.successEvaluation === true) {
+      callOutcome = 'resolved'
+    } else if (analysis?.successEvaluation === 'false' || analysis?.successEvaluation === false) {
+      callOutcome = 'not_interested'
+    }
+
+    // Prepare call log data
+    const callLogData = {
+      // VAPI-specific fields
+      vapi_call_id: call?.id || crypto.randomUUID(),
+      vapi_timestamp: timestamp,
+      messages: artifact?.messages || [],
+      analysis: analysis || {},
+
+      // Recording URLs
+      recording_url: artifact?.recordingUrl,
+      stereo_recording_url: artifact?.stereoRecordingUrl,
+      pcap_url: artifact?.pcapUrl,
+      log_url: artifact?.logUrl,
+
+      // Call metadata
+      assistant_id: call?.assistantId,
+      phone_number_id: call?.phoneNumberId,
+      customer_number: call?.customerId,
+
+      // Call status and outcome
+      status: 'completed',
+      call_outcome: callOutcome,
+      call_type: 'inbound', // VAPI calls are typically inbound
+
+      // Transcript and summary
+      transcript: transcript,
+      summary: analysis?.summary || '',
+
+      // Timing
+      started_at: new Date(timestamp).toISOString(),
+      ended_at: new Date(timestamp).toISOString(),
+      duration_seconds: durationSeconds,
+
+      // Costs
+      cost_stt: call?.costBreakdown?.stt || 0,
+      cost_llm: call?.costBreakdown?.llm || 0,
+      cost_tts: call?.costBreakdown?.tts || 0,
+      cost_twilio: call?.costBreakdown?.twilio || 0,
+      cost_total: call?.costBreakdown?.total || call?.cost || 0,
+      cost_totals: call?.costBreakdown?.total || call?.cost || 0,
+
+      // Timestamps
+      created_at: new Date().toISOString(),
+      updated_at: new Date().toISOString(),
+    }
+
+    console.log('Inserting call log:', callLogData)
+
+    // Insert call log into database
+    const { data, error } = await supabase
+      .from('call_logs')
+      .insert([callLogData])
+      .select()
+      .single()
+
+    if (error) {
+      console.error('Error inserting call log:', error)
+      return new Response(
+        JSON.stringify({ error: 'Failed to store call log', details: error.message }),
+        { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    console.log('Call log stored successfully:', data.id)
+
+    // Return success response
+    return new Response(
+      JSON.stringify({
+        status: 'success',
+        call_log_id: data.id,
+        message: 'Call log stored successfully'
+      }),
+      { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+
+  } catch (error) {
+    console.error('Error processing VAPI webhook:', error)
+    return new Response(
+      JSON.stringify({
+        error: 'Internal server error',
+        details: error instanceof Error ? error.message : 'Unknown error'
+      }),
+      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+  }
+})

+ 37 - 0
supabase/migrations/20251120_vapi_integration.sql

@@ -0,0 +1,37 @@
+-- Migration: Add VAPI integration fields to call_logs table
+-- Description: Adds fields to store VAPI webhook data including messages, analysis, and recording URLs
+-- Date: 2025-11-20
+-- Related Issue: #104
+
+-- Add VAPI-specific columns to call_logs table
+ALTER TABLE call_logs
+ADD COLUMN IF NOT EXISTS vapi_call_id TEXT,
+ADD COLUMN IF NOT EXISTS messages JSONB,
+ADD COLUMN IF NOT EXISTS analysis JSONB,
+ADD COLUMN IF NOT EXISTS stereo_recording_url TEXT,
+ADD COLUMN IF NOT EXISTS pcap_url TEXT,
+ADD COLUMN IF NOT EXISTS log_url TEXT,
+ADD COLUMN IF NOT EXISTS vapi_timestamp BIGINT;
+
+-- Add index on vapi_call_id for faster lookups
+CREATE INDEX IF NOT EXISTS idx_call_logs_vapi_call_id ON call_logs(vapi_call_id);
+
+-- Add index on vapi_timestamp for time-based queries
+CREATE INDEX IF NOT EXISTS idx_call_logs_vapi_timestamp ON call_logs(vapi_timestamp);
+
+-- Add comments to document the new fields
+COMMENT ON COLUMN call_logs.vapi_call_id IS 'Unique identifier from VAPI for this call';
+COMMENT ON COLUMN call_logs.messages IS 'Array of conversation messages from VAPI (bot/user/system/tool_calls)';
+COMMENT ON COLUMN call_logs.analysis IS 'VAPI analysis object containing summary and successEvaluation';
+COMMENT ON COLUMN call_logs.stereo_recording_url IS 'URL to stereo recording from VAPI';
+COMMENT ON COLUMN call_logs.pcap_url IS 'URL to SIP packet capture file from VAPI';
+COMMENT ON COLUMN call_logs.log_url IS 'URL to VAPI call logs';
+COMMENT ON COLUMN call_logs.vapi_timestamp IS 'VAPI event timestamp in milliseconds since epoch';
+
+-- Log completion
+DO $$
+BEGIN
+  RAISE NOTICE 'Migration completed: VAPI integration fields added to call_logs table';
+  RAISE NOTICE 'Added columns: vapi_call_id, messages, analysis, stereo_recording_url, pcap_url, log_url, vapi_timestamp';
+  RAISE NOTICE 'Added indexes: idx_call_logs_vapi_call_id, idx_call_logs_vapi_timestamp';
+END $$;