{
  "name": "ASM Investigator Enrichment",
  "nodes": [
    {
      "parameters": {
        "operation": "append",
        "sheetId": "",
        "range": "Output!A:ZZ",
        "key": "shop_name"
      },
      "id": "GoogleSheets_Write_Output",
      "name": "Write to Output",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 3
    },
    {
      "parameters": {
        "operation": "lookup",
        "sheetId": "",
        "range": "Input!A:F",
        "lookupColumn": "processed",
        "lookupValue": "FALSE",
        "returnAllMatches": false
      },
      "id": "GoogleSheets_Read_Input",
      "name": "Read next Input row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 3
    },
    {
      "parameters": {
        "functionCode": "// Normalize fields and derive domain\nconst row = items[0].json;\nfunction norm(x){return (x||'').toString().trim();}\nconst website_url = norm(row.website_url);\nlet website_domain = '';\ntry {\n  if (website_url) {\n    website_domain = new URL(website_url).hostname.replace(/^www\\./,'');\n  }\n} catch(e){}\nconst qBase = [norm(row.shop_name), norm(row.city), norm(row.state)].filter(Boolean).join(' ');\nreturn [{\n  json: {\n    ...row,\n    website_domain,\n    query_base: qBase,\n    processed: 'FALSE'\n  }\n}];\n"
      },
      "id": "Fn_Normalize",
      "name": "Normalize + Domain",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    },
    {
      "parameters": {
        "url": "https://maps.googleapis.com/maps/api/place/textsearch/json",
        "options": {},
        "queryParametersUi": {
          "parameter": [
            {
              "name": "query",
              "value": "{{$json[\"shop_name\"]}} {{$json[\"address\"]}} {{$json[\"city\"]}} {{$json[\"state\"]}} auto repair"
            },
            {
              "name": "key",
              "value": "={{$env.GOOGLE_PLACES_API_KEY}}"
            }
          ]
        }
      },
      "id": "HTTP_GPlaces_Search",
      "name": "Google Places Search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4
    },
    {
      "parameters": {
        "functionCode": "// Pick best place; pass place_id\nconst res = items[0].json;\nconst best = (res.results||[])[0] || {};\nreturn [{json:{place_id: best.place_id || '', place_name: best.name || ''}}];\n"
      },
      "id": "Fn_Pick_Place",
      "name": "Pick Place",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    },
    {
      "parameters": {
        "url": "https://maps.googleapis.com/maps/api/place/details/json",
        "options": {},
        "queryParametersUi": {
          "parameter": [
            {
              "name": "place_id",
              "value": "={{$json[\"place_id\"]}}"
            },
            {
              "name": "fields",
              "value": "name,formatted_phone_number,rating,user_ratings_total,website"
            },
            {
              "name": "key",
              "value": "={{$env.GOOGLE_PLACES_API_KEY}}"
            }
          ]
        }
      },
      "id": "HTTP_GPlaces_Details",
      "name": "Google Places Details",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4
    },
    {
      "parameters": {
        "functionCode": "// Merge Places details\nconst d = items[0].json.result || {};\nreturn [{\n  json: {\n    gbp_place_id: d.place_id || '',\n    gbp_name: d.name || '',\n    gbp_phone: d.formatted_phone_number || '',\n    gbp_rating: d.rating || '',\n    gbp_user_ratings_total: d.user_ratings_total || '',\n    website_url: d.website || $json.website_url || ''\n  }\n}];\n"
      },
      "id": "Fn_Merge_Places",
      "name": "Merge Places",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    },
    {
      "parameters": {
        "functionCode": "// Build URLs to fetch (homepage + /contact)\nconst url = $json.website_url || '';\nif (!url) { return [{json: {...$json, site_urls: []}}]; }\nlet base = url;\ntry { base = new URL(url).origin; } catch(e){}\nreturn [{\n  json: {\n    ...$json,\n    site_urls: [base, base + '/contact', base + '/about']\n  }\n}];\n"
      },
      "id": "Fn_Build_Site_URLs",
      "name": "Build Site URLs",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    },
    {
      "parameters": {
        "url": "={{$json[\"site_urls\"][0]}}",
        "options": {
          "ignoreResponseCode": true
        }
      },
      "id": "HTTP_Site_Fetch_1",
      "name": "Fetch Site 1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4
    },
    {
      "parameters": {
        "url": "={{$json[\"site_urls\"][1]}}",
        "options": {
          "ignoreResponseCode": true
        }
      },
      "id": "HTTP_Site_Fetch_2",
      "name": "Fetch Site 2",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4
    },
    {
      "parameters": {
        "url": "={{$json[\"site_urls\"][2]}}",
        "options": {
          "ignoreResponseCode": true
        }
      },
      "id": "HTTP_Site_Fetch_3",
      "name": "Fetch Site 3",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4
    },
    {
      "parameters": {
        "functionCode": "// Extract emails and candidate names from fetched HTML\nfunction extractEmails(text){\n  const re = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/ig;\n  const m = text.match(re) || [];\n  return Array.from(new Set(m));\n}\nfunction extractNames(text){\n  // crude: look for patterns like \"Owner: John Smith\" or \"Manager: Jane Doe\"\n  const re = /(Owner|Manager|Service Manager|Shop Owner)\\s*[:\\-]\\s*([A-Z][a-z]+\\s+[A-Z][a-z]+)/g;\n  const out = []; let m;\n  while ((m = re.exec(text)) !== null) out.push(m[2]);\n  return Array.from(new Set(out));\n}\nconst htmls = items.map(i => (i.json.body || '') + ' ' + (i.json.data || ''));\nconst big = htmls.join(' ');\nreturn [{\n  json: {\n    site_found_emails: extractEmails(big).join(';'),\n    site_found_names: extractNames(big).join(';')\n  }\n}];\n"
      },
      "id": "Fn_Extract_Email_Name",
      "name": "Extract Emails/Names",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    },
    {
      "parameters": {
        "url": "={{'https://rdap.org/domain/' + $json.website_domain}}",
        "options": {
          "ignoreResponseCode": true
        }
      },
      "id": "HTTP_RDAP",
      "name": "RDAP WHOIS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4
    },
    {
      "parameters": {
        "functionCode": "// Parse RDAP minimal fields\nconst j = items[0].json || {};\nlet registrar = j.ldhName || '';\nlet email = '';\nlet phone = '';\nif (j.entities) {\n  for (const e of j.entities) {\n    const v = (e.vcardArray && e.vcardArray[1]) || [];\n    for (const row of v) {\n      if (row[0] === 'email' && row[3]) email = row[3];\n      if (row[0] === 'tel' && row[3]) phone = row[3];\n    }\n  }\n}\nreturn [{json:{domain_registrar: registrar, whois_email: email, whois_phone: phone}}];\n"
      },
      "id": "Fn_Parse_RDAP",
      "name": "Parse RDAP",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    },
    {
      "parameters": {
        "functionCode": "// Build output row and score confidence\nfunction score(s){\n  let c = 0;\n  if (s.gbp_phone) c += 25;\n  if (s.site_found_emails) c += 25;\n  if (s.whois_email) c += 15;\n  if (s.website_url) c += 10;\n  if (s.gbp_rating) c += 5;\n  return Math.min(100, c);\n}\nconst now = new Date().toISOString();\nconst out = {\n  shop_name: $json.shop_name,\n  address: $json.address,\n  city: $json.city,\n  state: $json.state,\n  zip: $json.zip,\n  website_url: $json.website_url,\n  website_domain: $json.website_domain,\n  gbp_place_id: $json.gbp_place_id || '',\n  gbp_name: $json.gbp_name || '',\n  gbp_phone: $json.gbp_phone || '',\n  gbp_rating: $json.gbp_rating || '',\n  gbp_user_ratings_total: $json.gbp_user_ratings_total || '',\n  yelp_id: '',\n  yelp_name: '',\n  yelp_phone: '',\n  yelp_rating: '',\n  yelp_review_count: '',\n  sos_owner_name: '',\n  sos_entity_type: '',\n  sos_source_url: '',\n  license_owner: '',\n  license_number: '',\n  license_source_url: '',\n  linkedin_owner_name: '',\n  linkedin_owner_profile: '',\n  linkedin_source_url: '',\n  domain_registrar: $json.domain_registrar || '',\n  domain_registrant: '',\n  whois_email: $json.whois_email || '',\n  whois_phone: $json.whois_phone || '',\n  site_found_emails: $json.site_found_emails || '',\n  site_found_names: $json.site_found_names || '',\n  apollo_owner_email: '',\n  apollo_owner_phone: '',\n  rocketreach_owner_email: '',\n  rocketreach_owner_phone: '',\n  confidence_score: 0,\n  sources_used: '',\n  last_checked_utc: now\n};\nout.confidence_score = score(out);\nconst srcs = [];\nif (out.gbp_place_id) srcs.push('GooglePlaces');\nif (out.site_found_emails) srcs.push('SiteScrape');\nif (out.whois_email) srcs.push('RDAP');\nout.sources_used = srcs.join('|');\nreturn [{json: out}];\n"
      },
      "id": "Fn_Build_Output",
      "name": "Build Output Row",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2
    }
  ],
  "connections": {
    "Read next Input row": {
      "main": [
        [
          {
            "node": "Normalize + Domain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + Domain": {
      "main": [
        [
          {
            "node": "Google Places Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Places Search": {
      "main": [
        [
          {
            "node": "Pick Place",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Place": {
      "main": [
        [
          {
            "node": "Google Places Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Places Details": {
      "main": [
        [
          {
            "node": "Merge Places",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Places": {
      "main": [
        [
          {
            "node": "Build Site URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Site URLs": {
      "main": [
        [
          {
            "node": "Fetch Site 1",
            "type": "main",
            "index": 0
          },
          {
            "node": "RDAP WHOIS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Site 1": {
      "main": [
        [
          {
            "node": "Fetch Site 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Site 2": {
      "main": [
        [
          {
            "node": "Fetch Site 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Site 3": {
      "main": [
        [
          {
            "node": "Extract Emails/Names",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RDAP WHOIS": {
      "main": [
        [
          {
            "node": "Parse RDAP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Emails/Names": {
      "main": [
        [
          {
            "node": "Build Output Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse RDAP": {
      "main": [
        [
          {
            "node": "Build Output Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Output Row": {
      "main": [
        [
          {
            "node": "Write to Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "version": "2.0",
    "created": "2025-09-03T16:56:15.716067Z"
  }
}