N
n8n Store
Workflow Market
Smart Travel Package Finder - Search Flights & Hotels with Skyscanner-Booking.com

Smart Travel Package Finder - Search Flights & Hotels with Skyscanner-Booking.com

by cschin0 views

説明

Categories

📊 Productivity🤖 AI & Machine Learning

Nodes Used

n8n-nodes-base.setn8n-nodes-base.coden8n-nodes-base.coden8n-nodes-base.gmailn8n-nodes-base.mergen8n-nodes-base.webhookn8n-nodes-base.stickyNoten8n-nodes-base.stickyNoten8n-nodes-base.stickyNoten8n-nodes-base.httpRequest
Price無料
Views0
最終更新11/28/2025
workflow.json
{
  "id": "Q9TXNQXxpkuynljZ",
  "meta": {
    "instanceId": "b91e510ebae4127f953fd2f5f8d40d58ca1e71c746d4500c12ae86aad04c1502"
  },
  "name": "Smart Travel Package Finder - Search Flights & Hotels with Skyscanner-Booking.com",
  "tags": [],
  "nodes": [
    {
      "id": "e1964e90-e7d0-480d-8eed-36dd27d9d648",
      "name": "📥 Travel Request Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -640,
        -384
      ],
      "webhookId": "travel-itinerary-generator",
      "parameters": {
        "path": "travel-search",
        "options": {
          "rawBody": true
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "d8b0f0fc-3ab8-4f95-a9c8-6012ad56a9ab",
      "name": "📝 Parse & Validate Inputs",
      "type": "n8n-nodes-base.set",
      "position": [
        -480,
        -384
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "destination",
              "name": "destination",
              "type": "string",
              "value": "={{ $json.body.destination || 'Shanghai' }}"
            },
            {
              "id": "departure",
              "name": "departure",
              "type": "string",
              "value": "={{ $json.body.departure || 'New York' }}"
            },
            {
              "id": "checkInDate",
              "name": "checkInDate",
              "type": "string",
              "value": "={{ $json.body.checkInDate || '2025-12-01' }}"
            },
            {
              "id": "checkOutDate",
              "name": "checkOutDate",
              "type": "string",
              "value": "={{ $json.body.checkOutDate || '2025-12-08' }}"
            },
            {
              "id": "notificationEmail",
              "name": "notificationEmail",
              "type": "string",
              "value": "={{ $json.body.notificationEmail || $json.body.email }}"
            },
            {
              "id": "adults",
              "name": "adults",
              "type": "number",
              "value": "={{ $json.body.adults || 1 }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "89686042-6637-485a-ae2a-88eb5a2a7680",
      "name": "✈️ Search Flights (Skyscanner)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        -496
      ],
      "parameters": {
        "url": "https://sky-scrapper.p.rapidapi.com/api/v1/flights/searchFlights",
        "options": {
          "timeout": 30000
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "originSkyId",
              "value": "={{ $json.departure }}"
            },
            {
              "name": "destinationSkyId",
              "value": "={{ $json.destination }}"
            },
            {
              "name": "originEntityId",
              "value": "27537542"
            },
            {
              "name": "destinationEntityId",
              "value": "27537579"
            },
            {
              "name": "date",
              "value": "={{ $json.checkInDate }}"
            },
            {
              "name": "adults",
              "value": "={{ $json.adults }}"
            },
            {
              "name": "currency",
              "value": "USD"
            },
            {
              "name": "market",
              "value": "en-US"
            },
            {
              "name": "countryCode",
              "value": "US"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "f752c284-3bc5-430e-8193-da6876f52f66",
      "name": "🏨 Search Hotels (Booking.com)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        -288
      ],
      "parameters": {
        "url": "https://booking-com.p.rapidapi.com/v1/hotels/search",
        "options": {
          "timeout": 30000
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "dest_type",
              "value": "city"
            },
            {
              "name": "dest_id",
              "value": "-1746443"
            },
            {
              "name": "checkin_date",
              "value": "={{ $json.checkInDate }}"
            },
            {
              "name": "checkout_date",
              "value": "={{ $json.checkOutDate }}"
            },
            {
              "name": "adults_number",
              "value": "={{ $json.adults }}"
            },
            {
              "name": "order_by",
              "value": "price"
            },
            {
              "name": "filter_by_currency",
              "value": "USD"
            },
            {
              "name": "units",
              "value": "metric"
            },
            {
              "name": "room_number",
              "value": "1"
            },
            {
              "name": "page_number",
              "value": "0"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "6f48d727-b21c-4b6f-b9b5-b8b4de4fee95",
      "name": "🔀 Merge Flight & Hotel Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        -96,
        -496
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "5267fce7-f040-4a6c-8159-23351371c325",
      "name": "🧮 Generate Itinerary Combinations",
      "type": "n8n-nodes-base.code",
      "position": [
        80,
        -496
      ],
      "parameters": {
        "jsCode": "// Travel Itinerary Combination Engine\n// Combines flights and hotels into ranked packages\n\nconst inputData = $input.all();\n\n// Extract flight and hotel data from merged inputs\nlet flightData = [];\nlet hotelData = [];\nlet searchParams = {};\n\n// Parse the merged data\nfor (const item of inputData) {\n  if (item.json.data && item.json.data.itineraries) {\n    // Flight data\n    const flights = item.json.data.itineraries.results || [];\n    flightData = flights.slice(0, 10); // Top 10 flights\n  } else if (item.json.result) {\n    // Hotel data\n    const hotels = item.json.result || [];\n    hotelData = hotels.slice(0, 10); // Top 10 hotels\n  }\n  \n  // Capture search parameters\n  if (item.json.destination) {\n    searchParams = {\n      destination: item.json.destination,\n      departure: item.json.departure,\n      checkInDate: item.json.checkInDate,\n      checkOutDate: item.json.checkOutDate,\n      notificationEmail: item.json.notificationEmail,\n      adults: item.json.adults || 1\n    };\n  }\n}\n\n// Fallback: Create mock data if APIs failed\nif (flightData.length === 0) {\n  console.log('No flight data - using mock data');\n  flightData = [\n    {\n      id: 'mock-flight-1',\n      price: { raw: 650, formatted: '$650' },\n      legs: [{\n        origin: { displayCode: searchParams.departure || 'NYC' },\n        destination: { displayCode: searchParams.destination || 'PVG' },\n        departure: searchParams.checkInDate + 'T08:00:00',\n        arrival: searchParams.checkInDate + 'T14:30:00',\n        durationInMinutes: 870,\n        carriers: { marketing: [{ name: 'United Airlines' }] }\n      }]\n    },\n    {\n      id: 'mock-flight-2',\n      price: { raw: 720, formatted: '$720' },\n      legs: [{\n        origin: { displayCode: searchParams.departure || 'NYC' },\n        destination: { displayCode: searchParams.destination || 'PVG' },\n        departure: searchParams.checkInDate + 'T10:30:00',\n        arrival: searchParams.checkInDate + 'T17:00:00',\n        durationInMinutes: 870,\n        carriers: { marketing: [{ name: 'Delta Airlines' }] }\n      }]\n    },\n    {\n      id: 'mock-flight-3',\n      price: { raw: 580, formatted: '$580' },\n      legs: [{\n        origin: { displayCode: searchParams.departure || 'NYC' },\n        destination: { displayCode: searchParams.destination || 'PVG' },\n        departure: searchParams.checkInDate + 'T14:00:00',\n        arrival: searchParams.checkInDate + 'T20:30:00',\n        durationInMinutes: 870,\n        carriers: { marketing: [{ name: 'American Airlines' }] }\n      }]\n    }\n  ];\n}\n\nif (hotelData.length === 0) {\n  console.log('No hotel data - using mock data');\n  const nights = calculateNights(searchParams.checkInDate, searchParams.checkOutDate);\n  hotelData = [\n    {\n      hotel_id: 'mock-hotel-1',\n      hotel_name: 'Shanghai Grand Hotel',\n      price_breakdown: { gross_price: 150 * nights },\n      review_score: 8.5,\n      address: 'Pudong District, Shanghai',\n      url: 'https://booking.com/hotel-1',\n      pricePerNight: 150,\n      totalNights: nights\n    },\n    {\n      hotel_id: 'mock-hotel-2',\n      hotel_name: 'Bund Riverside Inn',\n      price_breakdown: { gross_price: 120 * nights },\n      review_score: 8.2,\n      address: 'The Bund, Shanghai',\n      url: 'https://booking.com/hotel-2',\n      pricePerNight: 120,\n      totalNights: nights\n    },\n    {\n      hotel_id: 'mock-hotel-3',\n      hotel_name: 'Lujiazui Business Hotel',\n      price_breakdown: { gross_price: 180 * nights },\n      review_score: 8.8,\n      address: 'Lujiazui, Shanghai',\n      url: 'https://booking.com/hotel-3',\n      pricePerNight: 180,\n      totalNights: nights\n    }\n  ];\n}\n\n// Calculate nights\nfunction calculateNights(checkIn, checkOut) {\n  const start = new Date(checkIn);\n  const end = new Date(checkOut);\n  return Math.ceil((end - start) / (1000 * 60 * 60 * 24));\n}\n\nconst nights = calculateNights(searchParams.checkInDate, searchParams.checkOutDate);\n\n// Process and normalize flight data\nconst processedFlights = flightData.map(flight => {\n  const leg = flight.legs ? flight.legs[0] : {};\n  return {\n    id: flight.id,\n    airline: leg.carriers?.marketing?.[0]?.name || 'Unknown Airline',\n    origin: leg.origin?.displayCode || searchParams.departure,\n    destination: leg.destination?.displayCode || searchParams.destination,\n    departure: leg.departure || searchParams.checkInDate,\n    arrival: leg.arrival || searchParams.checkInDate,\n    duration: leg.durationInMinutes ? `${Math.floor(leg.durationInMinutes / 60)}h ${leg.durationInMinutes % 60}m` : 'N/A',\n    price: flight.price?.raw || 0,\n    priceFormatted: flight.price?.formatted || `$${flight.price?.raw || 0}`,\n    bookingLink: `https://www.skyscanner.com/transport/flights/${leg.origin?.displayCode}/${leg.destination?.displayCode}`\n  };\n});\n\n// Process and normalize hotel data\nconst processedHotels = hotelData.map(hotel => {\n  const totalPrice = hotel.price_breakdown?.gross_price || hotel.pricePerNight * nights || 0;\n  return {\n    id: hotel.hotel_id,\n    name: hotel.hotel_name || 'Unknown Hotel',\n    rating: hotel.review_score || 0,\n    address: hotel.address || searchParams.destination,\n    pricePerNight: hotel.pricePerNight || Math.round(totalPrice / nights),\n    totalPrice: totalPrice,\n    nights: nights,\n    bookingLink: hotel.url || `https://www.booking.com/hotel/${hotel.hotel_id}.html`\n  };\n});\n\n// Create all possible combinations\nconst itineraries = [];\nfor (const flight of processedFlights) {\n  for (const hotel of processedHotels) {\n    itineraries.push({\n      id: `${flight.id}-${hotel.id}`,\n      flight: flight,\n      hotel: hotel,\n      totalPrice: flight.price + hotel.totalPrice,\n      savings: 0 // Will calculate after sorting\n    });\n  }\n}\n\n// Sort by total price (cheapest first)\nitineraries.sort((a, b) => a.totalPrice - b.totalPrice);\n\n// Calculate savings compared to most expensive option\nconst mostExpensive = itineraries[itineraries.length - 1].totalPrice;\nitineraries.forEach(item => {\n  item.savings = mostExpensive - item.totalPrice;\n});\n\n// Get top 5 itineraries\nconst topItineraries = itineraries.slice(0, 5);\n\n// Return result with metadata\nreturn [{\n  json: {\n    searchParams: searchParams,\n    itineraries: topItineraries,\n    totalCombinations: itineraries.length,\n    flightsFound: processedFlights.length,\n    hotelsFound: processedHotels.length,\n    generatedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "80f6f70c-8df0-42db-ba0c-97def58e5a36",
      "name": "🎨 Format HTML Email",
      "type": "n8n-nodes-base.code",
      "position": [
        272,
        -496
      ],
      "parameters": {
        "jsCode": "// HTML Email Generator for Travel Itineraries\n\nconst data = $input.first().json;\nconst { searchParams, itineraries, totalCombinations, flightsFound, hotelsFound } = data;\n\n// Format currency\nfunction formatCurrency(amount) {\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD'\n  }).format(amount);\n}\n\n// Format date\nfunction formatDate(dateString) {\n  const date = new Date(dateString);\n  return date.toLocaleDateString('en-US', {\n    weekday: 'short',\n    year: 'numeric',\n    month: 'short',\n    day: 'numeric'\n  });\n}\n\n// Format time\nfunction formatTime(dateString) {\n  const date = new Date(dateString);\n  return date.toLocaleTimeString('en-US', {\n    hour: '2-digit',\n    minute: '2-digit'\n  });\n}\n\n// Generate itinerary cards HTML\nfunction generateItineraryCards() {\n  return itineraries.map((itinerary, index) => {\n    const ranking = index + 1;\n    const badge = ranking === 1 ? '<span style=\"background: #10b981; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: 10px;\">🏆 BEST VALUE</span>' : '';\n    \n    return `\n      <div style=\"background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: ${ranking === 1 ? '3px solid #10b981' : '2px solid #e5e7eb'};\">\n        <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;\">\n          <h2 style=\"margin: 0; color: #1f2937; font-size: 20px;\">Option ${ranking}${badge}</h2>\n          <div style=\"text-align: right;\">\n            <div style=\"font-size: 28px; font-weight: bold; color: #10b981;\">${formatCurrency(itinerary.totalPrice)}</div>\n            ${itinerary.savings > 0 ? `<div style=\"color: #6b7280; font-size: 14px;\">Save ${formatCurrency(itinerary.savings)}</div>` : ''}\n          </div>\n        </div>\n        \n        <!-- Flight Section -->\n        <div style=\"background: #f9fafb; border-radius: 8px; padding: 16px; margin-bottom: 16px;\">\n          <div style=\"display: flex; align-items: center; margin-bottom: 12px;\">\n            <span style=\"font-size: 20px; margin-right: 8px;\">✈️</span>\n            <h3 style=\"margin: 0; color: #374151; font-size: 16px;\">Flight Details</h3>\n          </div>\n          <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;\">\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Airline</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${itinerary.flight.airline}</div>\n            </div>\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Duration</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${itinerary.flight.duration}</div>\n            </div>\n          </div>\n          <div style=\"display: flex; justify-content: space-between; align-items: center; padding: 12px; background: white; border-radius: 6px;\">\n            <div>\n              <div style=\"font-size: 18px; font-weight: bold; color: #1f2937;\">${itinerary.flight.origin}</div>\n              <div style=\"color: #6b7280; font-size: 13px;\">${formatDate(itinerary.flight.departure)}</div>\n              <div style=\"color: #374151; font-size: 14px; font-weight: 600;\">${formatTime(itinerary.flight.departure)}</div>\n            </div>\n            <div style=\"color: #9ca3af; font-size: 20px;\">→</div>\n            <div style=\"text-align: right;\">\n              <div style=\"font-size: 18px; font-weight: bold; color: #1f2937;\">${itinerary.flight.destination}</div>\n              <div style=\"color: #6b7280; font-size: 13px;\">${formatDate(itinerary.flight.arrival)}</div>\n              <div style=\"color: #374151; font-size: 14px; font-weight: 600;\">${formatTime(itinerary.flight.arrival)}</div>\n            </div>\n          </div>\n          <div style=\"margin-top: 12px; display: flex; justify-content: space-between; align-items: center;\">\n            <div style=\"color: #1f2937; font-weight: 600; font-size: 16px;\">${itinerary.flight.priceFormatted}</div>\n            <a href=\"${itinerary.flight.bookingLink}\" style=\"background: #3b82f6; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 600;\">Book Flight</a>\n          </div>\n        </div>\n        \n        <!-- Hotel Section -->\n        <div style=\"background: #f9fafb; border-radius: 8px; padding: 16px;\">\n          <div style=\"display: flex; align-items: center; margin-bottom: 12px;\">\n            <span style=\"font-size: 20px; margin-right: 8px;\">🏨</span>\n            <h3 style=\"margin: 0; color: #374151; font-size: 16px;\">Hotel Details</h3>\n          </div>\n          <div style=\"margin-bottom: 12px;\">\n            <div style=\"font-size: 16px; font-weight: bold; color: #1f2937; margin-bottom: 4px;\">${itinerary.hotel.name}</div>\n            <div style=\"color: #6b7280; font-size: 13px; margin-bottom: 4px;\">${itinerary.hotel.address}</div>\n            <div style=\"color: #f59e0b; font-size: 14px;\">⭐ ${itinerary.hotel.rating.toFixed(1)} / 10</div>\n          </div>\n          <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px; background: white; border-radius: 6px; margin-bottom: 12px;\">\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Nightly Rate</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${formatCurrency(itinerary.hotel.pricePerNight)}</div>\n            </div>\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Total (${itinerary.hotel.nights} nights)</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${formatCurrency(itinerary.hotel.totalPrice)}</div>\n            </div>\n          </div>\n          <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n            <div style=\"color: #6b7280; font-size: 13px;\">${formatDate(searchParams.checkInDate)} - ${formatDate(searchParams.checkOutDate)}</div>\n            <a href=\"${itinerary.hotel.bookingLink}\" style=\"background: #10b981; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 600;\">Book Hotel</a>\n          </div>\n        </div>\n      </div>\n    `;\n  }).join('');\n}\n\n// Generate comparison table\nfunction generateComparisonTable() {\n  const rows = itineraries.map((itinerary, index) => `\n    <tr style=\"${index % 2 === 0 ? 'background: #f9fafb;' : 'background: white;'}\">\n      <td style=\"padding: 12px; text-align: center; font-weight: 600; color: #1f2937;\">${index + 1}</td>\n      <td style=\"padding: 12px; color: #374151;\">${itinerary.flight.airline}</td>\n      <td style=\"padding: 12px; color: #374151;\">${itinerary.hotel.name}</td>\n      <td style=\"padding: 12px; text-align: right; color: #6b7280;\">${itinerary.flight.priceFormatted}</td>\n      <td style=\"padding: 12px; text-align: right; color: #6b7280;\">${formatCurrency(itinerary.hotel.totalPrice)}</td>\n      <td style=\"padding: 12px; text-align: right; font-weight: bold; color: #10b981; font-size: 16px;\">${formatCurrency(itinerary.totalPrice)}</td>\n    </tr>\n  `).join('');\n  \n  return `\n    <table style=\"width: 100%; border-collapse: collapse; margin: 24px 0; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.05);\">\n      <thead>\n        <tr style=\"background: #1f2937; color: white;\">\n          <th style=\"padding: 14px; text-align: center; font-weight: 600;\">#</th>\n          <th style=\"padding: 14px; text-align: left; font-weight: 600;\">Flight</th>\n          <th style=\"padding: 14px; text-align: left; font-weight: 600;\">Hotel</th>\n          <th style=\"padding: 14px; text-align: right; font-weight: 600;\">Flight Price</th>\n          <th style=\"padding: 14px; text-align: right; font-weight: 600;\">Hotel Price</th>\n          <th style=\"padding: 14px; text-align: right; font-weight: 600;\">Total</th>\n        </tr>\n      </thead>\n      <tbody>\n        ${rows}\n      </tbody>\n    </table>\n  `;\n}\n\n// Generate complete HTML email\nconst htmlEmail = `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Your Travel Itinerary Options</title>\n</head>\n<body style=\"margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f3f4f6;\">\n  <div style=\"max-width: 700px; margin: 0 auto; padding: 20px;\">\n    \n    <!-- Header -->\n    <div style=\"background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 32px; text-align: center; margin-bottom: 24px; color: white;\">\n      <h1 style=\"margin: 0 0 12px 0; font-size: 32px; font-weight: bold;\">✈️ Your Travel Options</h1>\n      <p style=\"margin: 0; font-size: 18px; opacity: 0.95;\">${searchParams.departure} → ${searchParams.destination}</p>\n      <p style=\"margin: 8px 0 0 0; font-size: 14px; opacity: 0.9;\">${formatDate(searchParams.checkInDate)} - ${formatDate(searchParams.checkOutDate)}</p>\n    </div>\n    \n    <!-- Summary Stats -->\n    <div style=\"background: white; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);\">\n      <div style=\"display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; text-align: center;\">\n        <div>\n          <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">OPTIONS FOUND</div>\n          <div style=\"font-size: 24px; font-weight: bold; color: #3b82f6;\">${itineraries.length}</div>\n        </div>\n        <div>\n          <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">BEST PRICE</div>\n          <div style=\"font-size: 24px; font-weight: bold; color: #10b981;\">${formatCurrency(itineraries[0].totalPrice)}</div>\n        </div>\n        <div>\n          <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">MAX SAVINGS</div>\n          <div style=\"font-size: 24px; font-weight: bold; color: #f59e0b;\">${formatCurrency(itineraries[0].savings)}</div>\n        </div>\n      </div>\n    </div>\n    \n    <!-- Itinerary Cards -->\n    <div>\n      <h2 style=\"color: #1f2937; font-size: 22px; margin-bottom: 16px;\">📋 Recommended Packages</h2>\n      ${generateItineraryCards()}\n    </div>\n    \n    <!-- Comparison Table -->\n    <div style=\"margin-top: 32px;\">\n      <h2 style=\"color: #1f2937; font-size: 22px; margin-bottom: 16px;\">📊 Quick Comparison</h2>\n      ${generateComparisonTable()}\n    </div>\n    \n    <!-- Footer -->\n    <div style=\"background: #f9fafb; border-radius: 12px; padding: 20px; margin-top: 24px; text-align: center; color: #6b7280; font-size: 13px;\">\n      <p style=\"margin: 0 0 8px 0;\">✨ Generated by Smart Travel Itinerary System</p>\n      <p style=\"margin: 0;\">Analyzed ${totalCombinations} combinations from ${flightsFound} flights and ${hotelsFound} hotels</p>\n      <p style=\"margin: 8px 0 0 0; font-size: 11px; color: #9ca3af;\">Prices are subject to availability and may change</p>\n    </div>\n    \n  </div>\n</body>\n</html>\n`;\n\nreturn [{\n  json: {\n    subject: `🎉 ${itineraries.length} Travel Options: ${searchParams.departure} → ${searchParams.destination}`,\n    htmlBody: htmlEmail,\n    recipient: searchParams.notificationEmail,\n    itinerariesCount: itineraries.length,\n    bestPrice: formatCurrency(itineraries[0].totalPrice),\n    searchParams: searchParams\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7ec3fb5b-a73e-4e99-9019-0b241a2fe736",
      "name": "✉️ Send via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        448,
        -496
      ],
      "webhookId": "d0670a84-770f-4fe9-9be4-040e3b63b1f9",
      "parameters": {
        "sendTo": "={{ $json.recipient }}",
        "message": "={{ $json.htmlBody }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ecdc9e1c-ae6d-4094-af8f-d9a9c4bc102c",
      "name": "📤 Webhook Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        608,
        -496
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  \"success\": true,\n  \"message\": \"Travel itinerary email sent successfully!\",\n  \"itinerariesGenerated\": $json.itinerariesCount,\n  \"bestPrice\": $json.bestPrice,\n  \"sentTo\": $json.recipient,\n  \"searchDetails\": {\n    \"from\": $json.searchParams.departure,\n    \"to\": $json.searchParams.destination,\n    \"checkIn\": $json.searchParams.checkInDate,\n    \"checkOut\": $json.searchParams.checkOutDate\n  },\n  \"timestamp\": new Date().toISOString()\n} }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "b43f6af3-042e-4258-8006-04cb17fade14",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1360,
        -496
      ],
      "parameters": {
        "width": 720,
        "height": 336,
        "content": "## Introduction\nAutomates travel itinerary creation by searching flights and hotels via APIs, then uses AI to generate personalized recommendations delivered as HTML emails through Gmail.\n\n## How It Works\nWebhook receives travel requests, searches Skyscanner and Booking.com APIs in parallel, merges results, uses AI to create optimized itineraries, formats as HTML email, sends via Gmail.\n\n## Workflow Template\nWebhook → Parse & Validate → Parallel Searches (Flights: Skyscanner | Hotels: Booking.com) → Merge Data → AI Generate Itinerary → Format HTML Email → Send Gmail → Webhook Response\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b27a8a26-8e23-4b58-945c-01bf5d4e8fe9",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        -352
      ],
      "parameters": {
        "color": 3,
        "width": 752,
        "height": 528,
        "content": "## Setup Instructions\n1. API Configuration: Add Skyscanner and Booking.com API credentials in n8n.\n2. Gmail Setup: Configure OAuth2 authentication.\n3. Customization: Copy webhook URL, adjust validation rules, modify AI prompts and HTML template.\n## Prerequisites\n- Skyscanner API key\n- Booking.com API credentials\n- Gmail with OAuth2\n- n8n instance\n## Use Cases\n- Personal vacation planning\n- Business travel arrangements\n## Customization\n- Add APIs (Kiwi, Expedia)\n- Filter by budget, Modify email design\n## Benefits\n- Saves 2-3 hours per trip\n- Real-time pricing comparison\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2d4004f8-8e1c-477c-8669-6facc12b7d85",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1360,
        -128
      ],
      "parameters": {
        "color": 6,
        "width": 720,
        "height": 224,
        "content": "## Workflow Steps\n1. Trigger & Validate: Webhook receives request, extracts destination/dates/budget/preferences, validates data, converts to API parameters.\n2. Parallel Search: Skyscanner fetches flights with price/duration/airline. Booking.com retrieves hotels with ratings/pricing. Merge combines both into single JSON object.\n3. AI Generation: AI analyzes merged data, evaluates by price/duration/rating, creates itinerary with daily schedule, pairings, costs, and rankings.\n4. Delivery: Converts JSON to HTML email with tables and booking links. Gmail sends email. Webhook confirms success."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5ed5ab9a-b808-494a-9bac-8919d45e814c",
  "connections": {
    "✉️ Send via Gmail": {
      "main": [
        [
          {
            "node": "📤 Webhook Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🎨 Format HTML Email": {
      "main": [
        [
          {
            "node": "✉️ Send via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "📥 Travel Request Webhook": {
      "main": [
        [
          {
            "node": "📝 Parse & Validate Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "📝 Parse & Validate Inputs": {
      "main": [
        [
          {
            "node": "✈️ Search Flights (Skyscanner)",
            "type": "main",
            "index": 0
          },
          {
            "node": "🏨 Search Hotels (Booking.com)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🔀 Merge Flight & Hotel Data": {
      "main": [
        [
          {
            "node": "🧮 Generate Itinerary Combinations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🏨 Search Hotels (Booking.com)": {
      "main": [
        [
          {
            "node": "🔀 Merge Flight & Hotel Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "✈️ Search Flights (Skyscanner)": {
      "main": [
        [
          {
            "node": "🔀 Merge Flight & Hotel Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🧮 Generate Itinerary Combinations": {
      "main": [
        [
          {
            "node": "🎨 Format HTML Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

相关工作流