From 0fb5491c6823feee87f9acda3d111d6fd95d6b3a Mon Sep 17 00:00:00 2001 From: AV77web Date: Sat, 6 Jun 2026 18:08:57 +0200 Subject: [PATCH] implementato automatismo con n8n Aggiunto webhook workflow e .gitignore; esclusa tesina_finale dal repo (file troppo grandi per Forgejo). Co-authored-by: Cursor --- .gitignore | Bin 519 -> 212 bytes webhook/magricambi-ricerca-prezzi | 266 ++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 webhook/magricambi-ricerca-prezzi diff --git a/.gitignore b/.gitignore index a576c85b313f46cea601c9c18168d6dcb226c444..10aedc2d88818194a66e0eb4414042c4b1435a8b 100644 GIT binary patch delta 168 zcmZo?xx&c&e)s-~;j$C&$keCim2v682t6$>E@g$1)Z)y%LAf_#Pa zqQtzEOof8fB8AKxg`(8l{E}2nE`5m7_%xvUoK$^~=G@HUWG+3I;P~K@{GwDYExm%& zT&}!=T-}t^q|$V~ocwext~`sp_>jc1%$&-2Nyh-scyHH8ExnS85-z>sg4E<V&9P z=F;AcS#c{m5U=~JU5>_9CbwZT6jH8~!n9Ct)sm}|nw#)}Q^!UKudr)^ws&G2>XoVCJ1R-@!=Df&Mt1Pb}GB)CFV5JBwnG{Eo;$I+^7 diff --git a/webhook/magricambi-ricerca-prezzi b/webhook/magricambi-ricerca-prezzi new file mode 100644 index 0000000..6d9b55d --- /dev/null +++ b/webhook/magricambi-ricerca-prezzi @@ -0,0 +1,266 @@ +{ + "name": "MagRicambi - Ricerca prezzi articolo", + "nodes": [ + { + "parameters": { + "content": "## MagRicambi โ€” Ricerca prezzi\n\n**Setup:**\n1. Credenziale Header Auth sul Webhook (`X-Webhook-Secret`)\n2. Credenziale Tavily su nodo HTTP\n3. Credenziale OpenAI\n4. Attiva workflow โ†’ copia Production URL in `N8N_PRICE_SEARCH_WEBHOOK_URL`\n\n**Chiamata da MagRicambi:**\n`POST /api/articoli/ricerca-prezzi` โ†’ proxy verso questo webhook", + "height": 320, + "width": 420 + }, + "id": "sticky-note-setup", + "name": "Note Setup", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [-80, 80] + }, + { + "parameters": { + "httpMethod": "POST", + "path": "magricambi-ricerca-prezzi", + "responseMode": "responseNode", + "authentication": "headerAuth", + "options": {} + }, + "id": "webhook-magricambi", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [240, 300], + "webhookId": "magricambi-ricerca-prezzi", + "credentials": { + "httpHeaderAuth": { + "id": "REPLACE_WITH_CREDENTIAL_ID", + "name": "MagRicambi Webhook Secret" + } + } + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "cond-descrizione-not-empty", + "leftValue": "={{ ($json.body?.descrizione ?? $json.descrizione ?? '').toString().trim() }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "if-descrizione-valida", + "name": "IF Descrizione valida", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [480, 300] + }, + { + "parameters": { + "jsCode": "const item = $input.first().json;\nconst body = item.body ?? item;\n\nconst descrizione = String(body.descrizione ?? '').trim();\nconst barcode = String(body.barcode ?? '').trim();\nconst codice = String(body.codice ?? '').trim();\n\nconst parts = [];\nif (barcode) parts.push(barcode);\nif (codice) parts.push(codice);\nif (descrizione) parts.push(descrizione);\nparts.push('prezzo acquisto ricambio industriale Italia EUR');\n\nreturn [{\n json: {\n descrizione,\n barcode: barcode || null,\n codice: codice || null,\n quantita: body.quantita ?? null,\n query: parts.join(' '),\n meta: body.meta ?? {},\n },\n}];" + }, + "id": "code-build-query", + "name": "Build Query", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [720, 200] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.tavily.com/search", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"query\": {{ JSON.stringify($json.query) }},\n \"search_depth\": \"basic\",\n \"max_results\": 8,\n \"include_answer\": false\n}", + "options": { + "timeout": 20000 + } + }, + "id": "http-tavily-search", + "name": "Tavily Search", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [960, 200], + "credentials": { + "httpHeaderAuth": { + "id": "REPLACE_WITH_CREDENTIAL_ID", + "name": "Tavily API" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "openAiApi", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"model\": \"gpt-4o-mini\",\n \"temperature\": 0.2,\n \"response_format\": { \"type\": \"json_object\" },\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Sei un assistente per acquisti industriali. Ricevi risultati di ricerca web. Estrai SOLO offerte con prezzo esplicito nei testi forniti. Se il prezzo non รจ chiaro usa price null e confidence low. Rispondi SOLO con JSON valido nel formato: {\\\"offers\\\":[{\\\"vendor\\\":\\\"\\\",\\\"title\\\":\\\"\\\",\\\"price\\\":null,\\\"currency\\\":\\\"EUR\\\",\\\"url\\\":\\\"\\\",\\\"shippingNote\\\":\\\"\\\",\\\"confidence\\\":\\\"high|medium|low\\\",\\\"source\\\":\\\"snippet|page|estimate\\\"}]}. Massimo 8 offerte. Valuta EUR. Nessun testo fuori dal JSON.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Query: \" + $('Build Query').first().json.query + \"\\n\\nRisultati ricerca:\\n\" + JSON.stringify($json.results ?? $json)\n }\n ]\n}", + "options": { + "timeout": 25000 + } + }, + "id": "http-openai-extract", + "name": "OpenAI Extract", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1200, 200], + "credentials": { + "openAiApi": { + "id": "REPLACE_WITH_CREDENTIAL_ID", + "name": "OpenAI API" + } + } + }, + { + "parameters": { + "jsCode": "const query = $('Build Query').first().json.query;\nconst descrizione = $('Build Query').first().json.descrizione;\n\nlet parsed = { offers: [] };\n\ntry {\n const item = $input.first().json;\n const raw =\n item.choices?.[0]?.message?.content ??\n item.message?.content ??\n item.text ??\n '{}';\n const clean = String(raw).replace(/```json|```/g, '').trim();\n parsed = JSON.parse(clean);\n} catch (e) {\n parsed = { offers: [] };\n}\n\nconst offers = (parsed.offers ?? [])\n .map((o) => ({\n vendor: String(o.vendor ?? 'Sconosciuto').slice(0, 120),\n title: String(o.title ?? descrizione).slice(0, 200),\n price:\n o.price === null || o.price === undefined || o.price === ''\n ? null\n : Number(o.price),\n currency: 'EUR',\n url: String(o.url ?? ''),\n shippingNote: o.shippingNote ? String(o.shippingNote) : undefined,\n confidence: ['high', 'medium', 'low'].includes(o.confidence)\n ? o.confidence\n : 'low',\n source: ['page', 'snippet', 'estimate'].includes(o.source)\n ? o.source\n : 'snippet',\n }))\n .filter((o) => o.url && o.url !== '#');\n\nreturn [{\n json: {\n query,\n searchedAt: new Date().toISOString(),\n offers,\n disclaimer:\n 'Prezzi indicativi da fonti web: verificare su ordine fornitore prima dell\\'acquisto.',\n },\n}];" + }, + "id": "code-format-response", + "name": "Format Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1440, 200] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": { + "responseCode": 200 + } + }, + "id": "respond-ok", + "name": "Respond OK", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [1680, 200] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { \"error\": \"descrizione obbligatoria\" } }}", + "options": { + "responseCode": 400 + } + }, + "id": "respond-errore-400", + "name": "Respond Errore 400", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [720, 420] + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "IF Descrizione valida", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF Descrizione valida": { + "main": [ + [ + { + "node": "Build Query", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Respond Errore 400", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Query": { + "main": [ + [ + { + "node": "Tavily Search", + "type": "main", + "index": 0 + } + ] + ] + }, + "Tavily Search": { + "main": [ + [ + { + "node": "OpenAI Extract", + "type": "main", + "index": 0 + } + ] + ] + }, + "OpenAI Extract": { + "main": [ + [ + { + "node": "Format Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Response": { + "main": [ + [ + { + "node": "Respond OK", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "settings": { + "executionOrder": "v1", + "saveManualExecutions": true, + "callerPolicy": "workflowsFromSameOwner", + "errorWorkflow": "" + }, + "staticData": null, + "tags": [ + { + "name": "magricambi" + }, + { + "name": "ricerca-prezzi" + } + ], + "triggerCount": 1, + "updatedAt": "2026-06-04T12:00:00.000Z", + "versionId": "magricambi-ricerca-prezzi-v1", + "meta": { + "templateCredsSetupCompleted": false, + "instanceId": "magricambi-local" + }, + "active": false +} \ No newline at end of file