Files
flux/supabase/functions/id_doc_manager/index.ts

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
})
}
})