From 6582da60d4d865403087a172e27969adafd9ecb6 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 5 Jun 2026 07:50:07 +0200 Subject: [PATCH] =?UTF-8?q?edge=20function=20per=20interrogare=20openai,?= =?UTF-8?q?=20capire=20quali=20immagini=20sono=20di=20documenti=20d'identi?= =?UTF-8?q?t=C3=A0=20e=20assegnarli=20al=20cliente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/id_doc_manager/index.ts | 136 +++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 supabase/functions/id_doc_manager/index.ts diff --git a/supabase/functions/id_doc_manager/index.ts b/supabase/functions/id_doc_manager/index.ts new file mode 100644 index 0000000..2cbc30b --- /dev/null +++ b/supabase/functions/id_doc_manager/index.ts @@ -0,0 +1,136 @@ +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 + }) + } +}) \ No newline at end of file