Преглед изворни кода

feat(vapi-webhook): extract call data and store recordings

- Add columns: started_at, ended_at, transcript, recording_url, duration, costs, cost_total, caller
- Download stereoRecordingUrl and store in Supabase Storage (call-recordings bucket)
- Extract call data from VAPI payload into dedicated columns
- Add optional caller query parameter for caller number
Fszontagh пре 4 месеци
родитељ
комит
7f568406e8
1 измењених фајлова са 83 додато и 2 уклоњено
  1. 83 2
      supabase/functions/vapi-webhook/index.ts

+ 83 - 2
supabase/functions/vapi-webhook/index.ts

@@ -11,6 +11,62 @@ const corsHeaders = {
   'Access-Control-Allow-Methods': 'POST, OPTIONS',
 }
 
+const STORAGE_BUCKET = 'call-recordings'
+
+/**
+ * Download recording from VAPI and upload to Supabase Storage
+ */
+async function downloadAndStoreRecording(
+  supabase: ReturnType<typeof createClient>,
+  recordingUrl: string,
+  storeId: string,
+  callId: string
+): Promise<string | null> {
+  try {
+    console.log('Downloading recording from:', recordingUrl)
+
+    // Download the recording
+    const response = await fetch(recordingUrl)
+    if (!response.ok) {
+      console.error('Failed to download recording:', response.status, response.statusText)
+      return null
+    }
+
+    const audioBlob = await response.blob()
+
+    // Extract filename from URL or generate one
+    const urlParts = recordingUrl.split('/')
+    const originalFilename = urlParts[urlParts.length - 1] || `${callId}.wav`
+    const storagePath = `${storeId}/recordings/${originalFilename}`
+
+    console.log('Uploading recording to storage:', storagePath)
+
+    // Upload to Supabase Storage
+    const { data: uploadData, error: uploadError } = await supabase.storage
+      .from(STORAGE_BUCKET)
+      .upload(storagePath, audioBlob, {
+        contentType: 'audio/wav',
+        upsert: true
+      })
+
+    if (uploadError) {
+      console.error('Failed to upload recording:', uploadError)
+      return null
+    }
+
+    // Get public URL
+    const { data: publicUrlData } = supabase.storage
+      .from(STORAGE_BUCKET)
+      .getPublicUrl(storagePath)
+
+    console.log('Recording stored successfully:', publicUrlData.publicUrl)
+    return publicUrlData.publicUrl
+  } catch (error) {
+    console.error('Error processing recording:', error)
+    return null
+  }
+}
+
 serve(async (req) => {
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
@@ -45,6 +101,7 @@ serve(async (req) => {
     // Extract and validate store_id from query parameters
     const url = new URL(req.url)
     const storeId = url.searchParams.get('store_id')
+    const caller = url.searchParams.get('caller') // Optional caller number
 
     if (!storeId) {
       return new Response(
@@ -98,11 +155,34 @@ serve(async (req) => {
       )
     }
 
-    // Prepare simplified call log data
+    const message = payload.message
+    const callId = crypto.randomUUID()
+
+    // Download and store recording if available
+    let recordingUrl: string | null = null
+    if (message.stereoRecordingUrl) {
+      recordingUrl = await downloadAndStoreRecording(
+        supabase,
+        message.stereoRecordingUrl,
+        storeId,
+        callId
+      )
+    }
+
+    // Prepare call log data with extracted fields
     const callLogData = {
-      id: crypto.randomUUID(),
+      id: callId,
       store_id: storeId,
       payload: payload,
+      // Extracted fields
+      started_at: message.startedAt || null,
+      ended_at: message.endedAt || null,
+      transcript: message.transcript || null,
+      recording_url: recordingUrl,
+      duration: message.durationSeconds || null,
+      costs: message.costs || null,
+      cost_total: message.call?.cost || message.cost || null,
+      caller: caller || null,
     }
 
     console.log('Inserting call log for store:', storeId)
@@ -129,6 +209,7 @@ serve(async (req) => {
       JSON.stringify({
         status: 'success',
         call_log_id: data.id,
+        recording_stored: !!recordingUrl,
         message: 'Call log stored successfully'
       }),
       { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }