{
  "id": "IuvYgHrzvtEck50d",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Automate flight Price Comparison",
  "tags": [],
  "nodes": [
    {
      "id": "d4a5235a-57c0-42f6-9841-8f45d04dfe1c",
      "name": "Webhook - Receive Flight Request",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1088,
        432
      ],
      "parameters": {
        "path": "flight-price-compare",
        "options": {
          "allowedOrigins": "*"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "81bb3dac-0cd3-4826-8436-573bc393f2c4",
      "name": "Parse & Validate Flight Request",
      "type": "n8n-nodes-base.code",
      "position": [
        -864,
        432
      ],
      "parameters": {
        "jsCode": "// Enhanced Flight Request Parser with NLP\nconst body = $input.first().json.body || {};\nconst query = body.message || body.query || '';\n\n// Extract user details\nconst userEmail = body.email || body.user_email || '';\nconst userName = body.name || body.user_name || 'Traveler';\nconst notifyPriceDrop = body.notify_price_drop || false;\n\n// Greeting handler\nconst greetings = ['hi', 'hello', 'hey', 'start', 'help'];\nif (greetings.some(g => query.toLowerCase().includes(g)) && query.split(' ').length < 5) {\n  return [{\n    json: {\n      status: 'greeting',\n      response: `Hi ${userName}! \u2708\ufe0f I'm your Flight Price Comparison Assistant.\\n\\nTell me:\\n\u2705 From city/airport\\n\u2705 To city/airport\\n\u2705 Departure date\\n\u2705 Trip type (one-way/round-trip)\\n\\nExample: \"Flight from New York to London on 25th March, one-way\"`,\n      userEmail,\n      userName\n    }\n  }];\n}\n\n// Airport/City codes mapping\nconst airportCodes = {\n  'new york': 'JFK', 'nyc': 'JFK', 'london': 'LHR', 'paris': 'CDG',\n  'dubai': 'DXB', 'singapore': 'SIN', 'tokyo': 'NRT', 'mumbai': 'BOM',\n  'delhi': 'DEL', 'bangalore': 'BLR', 'los angeles': 'LAX', 'chicago': 'ORD',\n  'san francisco': 'SFO', 'boston': 'BOS', 'miami': 'MIA', 'sydney': 'SYD',\n  'melbourne': 'MEL', 'hong kong': 'HKG', 'bangkok': 'BKK', 'amsterdam': 'AMS',\n  'frankfurt': 'FRA', 'toronto': 'YYZ', 'vancouver': 'YVR', 'seattle': 'SEA'\n};\n\nfunction getAirportCode(text) {\n  const lower = text.toLowerCase().trim();\n  if (/^[A-Z]{3}$/i.test(text)) return text.toUpperCase();\n  return airportCodes[lower] || text.toUpperCase();\n}\n\n// Parse dates\nfunction parseDate(text) {\n  const monthMap = {\n    jan: 0, january: 0, feb: 1, february: 1, mar: 2, march: 2,\n    apr: 3, april: 3, may: 4, jun: 5, june: 5,\n    jul: 6, july: 6, aug: 7, august: 7, sep: 8, september: 8,\n    oct: 9, october: 9, nov: 10, november: 10, dec: 11, december: 11\n  };\n\n  const dateRegex = /(\\d{1,2})(st|nd|rd|th)?\\s+(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)|\\d{4}-\\d{2}-\\d{2}/gi;\n  const matches = [...text.matchAll(dateRegex)];\n  const dates = [];\n  const currentYear = new Date().getFullYear();\n  \n  matches.forEach(match => {\n    if (match[0].includes('-')) {\n      dates.push(new Date(match[0]));\n    } else {\n      const day = parseInt(match[1]);\n      const monthStr = match[3].toLowerCase();\n      const month = monthMap[monthStr];\n      if (day && month !== undefined) {\n        dates.push(new Date(currentYear, month, day));\n      }\n    }\n  });\n  \n  return dates.sort((a, b) => a - b);\n}\n\n// Extract origin and destination\nlet origin = '';\nlet destination = '';\n\nconst routePattern = /(?:from|leaving)?\\s*([a-z\\s]{3,25})\\s+to\\s+([a-z\\s]{3,25})/i;\nconst routeMatch = query.match(routePattern);\n\nif (routeMatch) {\n  origin = routeMatch[1].trim();\n  destination = routeMatch[2].trim();\n} else {\n  origin = body.from || body.origin || body.departure_airport || '';\n  destination = body.to || body.destination || body.arrival_airport || '';\n}\n\nconst originCode = getAirportCode(origin);\nconst destinationCode = getAirportCode(destination);\n\nconst dates = parseDate(query + ' ' + (body.departure_date || ''));\nlet departureDate = dates[0] || null;\nlet returnDate = dates[1] || null;\n\nlet tripType = 'one-way';\nif (query.match(/round[\\s-]?trip|return/i) || returnDate) {\n  tripType = 'round-trip';\n}\nif (body.trip_type) {\n  tripType = body.trip_type.toLowerCase();\n}\n\nconst passengers = body.passengers || 1;\nconst cabinClass = body.class || body.cabin_class || 'economy';\n\nconst errors = [];\nif (!originCode || originCode.length < 3) errors.push('departure city/airport');\nif (!destinationCode || destinationCode.length < 3) errors.push('arrival city/airport');\nif (!departureDate) errors.push('departure date');\nif (tripType === 'round-trip' && !returnDate) errors.push('return date');\n\nif (errors.length > 0) {\n  return [{\n    json: {\n      status: 'missing_info',\n      response: `I need more information: ${errors.join(', ')}.\\n\\nExample: \"Flight from Mumbai to Dubai on 15th March, round-trip returning 20th March\"`,\n      userEmail,\n      userName\n    }\n  }];\n}\n\nconst formatDate = (date) => {\n  if (!date) return null;\n  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n  return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;\n};\n\nconst formatDateISO = (date) => {\n  if (!date) return null;\n  return date.toISOString().split('T')[0];\n};\n\nreturn [{\n  json: {\n    status: 'ready',\n    origin: originCode,\n    destination: destinationCode,\n    originCity: origin,\n    destinationCity: destination,\n    departureDate: formatDate(departureDate),\n    returnDate: returnDate ? formatDate(returnDate) : null,\n    departureDateISO: formatDateISO(departureDate),\n    returnDateISO: formatDateISO(returnDate),\n    tripType,\n    passengers,\n    cabinClass,\n    userEmail,\n    userName,\n    notifyPriceDrop,\n    originalQuery: query\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3f5bb362-ca6a-4c1d-a959-9f194fcc7e99",
      "name": "Check If Request Valid",
      "type": "n8n-nodes-base.if",
      "position": [
        -640,
        432
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.status}}",
              "value2": "ready"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "07df7d09-a38d-4540-b8f8-0e81405e086b",
      "name": "Scrape Kayak",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        48
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} kayak",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2acab75d-4d75-49d7-8c37-dc36740e7636",
      "name": "Scrape Skyscanner",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        240
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} skyscanner",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b74690a3-61b9-41df-967b-bf395fb6aade",
      "name": "Scrape Expedia",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        432
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} expedia",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "76e69f7a-9683-47ce-bdf0-b673c504a4b2",
      "name": "Scrape Google Flights",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        816
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} googleflights",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "020178b1-0de8-4cbc-812d-bec95ad92d8f",
      "name": "Aggregate & Analyze Prices",
      "type": "n8n-nodes-base.code",
      "position": [
        -64,
        336
      ],
      "parameters": {
        "jsCode": "// Aggregate flight prices from all platforms\nconst items = $input.all();\nconst searchData = items[0].json;\n\nconst flights = [];\nconst errors = [];\n\nconst platforms = ['Kayak', 'Skyscanner', 'Expedia', 'Google Flights'];\n\nitems.slice(1).forEach((item, index) => {\n  const platform = platforms[index];\n  const output = item.json.stdout || '';\n  \n  if (item.json.stderr || !output) {\n    errors.push(`${platform}: Unable to fetch`);\n    return;\n  }\n  \n  const lines = output.trim().split('\\n');\n  \n  lines.forEach(line => {\n    if (line.includes('|')) {\n      const parts = line.split('|');\n      if (parts.length >= 6) {\n        const price = parseFloat(parts[1].replace(/[^0-9.]/g, ''));\n        \n        if (price && price > 0) {\n          flights.push({\n            platform,\n            airline: parts[0].trim(),\n            price,\n            currency: parts[1].match(/[A-Z\u20b9$\u20ac\u00a3]/)?.[0] || '$',\n            duration: parts[2].trim(),\n            stops: parts[3].trim(),\n            departureTime: parts[4].trim(),\n            arrivalTime: parts[5].trim(),\n            bookingUrl: parts[6]?.trim() || '#',\n            cabinClass: searchData.cabinClass\n          });\n        }\n      }\n    }\n  });\n});\n\nif (flights.length === 0) {\n  return [{\n    json: {\n      status: 'no_results',\n      message: 'No flights found for your search criteria.',\n      errors,\n      ...searchData\n    }\n  }];\n}\n\nflights.sort((a, b) => a.price - b.price);\n\nconst bestDeal = flights[0];\nconst avgPrice = flights.reduce((sum, f) => sum + f.price, 0) / flights.length;\nconst maxPrice = flights[flights.length - 1].price;\nconst savings = maxPrice - bestDeal.price;\n\nconst directFlights = flights.filter(f => f.stops === '0' || f.stops.toLowerCase().includes('non'));\nconst bestDirectFlight = directFlights.length > 0 ? directFlights[0] : null;\n\nreturn [{\n  json: {\n    status: 'success',\n    ...searchData,\n    results: flights,\n    bestDeal,\n    bestDirectFlight,\n    avgPrice: Math.round(avgPrice),\n    maxPrice: Math.round(maxPrice),\n    savings: Math.round(savings),\n    totalResults: flights.length,\n    directFlightsCount: directFlights.length,\n    errors,\n    searchTimestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b195bb98-d48b-442d-8ceb-7809a2f65d15",
      "name": "Format Email Report",
      "type": "n8n-nodes-base.code",
      "position": [
        256,
        336
      ],
      "parameters": {
        "jsCode": "// Format email report\nconst data = $input.first().json;\n\nif (data.status === 'no_results') {\n  return [{\n    json: {\n      subject: `\u274c No Flights - ${data.origin} to ${data.destination}`,\n      text: `No flights found for ${data.origin} to ${data.destination} on ${data.departureDate}`,\n      ...data\n    }\n  }];\n}\n\nconst { origin, destination, departureDate, returnDate, tripType, passengers, results, bestDeal, avgPrice, savings } = data;\n\nconst topResults = results.slice(0, 10);\nconst resultsText = topResults.map((f, i) => \n  `${i + 1}. ${f.airline} - ${f.currency}${f.price} (${f.stops === '0' ? 'Non-stop' : f.stops + ' stop(s)'}) - ${f.platform}`\n).join('\\n');\n\nconst textReport = `\nFLIGHT PRICE COMPARISON\n${'='.repeat(50)}\n\nRoute: ${origin} \u2192 ${destination}\nDeparture: ${departureDate}\n${returnDate ? `Return: ${returnDate}\\n` : ''}Trip Type: ${tripType}\nPassengers: ${passengers}\n\n\ud83c\udfc6 BEST DEAL\n${'-'.repeat(50)}\n${bestDeal.airline}\nPrice: ${bestDeal.currency}${bestDeal.price}\nDuration: ${bestDeal.duration}\nStops: ${bestDeal.stops === '0' ? 'Non-stop' : bestDeal.stops + ' stop(s)'}\nPlatform: ${bestDeal.platform}\n${savings > 0 ? `\\n\ud83d\udcb0 Save ${bestDeal.currency}${savings} vs highest price!` : ''}\n\n\ud83d\udcca ALL RESULTS (Top 10)\n${'-'.repeat(50)}\n${resultsText}\n\nAverage Price: ${bestDeal.currency}${avgPrice}\nTotal Results: ${results.length}\n\nPrices subject to availability.\nHappy travels! \u2708\ufe0f\n`;\n\nreturn [{\n  json: {\n    subject: `\u2708\ufe0f ${origin} \u2192 ${destination} - Best: ${bestDeal.currency}${bestDeal.price}`,\n    text: textReport,\n    ...data\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "994f5fa6-2e45-42f7-af12-f98026250c04",
      "name": "Send Email Report",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        480,
        336
      ],
      "parameters": {
        "text": "={{$json.text}}",
        "options": {},
        "subject": "={{$json.subject}}",
        "toEmail": "={{$json.userEmail}}",
        "fromEmail": "user@example.com",
        "emailFormat": "text"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "8b3fbdb5-879f-4a02-8927-8632f944bd53",
      "name": "Webhook Response (Success)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        784,
        336
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  \"success\": true,\n  \"message\": \"Flight comparison sent to \" + $json.userEmail,\n  \"route\": $json.origin + \" \u2192 \" + $json.destination,\n  \"bestPrice\": $json.bestDeal.price,\n  \"airline\": $json.bestDeal.airline,\n  \"totalResults\": $json.totalResults\n} }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d1d130de-f7f2-42ed-9f2b-591446329e6c",
      "name": "Webhook Response (Error)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -416,
        624
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  \"success\": false,\n  \"message\": $json.response || \"Request failed\",\n  \"status\": $json.status\n} }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "35dd7ec1-cb14-4b34-9138-0d8e8af2380a",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1184,
        -624
      ],
      "parameters": {
        "color": 4,
        "width": 484,
        "height": 476,
        "content": "## \ud83c\udfaf Workflow Purpose\n\n**Smart Flight Price Comparison**\n\nAutomatically compares flight prices across multiple booking platforms and sends detailed reports via email.\n\n### Key Features:\n\u2705 Natural language input\n\u2705 Multi-platform scraping\n\u2705 Best deal identification  \n\u2705 Email reports\n\u2705 Real-time responses\n\n### Platforms Compared:\n- Kayak\n- Skyscanner\n- Expedia\n- Google Flights"
      },
      "typeVersion": 1
    },
    {
      "id": "c312076a-7442-41fe-ac65-692581acde7d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        96
      ],
      "parameters": {
        "color": 5,
        "width": 452,
        "height": 516,
        "content": "## \ud83d\udce5 INPUT STAGE\n\n**Webhook receives:**\n- Flight search query\n- User email\n- Alert preferences\n\n**Example:**\n```json\n{\n  \"message\": \"NYC to London March 25\",\n  \"email\": \"user@test.com\"\n}\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "f8aa3a61-314d-4b4e-83bb-477f50a7562a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 612,
        "content": "## \ud83e\udde0 PARSE STAGE\n\n**Extracts:**\n- Airport codes\n- Dates (ISO format)\n- Trip type\n- Passengers\n\n**Validates:**\n- Required fields\n- Date formats\n- Airport codes"
      },
      "typeVersion": 1
    },
    {
      "id": "32ea52fc-41ef-4f5c-bf12-c0b13b9a2866",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -508,
        -280
      ],
      "parameters": {
        "color": 5,
        "width": 280,
        "height": 1224,
        "content": "## \ud83d\udd0d SCRAPE STAGE\n\n**Parallel scraping:**\n- All platforms simultaneously\n- Continue on failures\n- 30s timeout per scraper\n\n**Output format:**\nAIRLINE|PRICE|DURATION|STOPS|TIME|TIME|URL"
      },
      "typeVersion": 1
    },
    {
      "id": "d503d1f7-06ca-4d75-96fc-b0a6d3fccb1a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        -32
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 660,
        "content": "## \ud83d\udcca ANALYZE STAGE\n\n**Processes:**\n- Parse all results\n- Find best deals\n- Calculate stats\n- Sort by price\n\n**Outputs:**\n- Best overall\n- Best direct\n- Avg price\n- Savings"
      },
      "typeVersion": 1
    },
    {
      "id": "b194c549-bdad-4f31-9ea3-5990caea9b84",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        0
      ],
      "parameters": {
        "color": 5,
        "width": 388,
        "height": 628,
        "content": "## \ud83d\udce7 REPORT STAGE\n\n**Email contains:**\n- Flight route & dates\n- Best deal highlight\n- Top 10 results\n- Price statistics\n- Booking links\n\n**Format:**\nPlain text (easy to read)"
      },
      "typeVersion": 1
    },
    {
      "id": "0e963069-8538-431f-977b-9832b8905af4",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        0
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 612,
        "content": "## \u2705 RESPONSE STAGE\n\n**Success:**\n- Best price found\n- Airline name\n- Total results\n- Email sent confirmation\n\n**Error:**\n- Helpful message\n- What's missing\n- Example format"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "8562102b-7099-407b-ae1f-69da37f8da99",
  "connections": {
    "Scrape Kayak": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Expedia": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Skyscanner": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Report": {
      "main": [
        [
          {
            "node": "Webhook Response (Success)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Email Report": {
      "main": [
        [
          {
            "node": "Send Email Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Google Flights": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Request Valid": {
      "main": [
        [
          {
            "node": "Scrape Kayak",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Skyscanner",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Expedia",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Google Flights",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Webhook Response (Error)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate & Analyze Prices": {
      "main": [
        [
          {
            "node": "Format Email Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Validate Flight Request": {
      "main": [
        [
          {
            "node": "Check If Request Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Flight Request": {
      "main": [
        [
          {
            "node": "Parse & Validate Flight Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}