AutomationFlowsEmail & Gmail › Ts Order Ingest

Ts Order Ingest

Ts Order Ingest. Uses httpRequest, itemLists, stopAndError, airtable. Manual trigger; 52 nodes.

Manual trigger★★★★★ complexity52 nodesHTTP RequestItem ListsStop And ErrorAirtableSlackGmailGoogle Drive
Email & Gmail Trigger: Manual Nodes: 52 Complexity: ★★★★★ Added:

This workflow follows the Airtable → Gmail recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "parameters": {
        "url": "https://transfer-superstars.myshopify.com/admin/api/2023-10/orders.json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "shopifyAccessTokenApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "limit",
              "value": "10"
            },
            {
              "name": "status",
              "value": "any"
            },
            {
              "name": "created_at_min",
              "value": "={{ $now.minus(2, 'day').toISO() }}"
            },
            {
              "name": "order",
              "value": "created_at desc"
            }
          ]
        },
        "options": {}
      },
      "id": "4c6e5ed0-4f13-4b5e-98c2-ddb8c08d4d42",
      "name": "Shopify HTTP",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        -1580,
        -420
      ],
      "credentials": {
        "shopifyAccessTokenApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "fieldToSplitOut": "orders",
        "options": {}
      },
      "id": "93de4b83-572d-4596-a455-f6a2ff14013b",
      "name": "Split Orders",
      "type": "n8n-nodes-base.itemLists",
      "typeVersion": 1,
      "position": [
        -1380,
        -420
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "numberInputs": 3
      },
      "id": "ea553eb9-8979-4d7d-af5a-b3298fab7831",
      "name": "Merge \u2192 Combine",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        -680,
        -100
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Enhanced Source Detection and Shipment Preparation\nconst items = $input.all();\n\nreturn items.map(item => {\n  const d = item.json;\n  \n  // More robust source detection with additional checks\n  let source = 'unknown';\n  let shipmentData = {};\n  \n  // Shopify Detection\n  if (d.line_items && Array.isArray(d.line_items)) {\n    source = 'shopify';\n    \n    // Extract Shopify-specific shipment data\n    shipmentData = {\n      orderId: d.name || d.id,\n      orderNumber: d.order_number,\n      fulfillmentStatus: d.fulfillment_status,\n      shippingAddress: d.shipping_address,\n      customer: {\n        email: d.email || d.customer?.email,\n        name: d.shipping_address?.name || `${d.customer?.first_name} ${d.customer?.last_name}`,\n        phone: d.shipping_address?.phone || d.customer?.phone\n      },\n      items: d.line_items?.map(item => ({\n        name: item.name,\n        quantity: item.quantity,\n        sku: item.sku,\n        variant: item.variant_title\n      })),\n      financial: {\n        subtotal: parseFloat(d.subtotal_price || 0),\n        shipping: parseFloat(d.total_shipping_price_set?.shop_money?.amount || 0),\n        tax: parseFloat(d.total_tax || 0),\n        total: parseFloat(d.total_price || 0)\n      }\n    };\n  }\n  \n  // JotForm Detection\n  else if (d.answers && typeof d.answers === 'object') {\n    source = 'jotform';\n    \n    // Extract JotForm-specific shipment data\n    shipmentData = {\n      orderId: d.submissionID || d.id,\n      submissionId: d.submissionID,\n      customer: {\n        email: d.answers?.email?.answer || d.email,\n        name: d.answers?.name?.answer || '',\n        phone: d.answers?.phone?.answer || ''\n      },\n      shippingAddress: {\n        // Parse from JotForm address fields if available\n        name: d.answers?.shippingName?.answer || d.answers?.name?.answer,\n        address1: d.answers?.address?.answer?.addr_line1,\n        address2: d.answers?.address?.answer?.addr_line2,\n        city: d.answers?.address?.answer?.city,\n        state: d.answers?.address?.answer?.state,\n        zip: d.answers?.address?.answer?.postal,\n        country: d.answers?.address?.answer?.country || 'US'\n      },\n      items: [], // Would need to parse from form answers\n      financial: {\n        total: parseFloat(d.answers?.total?.answer || 0)\n      }\n    };\n  }\n  \n  // Jiffy Email Detection (enhanced with your preprocessing data)\n  else if (d.orderId && d.orderId.startsWith('JIFFY-')) {\n    source = 'jiffy';\n    \n    // Use the preprocessed Jiffy data structure\n    shipmentData = {\n      orderId: d.orderId,\n      poNumber: d.poNumber || d.jiffyPoNumber,\n      orderType: d.orderType,\n      rushService: d.rushService,\n      customer: d.customer,\n      shippingAddress: d.shipping,\n      shippingDate: d.shippingDate,\n      items: d.items,\n      financial: d.financial,\n      \n      // Critical shipping label info\n      shippingLabel: d.shippingLabel,\n      shipping_label_url: d.shipping_label_url || d.shipping_label_google_drive,\n      \n      // File information\n      files: d.files,\n      gangSheetUrls: d.gangSheetUrls,\n      cutlineReferences: d.cutlineReferences,  // ADDED: Cutline references\n      \n      // Status\n      paymentStatus: d.paymentStatus,\n      fileStatus: d.fileStatus\n    };\n  }\n  \n  // Legacy Jiffy detection (for emails not yet preprocessed)\n  else if (d.id && (d.subject || d.snippet || d.text) && \n           (d.subject?.includes('Jiffy') || d.from?.includes('jiffy'))) {\n    source = 'jiffy';\n    \n    // Basic shipment data from raw email\n    shipmentData = {\n      orderId: `JIFFY-${Date.now()}`,\n      emailId: d.id,\n      subject: d.subject,\n      requiresProcessing: true\n    };\n  }\n  \n  // Additional source detection patterns\n  else if (d.source) {\n    // If source is already defined, use it\n    source = d.source;\n    shipmentData = {\n      orderId: d.orderId || d.id,\n      ...d // Include all existing data\n    };\n  }\n  \n  // Build the final output structure\n  return {\n    json: {\n      // Root level source\n      source: source,\n      \n      // Shipment-ready data\n      shipment: {\n        source: source,\n        sourceData: shipmentData,\n        \n        // Common fields across all sources\n        orderId: shipmentData.orderId,\n        customer: shipmentData.customer || {},\n        shippingAddress: shipmentData.shippingAddress || {},\n        items: shipmentData.items || [],\n        financial: shipmentData.financial || {},\n        \n        // Shipping specific\n        shippingLabel: shipmentData.shippingLabel,\n        shipping_label_url: shipmentData.shipping_label_url,\n        rushService: shipmentData.rushService,\n        \n        // File information - ADDED cutlineReferences\n        files: shipmentData.files,\n        gangSheetUrls: shipmentData.gangSheetUrls,\n        cutlineReferences: shipmentData.cutlineReferences,\n        \n        // Meta\n        createdAt: new Date().toISOString(),\n        requiresProcessing: shipmentData.requiresProcessing || false\n      },\n      \n      // Original payload for reference\n      payload: d,\n      \n      // Additional metadata\n      meta: {\n        processedAt: new Date().toISOString(),\n        hasShippingLabel: !!shipmentData.shipping_label_url,\n        hasCutlineReferences: !!(shipmentData.cutlineReferences && shipmentData.cutlineReferences.length > 0),\n        isReadyForShipment: !!(shipmentData.orderId && shipmentData.shippingAddress)\n      }\n    },\n    \n    // Preserve binary data if exists\n    binary: item.binary\n  };\n});"
      },
      "id": "51338e60-2c91-4dd9-9847-e32be5379211",
      "name": "Function - Detect Source",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -500,
        -100
      ]
    },
    {
      "parameters": {
        "dataType": "string",
        "value1": "={{ $json.source }}",
        "rules": {
          "rules": [
            {
              "value2": "shopify"
            },
            {
              "value2": "jotform",
              "output": 1
            },
            {
              "value2": "jiffy",
              "output": 2
            }
          ]
        },
        "fallbackOutput": 3
      },
      "id": "049fa0dd-400e-402e-ae2f-2600ec25d504",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 1,
      "position": [
        -320,
        -120
      ]
    },
    {
      "parameters": {
        "jsCode": "// Helper function to format addresses with proper title case\nfunction formatAddress(address) {\n  if (!address) return '';\n  \n  // Replace line breaks with comma and space, clean up extra spaces\n  const cleaned = address\n    .replace(/\\r?\\n/g, ', ')  // Replace line breaks with comma\n    .replace(/,\\s*,/g, ',')   // Remove duplicate commas\n    .replace(/\\s+/g, ' ')     // Replace multiple spaces with single space\n    .trim();\n  \n  const states = ['AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA','KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY','DC'];\n  \n  return cleaned.split(/\\b/).map(part => {\n    const word = part.trim();\n    if (!word || /^[,\\s]+$/.test(part)) return part;\n    \n    // Keep state codes uppercase\n    if (states.includes(word.toUpperCase())) {\n      return word.toUpperCase();\n    }\n    \n    // Keep ZIP codes as-is\n    if (/^\\d{5}(-\\d{4})?$/.test(word)) {\n      return word;\n    }\n    \n    // Title case other words\n    if (/^[a-zA-Z]+$/.test(word)) {\n      return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();\n    }\n    \n    return word;\n  }).join('');\n}\n\n// Helper function to create formatted full address\nfunction createFullAddress(addressObj) {\n  if (!addressObj) return '';\n  \n  const parts = [\n    addressObj.address1,\n    addressObj.address2,\n    addressObj.city,\n    addressObj.province_code || addressObj.state,\n    addressObj.zip\n  ].filter(Boolean);\n  \n  return formatAddress(parts.join(', '));\n}\n\n// Helper function to extract filename from URL\nfunction extractFilenameFromUrl(url) {\n  if (!url) return '';\n  \n  try {\n    // Extract the last part of the URL path\n    const urlParts = url.split('/');\n    const filename = urlParts[urlParts.length - 1];\n    \n    // Remove query parameters if present\n    return filename.split('?')[0] || '';\n  } catch (e) {\n    return '';\n  }\n}\n\n// Helper function to determine if title is a default product name\nfunction isDefaultProductTitle(title) {\n  if (!title) return false;\n  \n  const defaultTitles = [\n    'DTF Transfer Online Gang Sheet Builder PRO For Clothing',\n    'DTF Transfer Online Gang Sheet Builder PRO',\n    'Gang Sheet Builder',\n    'DTF Online Builder',\n    'Online Gang Sheet Builder',\n    'UV Sticker Online Gang Sheet Builder',\n    'UV Online Builder',\n    'Sublimation Transfer Builder',\n    'Heat Press Builder',\n    'Custom Transfer Builder',\n    'Online Transfer Builder',\n    'Transfer Builder PRO',\n    'Custom Gang Sheet Builder'\n  ];\n  \n  return defaultTitles.some(defaultTitle => \n    title.toLowerCase().includes(defaultTitle.toLowerCase())\n  );\n}\n\n// Enhanced product type detection based on Airtable schema\nfunction detectProductType(lineItem) {\n  const title = (lineItem.title || '').toLowerCase();\n  const sku = (lineItem.sku || '').toLowerCase();\n  const variantTitle = (lineItem.variant_title || '').toLowerCase();\n  const properties = lineItem.properties || [];\n  \n  // Check if it's a sample pack first\n  if (isSamplePackItem(lineItem)) {\n    return 'Sample Pack';\n  }\n  \n  // Check properties for product type\n  const productTypeProp = properties.find(p => \n    p.name && p.name.toLowerCase() === 'product type'\n  );\n  if (productTypeProp && productTypeProp.value) {\n    // Map to exact Airtable values\n    const typeMap = {\n      'dtf': 'DTF Transfers',\n      'dtf gang sheet': 'DTF Gang Sheet',\n      'uv gang sheet': 'UV Gang Sheet',\n      'gang sheet': 'Gang Sheet', // Generic gang sheet\n      'uv': 'UV Stickers',\n      'sublimation': 'Sublimation',\n      'heat press': 'Heat Press',\n      'heat tape': 'Heat Tape',\n      'matt finishing': 'Matt Finishing Sheet',\n      'matt finishing sheet': 'Matt Finishing Sheet',\n      'laser alignment': 'Laser Alignment',\n      'alignment tool': 'Alignment Tool',\n      'accessories': 'Accessories',\n      'dtf + uv': 'DTF + UV DTF',\n      'sample': 'Sample Pack',\n      'sample pack': 'Sample Pack'\n    };\n    \n    const propValue = productTypeProp.value.toLowerCase();\n    for (const [key, value] of Object.entries(typeMap)) {\n      if (propValue.includes(key)) return value;\n    }\n  }\n  \n  // Special handling for Gang Sheet Builder products\n  if (title.includes('gang sheet builder')) {\n    // Check if it's UV or DTF gang sheet\n    if (title.includes('uv')) {\n      return 'UV Gang Sheet';\n    } else {\n      // Default gang sheet builder products to DTF\n      return 'DTF Gang Sheet';\n    }\n  }\n  \n  // Check for specific product types in title/SKU\n  if (title.includes('laser alignment') || sku.includes('laser-align')) return 'Laser Alignment';\n  if (title.includes('alignment tool') || sku.includes('align-tool')) return 'Alignment Tool';\n  if (title.includes('matt finishing') || title.includes('matte finishing')) return 'Matt Finishing Sheet';\n  if (title.includes('heat tape') || sku.includes('heat-tape')) return 'Heat Tape';\n  if (title.includes('uv') && title.includes('dtf')) return 'DTF + UV DTF';\n  if (title.includes('uv') && title.includes('gang')) return 'UV Gang Sheet';\n  if (title.includes('dtf') && title.includes('gang')) return 'DTF Gang Sheet';\n  if (title.includes('uv') || sku.includes('uv')) return 'UV Stickers';\n  if (title.includes('sublimation') || sku.includes('sublimation')) return 'Sublimation';\n  if (title.includes('heat press') || title.includes('heat-press')) return 'Heat Press';\n  if (title.includes('gang sheet') || sku.includes('gang')) {\n    // Generic gang sheet - check other clues\n    if (variantTitle.includes('uv')) return 'UV Gang Sheet';\n    return 'DTF Gang Sheet'; // Default to DTF\n  }\n  if (title.includes('accessory') || title.includes('accessories')) return 'Accessories';\n  \n  // Check variant title\n  if (variantTitle.includes('uv')) return 'UV Stickers';\n  if (variantTitle.includes('gang')) {\n    if (variantTitle.includes('uv')) return 'UV Gang Sheet';\n    return 'DTF Gang Sheet';\n  }\n  \n  // Default to DTF Transfers\n  return 'DTF Transfers';\n}\n\n// Determine facility based on order data\nfunction determineFacility(order, lineItems) {\n  // For Shopify orders, leave facility blank for user selection\n  return null;\n}\n\n// Helper function to determine if order needs files\nfunction orderNeedsFiles(lineItems) {\n  if (!Array.isArray(lineItems)) return false;\n  \n  return lineItems.some(li => {\n    // Sample packs don't need files\n    if (isSamplePackItem(li)) return false;\n    \n    const properties = li.properties || [];\n    const hasOriginal = properties.find(p => p.name === '_original_image');\n    const hasDpi300 = properties.find(p => p.name === '_dpi300_image');\n    const hasPrintReady = properties.find(p => p.name === '_Print Ready File');\n    return !hasOriginal && !hasDpi300 && !hasPrintReady;\n  });\n}\n\n// Helper function to check if item is a sample pack\nfunction isSamplePackItem(lineItem) {\n  const title = (lineItem.title || '').toLowerCase();\n  const sku = (lineItem.sku || '').toLowerCase();\n  const variantTitle = (lineItem.variant_title || '').toLowerCase();\n  \n  return title.includes('sample') || \n         title.includes('sample pack') || \n         title.includes('swatch') ||\n         sku.includes('sample') ||\n         sku.includes('sample-pack') ||\n         variantTitle.includes('sample');\n}\n\n// Helper function to check if order is sample pack only\nfunction isSamplePackOnlyOrder(lineItems, shippingAmount) {\n  if (!Array.isArray(lineItems) || lineItems.length === 0) return false;\n  \n  // Check if ALL items are sample packs\n  const allItemsAreSamples = lineItems.every(item => isSamplePackItem(item));\n  \n  // Check if shipping is $2\n  const hasLowShipping = shippingAmount === 2 || shippingAmount === 2.00;\n  \n  return allItemsAreSamples && hasLowShipping;\n}\n\n// Process ALL items\nconst items = $input.all();\nconst allOutputs = [];\n\nitems.forEach(item => {\n  try {\n    const order = item.json.payload;\n    \n    // Validate order structure\n    if (!order || typeof order !== 'object') {\n      throw new Error('Invalid order structure');\n    }\n\n    // UPDATED: Format IDs according to new requirements\n    // Extract the TSS number from order.name (e.g., \"#TSS9183\" -> \"TSS9183\")\n    const tssNumber = (order.name || '').replace('#', '').trim();\n    \n    // orderId: 'TS-TSS9183' format\n    const orderId = tssNumber ? `TS-${tssNumber}` : `TS-TSS${order.order_number || Date.now()}`;\n    \n    // submissionId: 'TSS9183' (without # or TS- prefix)\n    const submissionId = tssNumber || `TSS${order.order_number || Date.now()}`;\n    \n    // shopifyOrderNumber: the numeric Shopify ID\n    const shopifyOrderNumber = order.id || '';\n\n    // Extract order details with validation\n    const hasRushTag = (order.tags || '').toLowerCase().includes('rush');\n    const has24HourTag = (order.tags || '').toLowerCase().includes('24 hour') || \n                         (order.tags || '').toLowerCase().includes('24hr');\n    const hasSuperRushTag = (order.tags || '').toLowerCase().includes('super rush');\n    \n    // Determine rush service level for Production Option\n    let productionOption = 'Standard 2-3 Days'; // Default\n    let rushService = false; // Checkbox - boolean\n    \n    if (hasSuperRushTag || has24HourTag) {\n      productionOption = 'Super Rush 24 hrs';\n      rushService = true;\n    } else if (hasRushTag) {\n      productionOption = 'Rush 1-2 Days';\n      rushService = true;\n    }\n    \n    const precut = (order.tags || '').toLowerCase().includes('precut') || \n                   (order.tags || '').toLowerCase().includes('pre-cut');\n    \n    // Check for gang sheet required - includes \"by Size\" products\n    const gangSheetRequired = (order.line_items || []).some(li => \n      (li.title || '').toLowerCase().includes('gang sheet') ||\n      (li.title || '').toLowerCase().includes('by size') ||\n      (li.properties || []).some(p => p.name === 'Gang Sheet' && p.value)\n    );\n    \n    const shippingLine = (order.shipping_lines && order.shipping_lines[0]) || {};\n    const shippingAmount = parseFloat(order.total_shipping_price_set?.shop_money?.amount || 0);\n    const needsFile = orderNeedsFiles(order.line_items || []);\n    \n    // Determine facility for this order\n    const facility = determineFacility(order, order.line_items || []);\n    \n    // Extract all product types\n    const productTypes = [...new Set((order.line_items || []).map(li => detectProductType(li)))];\n    \n    // Extract discount information\n    const discountCodes = order.discount_codes || [];\n    const discountAmount = parseFloat(order.total_discounts || 0);\n    \n    // Check if this is a sample pack only order\n    const isSamplePackOnly = isSamplePackOnlyOrder(order.line_items || [], shippingAmount);\n    \n    // Determine fulfillment and shipping options\n    let fulfillmentOption = 'Ship'; // Default\n    let shippingOption = 'Ground'; // Default\n    \n    const shippingTitle = (shippingLine.title || '').toLowerCase();\n    const shippingCode = (shippingLine.code || '').toLowerCase();\n    const shippingText = shippingTitle + ' ' + shippingCode;\n    \n    // First check if it's Will Call (fulfillment option)\n    if (shippingText.includes('will call') || \n        shippingText.includes('pickup') || \n        shippingText.includes('pick up') ||\n        shippingText.includes('local') ||\n        shippingText.includes('transfer superstars hq') ||\n        shippingAmount === 0) {\n      fulfillmentOption = 'Will Call';\n      shippingOption = null; // No shipping option needed for Will Call\n    } else {\n      // It's a Ship order, determine shipping speed\n      if (shippingText.includes('overnight') || \n          shippingText.includes('next day') || \n          shippingText.includes('1 day') ||\n          shippingText.includes('1-day') ||\n          shippingText.includes('priority overnight')) {\n        shippingOption = 'Overnight';\n      } else if (shippingText.includes('express') || \n                 shippingText.includes('2 day') || \n                 shippingText.includes('2-day') ||\n                 shippingText.includes('second day')) {\n        shippingOption = 'Express';\n      } else {\n        shippingOption = 'Ground';\n      }\n    }\n    \n    // Extract order tags - lowercase, meaningful tags\n    const orderTags = [];\n    \n    // Add tags from order.tags\n    if (order.tags) {\n      const rawTags = order.tags.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);\n      \n      // Filter and add meaningful tags\n      rawTags.forEach(tag => {\n        if (tag.includes('rush')) orderTags.push('rush');\n        if (tag.includes('precut') || tag.includes('pre-cut')) orderTags.push('precut');\n        if (tag.includes('sample')) orderTags.push('sample');\n        if (tag.includes('wholesale')) orderTags.push('wholesale');\n        if (tag.includes('repeat')) orderTags.push('repeat-customer');\n        if (tag.includes('vip')) orderTags.push('vip');\n        if (tag.includes('priority')) orderTags.push('priority');\n      });\n    }\n    \n    // Add automatic tags based on order properties\n    if (rushService && !orderTags.includes('rush')) orderTags.push('rush');\n    if (precut && !orderTags.includes('precut')) orderTags.push('precut');\n    if (gangSheetRequired && !orderTags.includes('gang-sheet')) orderTags.push('gang-sheet');\n    if (isSamplePackOnly && !orderTags.includes('sample')) orderTags.push('sample');\n    \n    // Add customer-based tags\n    if (order.customer?.tags?.includes('wholesale') && !orderTags.includes('wholesale')) {\n      orderTags.push('wholesale');\n    }\n    if (order.customer?.orders_count > 1 && !orderTags.includes('repeat-customer')) {\n      orderTags.push('repeat-customer');\n    }\n    if (order.total_price > 500 && !orderTags.includes('high-value')) {\n      orderTags.push('high-value');\n    }\n    \n    // Add payment tags\n    if (order.financial_status === 'paid' && !orderTags.includes('paid')) {\n      orderTags.push('paid');\n    }\n    \n    // Remove duplicates\n    const uniqueTags = [...new Set(orderTags)];\n    \n    // Determine priority level based on production option\n    let priorityLevel = 'Normal';\n    if (rushService) priorityLevel = 'High';\n    \n    // Create order summary\n    const orderSummary = `Order: ${orderId}\nDate: ${new Date(order.created_at).toLocaleDateString()}\nCustomer: ${order.customer?.first_name} ${order.customer?.last_name} (${order.customer?.email || order.email})\n${order.billing_address?.company ? `Company: ${order.billing_address.company}\\n` : ''}\nItems: ${(order.line_items || []).length} item(s)\n${productTypes.length > 0 ? `Products: ${productTypes.join(', ')}\\n` : ''}\nTotal: ${order.total_price} ${order.currency}\n${rushService ? `\u26a1 ${productionOption}\\n` : ''}${precut ? '\u2702\ufe0f Pre-cut Required\\n' : ''}${gangSheetRequired ? '\ud83d\udccf Gang Sheet Required\\n' : ''}\nFulfillment: ${fulfillmentOption}${fulfillmentOption === 'Ship' ? ` - ${shippingOption}` : ''}\n${order.note ? `\\nCustomer Notes: ${order.note}` : ''}\n${discountCodes.length > 0 ? `\\nDiscounts: ${discountCodes.map(d => d.code).join(', ')} (-${discountAmount})` : ''}\nStatus: ${order.financial_status === 'paid' ? '\u2705 Paid' : '\u23f3 Payment Pending'}`;\n    \n    // Determine file status\n    let fileStatus = 'All Files Received';\n    if (needsFile) fileStatus = 'Awaiting Files';\n    else if ((order.line_items || []).some(li => {\n      const props = li.properties || [];\n      return props.some(p => p.name === '_dpi300_image' && !p.value);\n    })) fileStatus = 'Processing';\n\n    const orderCanon = {\n      // UPDATED: Identification with new format\n      orderId,           // 'TS-TSS9183'\n      submissionId,      // 'TSS9183'\n      source: 'Shopify',\n      shopifyOrderNumber, // numeric ID like 6329978978592\n      \n      // Timestamps\n      orderDate: order.created_at || new Date().toISOString(),\n      createdAt: order.created_at || new Date().toISOString(),\n      updatedAt: order.updated_at || new Date().toISOString(),\n      paidAt: order.processed_at || null,\n      fulfilledAt: order.closed_at || null,\n      \n      // Customer data - enhanced\n      customer: {\n        id: (order.customer?.id || '').toString(),\n        email: order.customer?.email || order.email || order.contact_email || '',\n        name: order.customer?.first_name && order.customer?.last_name ? \n              `${order.customer.first_name} ${order.customer.last_name}` :\n              order.billing_address?.name || order.shipping_address?.name || 'Unknown',\n        firstName: order.customer?.first_name || order.billing_address?.first_name || order.shipping_address?.first_name || '',\n        lastName: order.customer?.last_name || order.billing_address?.last_name || order.shipping_address?.last_name || '',\n        phone: order.customer?.phone || order.billing_address?.phone || order.shipping_address?.phone || order.phone || '',\n        company: order.billing_address?.company || order.shipping_address?.company || order.customer?.default_address?.company || '',\n        tags: order.customer?.tags || '',\n        note: order.customer?.note || '',\n        emailOptIn: order.buyer_accepts_marketing || false,\n        smsOptIn: order.customer?.sms_marketing_consent?.state === 'subscribed' || false,\n        taxExempt: order.customer?.tax_exempt || false,\n        verifiedEmail: order.customer?.verified_email || false,\n        ordersCount: order.customer?.orders_count || 1,\n        totalSpent: order.customer?.total_spent || order.total_price || 0\n      },\n      \n      // Shipping address - complete with formatting\n      shipping: {\n        fullAddress: createFullAddress(order.shipping_address),\n        name: order.shipping_address?.name || '',\n        firstName: order.shipping_address?.first_name || '',\n        lastName: order.shipping_address?.last_name || '',\n        company: order.shipping_address?.company || '',\n        address1: order.shipping_address?.address1 || '',\n        address2: order.shipping_address?.address2 || '',\n        city: order.shipping_address?.city || '',\n        state: order.shipping_address?.province_code || '',\n        zip: order.shipping_address?.zip || '',\n        country: order.shipping_address?.country || '',\n        countryCode: order.shipping_address?.country_code || '',\n        phone: order.shipping_address?.phone || '',\n        method: shippingOption  // DEPRECATED - use fulfillmentOption and shippingOption instead\n      },\n      \n      // Billing address - complete with formatting\n      billing: {\n        fullAddress: createFullAddress(order.billing_address),\n        name: order.billing_address?.name || '',\n        firstName: order.billing_address?.first_name || '',\n        lastName: order.billing_address?.last_name || '',\n        company: order.billing_address?.company || '',\n        address1: order.billing_address?.address1 || '',\n        address2: order.billing_address?.address2 || '',\n        city: order.billing_address?.city || '',\n        state: order.billing_address?.province_code || '',\n        zip: order.billing_address?.zip || '',\n        country: order.billing_address?.country || '',\n        countryCode: order.billing_address?.country_code || '',\n        phone: order.billing_address?.phone || ''\n      },\n      \n      // Financial data - enhanced\n      financial: {\n        subtotal: parseFloat(order.subtotal_price || 0),\n        shipping: shippingAmount,\n        tax: parseFloat(order.total_tax || 0),\n        total: parseFloat(order.total_price || 0),\n        discountAmount: discountAmount,\n        discountCodes: discountCodes.map(d => d.code).join(', '),\n        currency: order.currency || 'USD',\n        paymentStatus: order.financial_status === 'paid' ? 'Paid' : 'Pending Payment',\n        paymentMethod: (order.payment_gateway_names && order.payment_gateway_names[0]) || 'Unknown',\n        refundedAmount: parseFloat(order.total_refunded || 0),\n        outstandingBalance: parseFloat(order.total_outstanding || 0)\n      },\n      \n      // Order status and options\n      status: {\n        orderStatus: 'New',\n        fulfillmentStatus: order.fulfillment_status || null,\n        syncStatus: 'Success',\n        qcStatus: 'Not Started'\n      },\n      \n      // Production options with facility\n      options: {\n        rushService,  // Boolean checkbox\n        productionOption,  // \"Standard 2-3 Days\", \"Rush 1-2 Days\", \"Super Rush 24 hrs\"\n        fulfillmentOption,  // \"Ship\" or \"Will Call\"\n        shippingOption,  // \"Ground\", \"Express\", \"Overnight\" (null for Will Call)\n        precut,\n        gangSheetRequired,\n        needsFile,\n        notes: order.note || '',\n        customerNotes: order.note || '',\n        internalNotes: '',\n        tags: uniqueTags.join(','), // lowercase, comma-separated\n        priorityLevel,\n        productTypes: productTypes.join(', '),\n        fileStatus,\n        isSamplePackOnly,\n        facility,\n        orderSummary\n      },\n      \n      // Summary fields\n      summary: {\n        itemCount: (order.line_items || []).length,\n        sourceUrl: `https://admin.shopify.com/store/transfer-superstars/orders/${order.id || ''}`\n      },\n      \n      // Complete metadata\n      meta: {\n        shopify: order,\n        facility,\n        processedAt: new Date().toISOString()\n      }\n    };\n\n    // Create array of line items with complete mapping\n    const lineItems = (order.line_items || []).map(li => {\n      // Extract all properties\n      const properties = li.properties || [];\n      const propertiesObj = {};\n      properties.forEach(p => {\n        if (p.name) propertiesObj[p.name] = p.value;\n      });\n      \n      // File handling\n      const originalImage = properties.find(p => p.name === '_original_image');\n      const dpi300Image = properties.find(p => p.name === '_dpi300_image');\n      const previewImage = properties.find(p => p.name === 'Preview');\n      const printReadyFile = properties.find(p => p.name === '_Print Ready File');\n      \n      // Extract dimensions with multiple fallbacks\n      const sizeProp = properties.find(p => /size/i.test(p.name||''))?.value || '';\n      const dimensionsProp = properties.find(p => p.name === 'dimensions')?.value || '';\n      const dims = sizeProp || dimensionsProp || \n                   (li.variant_title || '').replace(/[^\\d.x]/g, '') ||\n                   ((li.sku || '').match(/(\\d+\\.?\\d*)x(\\d+\\.?\\d*)/)||[]).join('x');\n      \n      // File quality info\n      const dpi = properties.find(p => p.name === 'dpi')?.value || '';\n      const colorMode = properties.find(p => p.name === 'color_mode')?.value || '';\n      const fileFormat = properties.find(p => p.name === 'file_format')?.value || '';\n      \n      // Determine file URL (prioritize DPI300, then print ready, then original)\n      const fileUrl = dpi300Image?.value || printReadyFile?.value || \n                     originalImage?.value || previewImage?.value || '';\n      \n      // Get filename - extract from URL if title is generic\n      let fileName = properties.find(p => p.name === 'File name')?.value || '';\n      \n      // If no filename or title is a default product name, extract from URL\n      if (!fileName || isDefaultProductTitle(li.title)) {\n        if (fileUrl) {\n          fileName = extractFilenameFromUrl(fileUrl);\n        }\n      }\n      \n      // Production options\n      const isPrecut = properties.find(p => p.name === 'Precut')?.value === 'Yes' ||\n                      properties.find(p => p.name === 'Pre-cut')?.value === 'Yes' ||\n                      precut; // Inherit from order level\n      const addWeeding = properties.find(p => p.name === 'Add Weeding')?.value === 'Yes';\n      const isGangSheet = properties.find(p => p.name === 'Gang Sheet')?.value === 'Yes' ||\n                         detectProductType(li) === 'Gang Sheet' ||\n                         detectProductType(li) === 'DTF Gang Sheet' ||\n                         detectProductType(li) === 'UV Gang Sheet';\n      const transferTape = properties.find(p => p.name === 'Transfer Tape')?.value === 'Yes';\n      const gangSheetLength = parseFloat(properties.find(p => p.name === 'Gang Sheet Length')?.value || 0);\n\n      // Check if this is a sample pack\n      const isSamplePack = isSamplePackItem(li);\n\n      // Determine item status\n      let itemStatus = 'Pending';\n      \n      // Sample packs are always ready - they don't need files\n      if (isSamplePack) {\n        itemStatus = 'Ready';\n      } else if (fileUrl && dpi300Image?.value) {\n        itemStatus = 'Ready';\n      } else if (fileUrl) {\n        itemStatus = 'Processing';\n      } else {\n        itemStatus = 'Pending';\n      }\n\n      // Determine artwork QC status\n      let artworkQC = 'Pending';\n      if (properties.find(p => p.name === 'QC Approved')?.value === 'Yes') {\n        artworkQC = 'Approved';\n      } else if (properties.find(p => p.name === 'QC Rejected')?.value === 'Yes') {\n        artworkQC = 'Rejected';\n      }\n\n      return {\n        // UPDATED: Identification with new orderId format\n        itemUid: `${orderId}-${li.id || Date.now()}`,  // 'TS-TSS9183-123456'\n        orderId,  // 'TS-TSS9183'\n        lineItemId: (li.id || Date.now()).toString(),\n        \n        // Product info with proper type detection\n        productType: detectProductType(li),\n        productId: (li.product_id || '').toString(),\n        variantId: (li.variant_id || '').toString(),\n        sku: li.sku || '',\n        title: li.title || '',\n        variantTitle: li.variant_title || '',\n        vendor: li.vendor || '',\n        \n        // Quantities and pricing\n        qty: Number(li.quantity || 1),\n        unitPrice: Number(li.price || 0),\n        compareAtPrice: Number(li.compare_at_price || 0),\n        totalDiscount: Number(li.total_discount || 0),\n        \n        // Physical properties\n        dimensions: dims,\n        weight: li.grams || 0,\n        requiresShipping: li.requires_shipping !== false,\n        \n        // File information - Sample packs don't need files\n        fileUrl: isSamplePack ? 'N/A - Sample Pack' : fileUrl,\n        needsFile: isSamplePack ? false : !fileUrl,\n        fileName: isSamplePack ? 'Sample Pack - No File Required' : fileName,\n        originalFileUrl: isSamplePack ? '' : (originalImage?.value || ''),\n        processedFileUrl: isSamplePack ? '' : (dpi300Image?.value || ''),\n        previewUrl: isSamplePack ? '' : (previewImage?.value || ''),\n        \n        // File quality\n        fileDpi: Number(dpi) || 300,\n        fileColorMode: colorMode || 'RGB',\n        fileFormat: fileFormat || (fileName ? fileName.split('.').pop()?.toUpperCase() : 'PNG') || 'PNG',\n        fileDimensions: dimensionsProp,\n        \n        // Production options\n        isPrecut,\n        addWeeding,\n        isGangSheet,\n        transferTape,\n        gangSheetLength,\n        isSamplePack,\n        \n        // Fulfillment\n        fulfillableQuantity: li.fulfillable_quantity || li.quantity || 0,\n        fulfillmentService: li.fulfillment_service || 'manual',\n        fulfillmentStatus: li.fulfillment_status || null,\n        \n        // Tax info\n        taxable: li.taxable !== false,\n        \n        // Gift card\n        giftCard: li.gift_card || false,\n        \n        // Status\n        itemStatus,\n        artworkQC,\n        qcNotes: properties.find(p => p.name === 'QC Notes')?.value || '',\n        \n        // Facility assignment\n        facility: facility,\n        \n        // Complete metadata\n        meta: {\n          shopify: {\n            lineItemId: li.id,\n            properties: properties,\n            propertiesObject: propertiesObj,\n            productHandle: li.product_handle || ''\n          },\n          facility: facility,\n          processedAt: new Date().toISOString()\n        }\n      };\n    });\n\n    // Add this order with its items to output\n    allOutputs.push({\n      json: {\n        order: orderCanon,\n        items: lineItems\n      }\n    });\n  } catch (error) {\n    // Log error but continue processing other orders\n    console.error('Error processing order:', error.message);\n  }\n});\n\nreturn allOutputs.length > 0 ? allOutputs : [{ json: { error: 'No valid orders to process' } }];"
      },
      "id": "936e332c-de2d-4180-abef-f7172a0ebaca",
      "name": "Function - Parse Shopify",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -60,
        -300
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Helper function to extract filename from URL\nfunction extractFilenameFromUrl(url) {\n  if (!url) return '';\n  \n  try {\n    // Decode the URL first to handle encoded characters\n    const decodedUrl = decodeURIComponent(url);\n    // Extract the last part of the URL path\n    const urlParts = decodedUrl.split('/');\n    const filename = urlParts[urlParts.length - 1];\n    \n    // Remove query parameters if present\n    return filename.split('?')[0] || '';\n  } catch (e) {\n    // If decoding fails, try without decoding\n    const urlParts = url.split('/');\n    const filename = urlParts[urlParts.length - 1];\n    return filename.split('?')[0] || '';\n  }\n}\n\n// Enhanced product type detection based on Airtable schema and form data\nfunction detectProductType(productType, formName, answers, formId) {\n  const type = (productType || '').toLowerCase();\n  const form = (formName || '').toLowerCase();\n  \n  // First check direct product type answer\n  if (productType) {\n    const typeMap = {\n      'dtf': 'DTF Transfers',\n      'dtf transfer': 'DTF Transfers',\n      'dtf transfers': 'DTF Transfers',\n      'dtf gang sheet': 'DTF Gang Sheet',\n      'uv gang sheet': 'UV Gang Sheet',\n      'gang sheet': 'Gang Sheet',\n      'gang sheets': 'Gang Sheet',\n      'uv': 'UV Stickers',\n      'uv sticker': 'UV Stickers',\n      'uv stickers': 'UV Stickers',\n      'sublimation': 'Sublimation',\n      'heat press': 'Heat Press',\n      'heat tape': 'Heat Tape',\n      'matt finishing': 'Matt Finishing Sheet',\n      'matt finishing sheet': 'Matt Finishing Sheet',\n      'laser alignment': 'Laser Alignment',\n      'alignment tool': 'Alignment Tool',\n      'accessories': 'Accessories',\n      'dtf + uv': 'DTF + UV DTF',\n      'dtf and uv': 'DTF + UV DTF',\n      'uv dtf': 'DTF + UV DTF'\n    };\n    \n    for (const [key, value] of Object.entries(typeMap)) {\n      if (type === key || type.includes(key)) {\n        // Special handling for generic \"Gang Sheet\"\n        if (value === 'Gang Sheet') {\n          // Determine based on form\n          if (form.includes('uv') || formId === '233115151476147') {\n            return 'UV Gang Sheet';\n          } else {\n            return 'DTF Gang Sheet';\n          }\n        }\n        return value;\n      }\n    }\n  }\n  \n  // Check form name for product type hints\n  if (form.includes('uv') && form.includes('dtf')) return 'DTF + UV DTF';\n  if (form.includes('uv') && form.includes('gang')) return 'UV Gang Sheet';\n  if (form.includes('dtf') && form.includes('gang')) return 'DTF Gang Sheet';\n  if (form.includes('uv sticker') || form.includes('uv-sticker')) return 'UV Stickers';\n  if (form.includes('uv')) return 'UV Stickers';\n  if (form.includes('sublimation')) return 'Sublimation';\n  if (form.includes('heat press')) return 'Heat Press';\n  if (form.includes('gang sheet') || form.includes('gangsheet')) {\n    // Try to determine if UV or DTF\n    if (form.includes('uv') || formId === '233115151476147') {\n      return 'UV Gang Sheet';\n    }\n    return 'DTF Gang Sheet'; // Default to DTF\n  }\n  \n  // Check special instructions or notes for product type hints\n  if (answers) {\n    for (const answer of Object.values(answers)) {\n      const text = (answer.answer || answer.prettyFormat || '').toString().toLowerCase();\n      if (text.includes('uv') && text.includes('dtf')) return 'DTF + UV DTF';\n      if (text.includes('uv') && text.includes('gang')) return 'UV Gang Sheet';\n      if (text.includes('dtf') && text.includes('gang')) return 'DTF Gang Sheet';\n      if (text.includes('uv sticker')) return 'UV Stickers';\n      if (text.includes('gang sheet')) {\n        if (text.includes('uv') || formId === '233115151476147') {\n          return 'UV Gang Sheet';\n        }\n        return 'DTF Gang Sheet';\n      }\n      if (text.includes('sublimation')) return 'Sublimation';\n      if (text.includes('alignment tool')) return 'Alignment Tool';\n    }\n  }\n  \n  // Default to DTF Transfers\n  return 'DTF Transfers';\n}\n\n// Determine facility based on form data and shipping location\nfunction determineFacility(formName, shippingState, productType) {\n  // For JotForm orders, leave facility blank for user selection\n  return null;\n}\n\n// Extract order tags based on form data\nfunction extractOrderTags(rushService, precut, formName, answers) {\n  const tags = [];\n  \n  if (rushService) tags.push('rush');\n  if (precut) tags.push('precut');\n  \n  // Add form-specific tags\n  const form = (formName || '').toLowerCase();\n  if (form.includes('wholesale') || form.includes('b2b')) {\n    tags.push('wholesale');\n  }\n  \n  if (form.includes('sample')) {\n    tags.push('sample');\n  }\n  \n  // Check answers for additional tags\n  if (answers) {\n    for (const [key, answer] of Object.entries(answers)) {\n      const text = (answer.text || '').toLowerCase();\n      const value = (answer.answer || answer.prettyFormat || '').toString().toLowerCase();\n      \n      if (text.includes('repeat customer') && value.includes('yes')) {\n        tags.push('repeat-customer');\n      }\n      \n      if (text.includes('referral') && value) {\n        tags.push('referral');\n      }\n    }\n  }\n  \n  return tags;\n}\n\n// Process ALL items\nconst items = $input.all();\n\nreturn items.map(item => {\n  try {\n    const submission = item.json.payload || item.json;\n    const answers = submission.answers || {};\n    \n    // Validate submission\n    if (!submission.id) {\n      throw new Error('Invalid JotForm submission - no ID');\n    }\n    \n    // Get form metadata from the tagged data\n    const formName = item.json.form_name || '';\n    const formLayout = item.json.form_layout || '';\n    const formId = submission.form_id || '';\n    \n    // Determine prefix based on form type\n    // Check form ID, form name, and form layout for UV forms\n    const isUVForm = formId === '233115151476147' || \n                     formName.toLowerCase().includes('uv') || \n                     formLayout.toLowerCase().includes('uv');\n    const prefix = isUVForm ? 'UV' : 'DTF';\n    \n    // UPDATED: Format IDs according to new requirements\n    // JotForm: orderId = \"DTF-2398234792374\" or \"UV-234792873472\", submissionId = \"2398234792374\"\n    const submissionId = submission.id.toString();\n    const orderId = `${prefix}-${submissionId}`;\n\n    let customer = {\n      email: '',\n      name: '',\n      firstName: '',\n      lastName: '',\n      phone: '',\n      company: '',\n      emailOptIn: true,\n      smsOptIn: false,\n      taxExempt: false,\n      verifiedEmail: false,\n      tags: '',\n      note: ''\n    };\n\n    let shipping = {\n      fullAddress: '',\n      name: '',\n      firstName: '',\n      lastName: '',\n      company: '',\n      address1: '',\n      address2: '',\n      city: '',\n      state: '',\n      zip: '',\n      country: 'US',\n      countryCode: 'US',\n      phone: '',\n      method: 'Ground' // Default to Ground\n    };\n    \n    let billing = {\n      fullAddress: '',\n      name: '',\n      firstName: '',\n      lastName: '',\n      company: '',\n      address1: '',\n      address2: '',\n      city: '',\n      state: '',\n      zip: '',\n      country: 'US',\n      countryCode: 'US',\n      phone: ''\n    };\n\n    let orderDetails = {\n      productType: '',\n      quantity: 1,\n      dimensions: '',\n      fileData: [], // Changed to store file objects with url and name\n      specialInstructions: '',\n      productionOption: 'Standard 2-3 Days', // Default production option\n      rushService: false, // Boolean checkbox\n      precut: false,\n      gangSheetRequired: false,\n      addWeeding: false,\n      transferTape: false,\n      paymentMethod: 'Pending',\n      referralSource: '',\n      fulfillmentOption: 'Ship', // Default fulfillment option\n      shippingOption: 'Ground' // Default shipping option\n    };\n\n    // Debug log to see what we're getting\n    console.log(`Processing submission ${submissionId} with ${Object.keys(answers).length} answers`);\n\n    // Parse JotForm answers - use for...of instead of forEach\n    for (const [key, answer] of Object.entries(answers)) {\n      if (!answer || typeof answer !== 'object') continue;\n      \n      const text = answer.text || '';\n      const value = answer.answer || answer.prettyFormat || '';\n      \n      // Debug file upload fields\n      if (answer.type === 'control_fileupload' || text.includes('Upload') || text.includes('File')) {\n        console.log(`Found file upload field ${key}:`, answer);\n      }\n      \n      // Customer info - improved detection with specific field IDs\n      // Email - check specific field ID 331 first\n      if (key === '331') {\n        // This is the email field\n        if (typeof answer.answer === 'string') {\n          customer.email = answer.answer;\n        } else if (typeof answer.prettyFormat === 'string') {\n          customer.email = answer.prettyFormat;\n        } else if (typeof value === 'string') {\n          customer.email = value;\n        }\n        console.log(`Found email in field 331: ${customer.email}`);\n      } else if (answer.type === 'control_email' || \n                 text.toLowerCase().includes('email') || \n                 text.toLowerCase().includes('e-mail')) {\n        // Fallback email detection\n        if (typeof value === 'string' && value.includes('@')) {\n          customer.email = value;\n        } else if (typeof answer.answer === 'string' && answer.answer.includes('@')) {\n          customer.email = answer.answer;\n        } else if (answer.prettyFormat && typeof answer.prettyFormat === 'string') {\n          customer.email = answer.prettyFormat;\n        }\n      } else if (answer.type === 'control_fullname' || \n                 (text.toLowerCase().includes('name') && \n                  !text.toLowerCase().includes('company') && \n                  !text.toLowerCase().includes('business'))) {\n        // Handle fullname field - JotForm often returns an object\n        if (typeof answer.answer === 'object' && answer.answer) {\n          customer.firstName = answer.answer.first || '';\n          customer.lastName = answer.answer.last || '';\n          customer.name = `${customer.firstName} ${customer.lastName}`.trim();\n        } else if (typeof value === 'string' && value) {\n          customer.name = value;\n          const nameParts = value.split(' ');\n          customer.firstName = nameParts[0] || '';\n          customer.lastName = nameParts.slice(1).join(' ') || '';\n        }\n      } else if (answer.type === 'control_phone' || text.toLowerCase().includes('phone')) {\n        customer.phone = answer.answer?.full || (typeof value === 'string' ? value : '');\n      } else if ((text.toLowerCase().includes('company') || text.toLowerCase().includes('business')) && \n                 !text.toLowerCase().includes('address')) {\n        // Only capture as company if it's actually a company field, not a date or other field\n        const companyValue = typeof value === 'string' ? value : '';\n        // Check if the value looks like a date (e.g., \"Wed Jun 25\")\n        if (!companyValue.match(/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+/i)) {\n          customer.company = companyValue;\n        }\n      }\n      \n      // Address info\n      if ((answer.type === 'control_address' || text.toLowerCase().includes('address')) && answer.answer && typeof answer.answer === 'object') {\n        const addr = answer.answer;\n        shipping.address1 = addr.addr_line1 || '';\n        shipping.address2 = addr.addr_line2 || '';\n        shipping.city = addr.city || '';\n        shipping.state = addr.state || '';\n        shipping.zip = addr.postal || '';\n        shipping.country = addr.country || 'United States';\n        shipping.countryCode = addr.country === 'United States' ? 'US' : addr.country?.substring(0, 2).toUpperCase() || 'US';\n        \n        // Format full address\n        const parts = [shipping.address1, shipping.address2, shipping.city, shipping.state, shipping.zip].filter(Boolean);\n        shipping.fullAddress = parts.join(', ');\n        \n        // Copy to billing\n        billing = {...shipping};\n      }\n      \n      // Product details - Enhanced detection\n      if (text.includes('Product Type') || text.includes('What are you printing') || text.includes('Service Type')) {\n        // Handle multiple choice or dropdown answers\n        if (typeof value === 'string') {\n          orderDetails.productType = value;\n        } else if (Array.isArray(value) && value.length > 0) {\n          orderDetails.productType = value[0];\n        }\n      } else if (text.includes('Quantity') || text.includes('How many')) {\n        // Handle matrix field for quantity\n        if (answer.answer && Array.isArray(answer.answer)) {\n          // Matrix field structure: [[row_name, col1_value, col2_value, ...]]\n          if (Array.isArray(answer.answer[0])) {\n            orderDetails.quantity = parseInt(answer.answer[0][2]) || parseInt(answer.answer[0][1]) || 1;\n          }\n        } else if (typeof value === 'string' || typeof value === 'number') {\n          orderDetails.quantity = parseInt(value) || 1;\n        }\n      } else if (text.includes('Size') || text.includes('Dimensions')) {\n        if (typeof value === 'string') {\n          orderDetails.dimensions = value;\n        } else if (answer.answer && typeof answer.answer === 'object') {\n          // Handle structured dimension answers\n          const width = answer.answer.width || '';\n          const height = answer.answer.height || '';\n          if (width && height) {\n            orderDetails.dimensions = `${width}x${height}`;\n          }\n        }\n      } else if (text.includes('Special Instructions') || text.includes('Notes') || text.includes('Additional')) {\n        orderDetails.specialInstructions = typeof value === 'string' ? value : '';\n      } else if (text.includes('Rush') || text.includes('Expedited') || text.includes('24 hour') || text.includes('Production')) {\n        const rushValue = typeof value === 'string' ? value : '';\n        if (rushValue.toLowerCase().includes('super') || rushValue.toLowerCase().includes('24')) {\n          orderDetails.productionOption = 'Super Rush 24 hrs';\n          orderDetails.rushService = true;\n        } else if (rushValue.toLowerCase().includes('yes') || rushValue.toLowerCase().includes('rush') || rushValue.toLowerCase().includes('1-2')) {\n          orderDetails.productionOption = 'Rush 1-2 Days';\n          orderDetails.rushService = true;\n        } else {\n          orderDetails.productionOption = 'Standard 2-3 Days';\n          orderDetails.rushService = false;\n        }\n      } else if (text.includes('Pre-cut') || text.includes('Precut')) {\n        const precutValue = typeof value === 'string' ? value : '';\n        orderDetails.precut = precutValue.toLowerCase().includes('yes');\n      } else if (text.includes('Gang Sheet')) {\n        const gangValue = typeof value === 'string' ? value : '';\n        orderDetails.gangSheetRequired = gangValue.toLowerCase().includes('yes');\n      } else if (text.includes('Weeding')) {\n        const weedingValue = typeof value === 'string' ? value : '';\n        orderDetails.addWeeding = weedingValue.toLowerCase().includes('yes');\n      } else if (text.includes('Transfer Tape') || text.includes('Application Tape')) {\n        const tapeValue = typeof value === 'string' ? value : '';\n        orderDetails.transferTape = tapeValue.toLowerCase().includes('yes');\n      } else if (text.includes('Shipping Method') || text.includes('Shipping Option') || text.includes('Delivery Method') || text.includes('Fulfillment')) {\n        const shipValue = typeof value === 'string' ? value : '';\n        // Map to allowed values with more comprehensive detection\n        const shipLower = shipValue.toLowerCase();\n        \n        // First check if it's a Will Call order\n        if (shipLower.includes('will call') || \n            shipLower.includes('pickup') || \n            shipLower.includes('pick up') ||\n            shipLower.includes('local') ||\n            shipLower.includes('in store') ||\n            shipLower.includes('in-store')) {\n          orderDetails.fulfillmentOption = 'Will Call';\n          orderDetails.shippingOption = null; // No shipping needed\n        } else {\n          // It's a Ship order, determine speed\n          orderDetails.fulfillmentOption = 'Ship';\n          \n          if (shipLower.includes('overnight') || \n              shipLower.includes('next day') || \n              shipLower.includes('next-day') ||\n              shipLower.includes('1 day') ||\n              shipLower.includes('1-day') ||\n              shipLower.includes('priority overnight')) {\n            orderDetails.shippingOption = 'Overnight';\n          } else if (shipLower.includes('express') || \n                     shipLower.includes('2 day') || \n                     shipLower.includes('2-day') ||\n                     shipLower.includes('second day') ||\n                     shipLower.includes('expedited')) {\n            orderDetails.shippingOption = 'Express';\n          } else {\n            orderDetails.shippingOption = 'Ground';\n          }\n        }\n      } else if (text.includes('Payment') || text.includes('How will you pay')) {\n        orderDetails.paymentMethod = typeof value === 'string' ? value : 'Pending';\n      } else if (text.includes('How did you hear') || text.includes('Referral')) {\n        orderDetails.referralSource = typeof value === 'string' ? value : '';\n      } else if (text.includes('Marketing') || text.includes('Newsletter')) {\n        const marketingValue = typeof value === 'string' ? value : '';\n        customer.emailOptIn = marketingValue.toLowerCase().includes('yes');\n      } else if (text.includes('SMS') || text.includes('Text')) {\n        const smsValue = typeof value === 'string' ? value : '';\n        customer.smsOptIn = smsValue.toLowerCase().includes('yes');\n      } else if (text.includes('Tax Exempt')) {\n        const taxValue = typeof value === 'string' ? value : '';\n        customer.taxExempt = taxValue.toLowerCase().includes('yes');\n      }\n      \n      // File uploads - Updated to extract filenames\n      if ((answer.type === 'control_fileupload' || text.includes('Upload') || text.includes('File')) && answer.answer) {\n        const files = Array.isArray(answer.answer) ? answer.answer : [answer.answer];\n        files.forEach(fileUrl => {\n          if (fileUrl && typeof fileUrl === 'string') {\n            const fileName = extractFilenameFromUrl(fileUrl);\n            console.log(`Adding file: ${fileName} from URL: ${fileUrl}`);\n            orderDetails.fileData.push({\n              url: fileUrl,\n              name: fileName\n            });\n          }\n        });\n      }\n    }\n\n    // After parsing, ensure shipping has customer info\n    if (customer.name && !shipping.name) {\n      shipping.name = customer.name;\n      shipping.firstName = customer.firstName;\n      shipping.lastName = customer.lastName;\n    }\n    if (customer.company) {\n      shipping.company = customer.company;\n    }\n    if (customer.phone && !shipping.phone) {\n      shipping.phone = customer.phone;\n    }\n\n    // Update billing to match shipping\n    billing = {...shipping};\n\n    console.log(`Found ${orderDetails.fileData.length} files for order ${orderId}`);\n    console.log(`Customer data: name=\"${customer.name}\", email=\"${customer.email}\", phone=\"${customer.phone}\"`);\n\n    // Detect product type with all available data\n    const detectedProductType = detectProductType(orderDetails.productType, formName, answers, formId);\n    \n    // Determine facility\n    const facility = determineFacility(formName, shipping.state, detectedProductType);\n    \n    // Update shipping method to shipping/fulfillment options\n    shipping.method = orderDetails.shippingOption;  // DEPRECATED - use fulfillmentOption and shippingOption instead\n    \n    // Extract order tags\n    const orderTags = [];\n    \n    if (orderDetails.rushService) orderTags.push('rush');\n    if (orderDetails.precut) orderTags.push('precut');\n    if (orderDetails.gangSheetRequired || detectedProductType.includes('Gang Sheet')) orderTags.push('gang-sheet');\n    \n    // Add form-specific tags\n    const form = (formName || '').toLowerCase();\n    if (form.includes('wholesale') || form.includes('b2b')) orderTags.push('wholesale');\n    if (form.includes('sample')) orderTags.push('sample');\n    if (form.includes('custom')) orderTags.push('custom');\n    if (form.includes('bulk')) orderTags.push('bulk');\n    \n    // Add referral tag if applicable\n    if (orderDetails.referralSource) {\n      const referral = orderDetails.referralSource.toLowerCase();\n      if (referral.includes('google')) orderTags.push('google-referral');\n      else if (referral.includes('facebook') || referral.includes('instagram')) orderTags.push('social-referral');\n      else if (referral.includes('friend') || referral.includes('word')) orderTags.push('word-of-mouth');\n      else if (referral.includes('return') || referral.includes('repeat')) orderTags.push('repeat-customer');\n    }\n    \n    // Check for specific product tags\n    if (detectedProductType === 'UV Stickers' || detectedProductType === 'UV Gang Sheet') orderTags.push('uv');\n    if (detectedProductType === 'DTF + UV DTF') orderTags.push('uv', 'dtf');\n    \n    // Determine priority level based on rush service\n    let priorityLevel = 'Normal';\n    if (orderDetails.rushService) priorityLevel = 'High';\n    \n    // Create order summary\n    const orderSummary = `Order: ${orderId}\nDate: ${new Date(submission.created_at).toLocaleDateString()}\nForm: ${formName}\nCustomer: ${customer.name} (${customer.email})\n${customer.company ? `Company: ${customer.company}\\n` : ''}\nProduct: ${detectedProductType}\nQuantity: ${orderDetails.quantity}\n${orderDetails.dimensions ? `Size: ${orderDetails.dimensions}\\n` : ''}\n${orderDetails.rushService ? `\u26a1 ${orderDetails.productionOption}\\n` : ''}${orderDetails.precut ? '\u2702\ufe0f Pre-cut Required\\n' : ''}${orderDetails.gangSheetRequired ? '\ud83d\udccf Gang Sheet Required\\n' : ''}\nFiles: ${orderDeta

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Ts Order Ingest. Uses httpRequest, itemLists, stopAndError, airtable. Manual trigger; 52 nodes.

Source: https://github.com/niganuga/production-scanner/blob/53cdcbfc592abf5d0f33bc3e7f2fd054240042b4/n8n/ts_order_ingest.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

14310 Send Overdue Invoice Payment Reminders With Ifirma Gmail Postgrid And Slack. Uses httpRequest, stopAndError, slack, gmail. Scheduled trigger; 53 nodes.

HTTP Request, Stop And Error, Slack +1
Email & Gmail

Recruiting agency. Uses typeformTrigger, airtable, httpRequest, googleDrive. Event-driven trigger; 36 nodes.

Typeform Trigger, Airtable, HTTP Request +4
Email & Gmail

Categories: Payments, Project Operations, Client Onboarding

Stripe Trigger, Google Drive, ClickUp +4
Email & Gmail

This workflow automates the complete end-to-end processing of daily revenue transactions for finance and accounting teams. It systematically retrieves, validates, and standardizes transaction data fro

HTTP Request, Gmail, Google Drive +2
Email & Gmail

Wait. Uses httpRequest, itemLists, slack, gmail. Webhook trigger; 29 nodes.

HTTP Request, Item Lists, Slack +2