136 lines
5.7 KiB
TypeScript
136 lines
5.7 KiB
TypeScript
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',
|
|
}
|
|
|
|
serve(async (req) => {
|
|
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
|
|
|
|
try {
|
|
const payload = await req.json()
|
|
const { table, type, record } = payload
|
|
|
|
// 🥷 1. FILTRO EVENTI: Agiamo solo sui nuovi allegati
|
|
if (table !== 'attachments' || type !== 'INSERT') {
|
|
return new Response(JSON.stringify({ message: "Ignorato: non è un INSERT su attachments." }), { status: 200, headers: corsHeaders })
|
|
}
|
|
|
|
// Se l'allegato ha GIÀ un customer_id, l'operatore l'ha caricato direttamente
|
|
// dalla scheda cliente. Non serve sprecare soldi con l'AI.
|
|
if (record.customer_id) {
|
|
return new Response(JSON.stringify({ message: "Customer già presente, analisi saltata." }), { status: 200, headers: corsHeaders })
|
|
}
|
|
|
|
// Se non è collegato a un ticket, a un'operazione o a una nota, non abbiamo
|
|
// modo di risalire al proprietario, quindi fermiamo tutto.
|
|
if (!record.operation_id && !record.ticket_id && !record.note_id) {
|
|
return new Response(JSON.stringify({ message: "Nessuna entità collegata da cui estrarre il customer." }), { status: 200, headers: corsHeaders })
|
|
}
|
|
|
|
// 🥷 2. FILTRO FORMATI: Passiamo a OpenAI solo immagini vere (niente zip o pdf per ora)
|
|
// Nota: OpenAI supporta anche i PDF in vision, ma le immagini sono più sicure/economiche per i documenti.
|
|
const validExtensions = ['jpg', 'jpeg', 'png', 'webp']
|
|
const ext = record.extension?.replace('.', '').toLowerCase()
|
|
|
|
if (!validExtensions.includes(ext)) {
|
|
return new Response(JSON.stringify({ message: "Formato non supportato da Vision." }), { status: 200, headers: corsHeaders })
|
|
}
|
|
|
|
const supabase = createClient(
|
|
Deno.env.get('SUPABASE_URL') ?? '',
|
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
|
)
|
|
|
|
// 🥷 3. GENERAZIONE URL SICURO
|
|
// Dal momento che il tuo storage bucket è (spero) privato, dobbiamo creare un URL
|
|
// temporaneo firmato da passare a OpenAI, valido solo per i prossimi 60 secondi.
|
|
const { data: urlData, error: urlError } = await supabase.storage
|
|
.from('documents')
|
|
.createSignedUrl(record.storage_path, 60)
|
|
|
|
if (urlError || !urlData) throw new Error(`Errore generazione URL: ${urlError?.message}`)
|
|
|
|
const imageUrl = urlData.signedUrl
|
|
|
|
// 🥷 4. L'INTERROGAZIONE ALL'AI (Il cuore del sistema)
|
|
console.log(`Analizzo immagine ${record.id} con OpenAI...`)
|
|
|
|
const openAiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
model: "gpt-4o-mini", // Il modello super veloce ed economico
|
|
response_format: { type: "json_object" }, // Vogliamo solo codice, niente chiacchiere
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Questa immagine è una foto o una scansione di un documento d'identità personale valido (es. carta d'identità, patente di guida, passaporto, tessera sanitaria, codice fiscale)? Rispondi ESCLUSIVAMENTE con un JSON in questo formato: { \"is_id\": true } oppure { \"is_id\": false }"
|
|
},
|
|
{
|
|
type: "image_url",
|
|
image_url: { "url": imageUrl }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
})
|
|
|
|
const aiData = await openAiResponse.json()
|
|
const resultText = aiData.choices[0].message.content
|
|
const result = JSON.parse(resultText)
|
|
|
|
// 🥷 5. LA LOGICA DI SMISTAMENTO
|
|
if (result.is_id === true) {
|
|
console.log(`🎯 Documento rilevato! Cerco il padrone...`)
|
|
|
|
let targetCustomerId = null
|
|
|
|
// Andiamo a pescare il customer_id esplorando l'albero delle relazioni
|
|
if (record.operation_id) {
|
|
const { data } = await supabase.from('operations').select('customer_id').eq('id', record.operation_id).single()
|
|
targetCustomerId = data?.customer_id
|
|
} else if (record.ticket_id) {
|
|
const { data } = await supabase.from('tickets').select('customer_id').eq('id', record.ticket_id).single()
|
|
targetCustomerId = data?.customer_id
|
|
} else if (record.note_id) {
|
|
// Se le note hanno una FK verso i clienti:
|
|
const { data } = await supabase.from('notes').select('customer_id').eq('id', record.note_id).single()
|
|
targetCustomerId = data?.customer_id
|
|
}
|
|
|
|
// Se abbiamo trovato il proprietario, facciamo l'update dell'allegato!
|
|
if (targetCustomerId) {
|
|
await supabase
|
|
.from('attachments')
|
|
.update({ customer_id: targetCustomerId })
|
|
.eq('id', record.id)
|
|
|
|
console.log(`✅ Allegato aggiornato. Legato al cliente: ${targetCustomerId}`)
|
|
} else {
|
|
console.log(`⚠️ Documento rilevato, ma non c'è nessun cliente legato all'entità genitore.`)
|
|
}
|
|
} else {
|
|
console.log(`L'immagine non è un documento (è uno scontrino, una foto di un router, ecc.).`)
|
|
}
|
|
|
|
return new Response(JSON.stringify({ success: true, is_id: result.is_id }), {
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error("ERRORE FATALE NELLA FUNZIONE:", error)
|
|
return new Response(JSON.stringify({ error: error.toString() }), {
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500
|
|
})
|
|
}
|
|
}) |