// create_whatsapp_workflow.js — Prisa Yachts WhatsApp Bot for n8n const https = require('https'); const N8N_BASE = process.env.N8N_BASE_URL || 'https://n8n.crewinghunters.com'; const API_KEY = process.env.N8N_API_KEY; if (!API_KEY) { console.error('ERROR: N8N_API_KEY environment variable is required'); process.exit(1); } /* ─────────────────────────────────────────────────────────── helpers */ function apiCall(method, path, body) { return new Promise((resolve, reject) => { const data = body ? JSON.stringify(body) : null; const opts = { hostname: 'n8n.crewinghunters.com', path: '/api/v1' + path, method, headers: { 'X-N8N-API-KEY': API_KEY, 'Content-Type': 'application/json', 'Accept': 'application/json', ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } }; const req = https.request(opts, res => { let buf = ''; res.on('data', c => buf += c); res.on('end', () => { try { resolve({ status: res.statusCode, data: JSON.parse(buf) }); } catch { resolve({ status: res.statusCode, data: buf }); } }); }); req.on('error', reject); if (data) req.write(data); req.end(); }); } /* ─────────────────────────────────────── state-machine code (runs inside n8n) */ const STATE_MACHINE_CODE = ` const WELCOME = \`👋 Welcome to Prisa Yachts LLC! Bienvenido a Prisa Yachts LLC Safe Command ♦ Luxury Maintenance and Care Select your service / Selecciona tu servicio: 1️⃣ Engines & Mechanical 2️⃣ Electrical & Electronics / NMEA 3️⃣ Teak Deck Recovery 4️⃣ Captaining & Crewing 5️⃣ Yacht Care & Detailing 6️⃣ Crew Placement & Staffing 7️⃣ Other / Otro Reply 1–7 / Responde 1–7\`; const CAT_MSGS = { 1:\`🔧 Engines & Mechanical 1. Vessel name & type? 2. Engine brand & approx. year? 3. Briefly describe the issue: (noise, leak, won't start, overheating, routine service, fuel system, etc.) 4. Current location / Marina? 5. When do you need service? · Flexible · This week · ASAP ───────────────── Your contact info: - Full name: - Phone: - Email: ───────────────── Thank you! Our specialist will contact you within 2 hours. 🛥️\`, 2:\`⚡ Electrical & Electronics / NMEA 1. Vessel name & type? 2. Briefly describe the issue or installation: (wiring, shore power, panel, NMEA 2000, AIS, chartplotter, VHF, autopilot, etc.) 3. Is it a new installation or a repair? · New install · Repair · Diagnosis 4. Current location / Marina? 5. When do you need service? · Flexible · This week · ASAP ───────────────── Your contact info: - Full name: - Phone: - Email: ───────────────── Thank you! Our specialist will contact you within 2 hours. 🛥️\`, 3:\`🪵 Teak Deck Recovery 1. Vessel name & LOA (feet)? 2. Teak condition: · Cleaning needed · Caulking damaged · Planks cracked · Full replacement 3. Which deck areas? (cockpit, flybridge, side decks, all) 4. Briefly describe the current condition: 5. Current location / Marina? 6. When do you need service? · Flexible · This week · ASAP ───────────────── Your contact info: - Full name: - Phone: - Email: ───────────────── Thank you! Our specialist will contact you within 2 hours. 🛥️\`, 4:\`⚓ Captaining & Crewing 1. Vessel name, type & LOA (feet)? 2. Service needed: · Day trip · Charter · Vessel delivery · Offshore passage · Event crew 3. Departure & destination? 4. Date & duration? 5. Number of crew needed? 6. Any special requirements? ───────────────── Your contact info: - Full name: - Phone: - Email: ───────────────── Thank you! Our captain will contact you within 2 hours. 🛥️\`, 5:\`🛥️ Yacht Care & Detailing 1. Vessel name, type & LOA (feet)? 2. Service needed: · Full wash & wax · Hull polish · Interior cleaning · Brightwork · Antifouling prep · Full detail 3. Briefly describe current condition: 4. Current location / Marina? 5. When do you need service? · Flexible · This week · ASAP ───────────────── Your contact info: - Full name: - Phone: - Email: ───────────────── Thank you! Our team will contact you within 2 hours. 🛥️\`, 6:\`👥 Crew Placement & Staffing 1. Vessel name, type & LOA (feet)? 2. Position(s) needed: · Captain · Chief Mate / Deck Officer · Deckhand · Chief Engineer · Stewardess / Interior crew · Chef · Other 3. Contract type: · Permanent · Seasonal · One-way delivery · Day work · Relief / Temporary 4. Start date & duration? 5. Vessel flag & trading area? 6. Any certifications required? (STCW, license type, experience level) ───────────────── Your contact info: - Full name: - Phone: - Email: - Company (if applicable): ───────────────── Thank you! Our crewing team will contact you within 2 hours. 🛥️\`, 7:\`📋 Other / Otro 1. Vessel name & type? 2. Briefly describe what you need: 3. Current location / Marina? 4. When do you need service? · Flexible · This week · ASAP ───────────────── Your contact info: - Full name: - Phone: - Email: ───────────────── Thank you! Our team will contact you within 2 hours. 🛥️\` }; const CONFIRMATION = \`✅ Request received! / ¡Solicitud recibida! We have all your information and a Prisa Yachts specialist will contact you within 2 hours. 📞 (786) 396-3315 📧 info@prisayachts.com 🌐 prisayachts.com 📸 @prisayachts Safe Command ♦ Luxury Maintenance and Care\`; const CAT_NAMES = { 1:'Engines & Mechanical', 2:'Electrical & Electronics / NMEA', 3:'Teak Deck Recovery', 4:'Captaining & Crewing', 5:'Yacht Care & Detailing', 6:'Crew Placement & Staffing', 7:'Other / Otro' }; const ROUTING = {1:'alvaro',2:'alvaro',3:'federico',4:'federico',5:'federico',6:'federico',7:'both'}; const ALVARO = '19546554084'; const FEDERICO = '17542093375'; const RESET_WORDS = ['menu','start','hola','hello','hi']; const TIMEOUT_MS = 24 * 60 * 60 * 1000; // ── parse webhook payload ────────────────────────────────────────── const raw = $input.first().json; // Meta GET verification const q = raw.query || {}; if (q['hub.challenge']) { return [{ json: { action: 'webhook_verify', challenge: String(q['hub.challenge']) } }]; } const body = raw.body || raw; const msg = body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]; if (!msg || msg.type !== 'text') { return [{ json: { action: 'ignore' } }]; } const from = msg.from; const msgText = msg.text.body.trim(); const msgLow = msgText.toLowerCase(); // ── state machine ───────────────────────────────────────────────── const sd = $getWorkflowStaticData('global'); if (!sd.states) sd.states = {}; const now = Date.now(); let state = sd.states[from] || { step: 'new', lastActivity: 0, data: {} }; if (now - state.lastActivity > TIMEOUT_MS) state = { step: 'new', lastActivity: now, data: {} }; state.lastActivity = now; const reply = (messageText, extra={}) => { sd.states[from] = state; return [{ json: { action: 'send_to_client', to: from, messageText, notifySpecialist: false, ...extra } }]; }; if (RESET_WORDS.includes(msgLow) || state.step === 'new') { state.step = 'waiting_category'; state.data = {}; return reply(WELCOME); } if (state.step === 'waiting_category') { const cat = parseInt(msgText, 10); if (cat >= 1 && cat <= 7) { state.step = 'collecting_info'; state.data.category = cat; return reply(CAT_MSGS[cat]); } return reply(WELCOME); } if (state.step === 'collecting_info') { const cat = state.data.category; const ts = new Date().toLocaleString('en-US', { timeZone: 'America/New_York' }); const specMsg = \`🔔 NEW SERVICE REQUEST — Prisa Yachts 📋 Category: \${CAT_NAMES[cat]} 👤 Client WhatsApp: +\${from} 📝 Info received: \${msgText} 🕐 Received: \${ts} ET\`; state.step = 'complete'; return reply(CONFIRMATION, { notifySpecialist: true, routing: ROUTING[cat], specialistMsg: specMsg, alvaroPhone: ALVARO, federicoPhone: FEDERICO }); } // complete → restart state.step = 'waiting_category'; state.data = {}; return reply(WELCOME); `; /* ────────────────────────────────────────── build workflow JSON */ function buildWorkflow(credId) { const credential = { httpHeaderAuth: { id: String(credId), name: 'WhatsApp_Prisa' } }; const waUrl = 'https://graph.facebook.com/v18.0/PLACEHOLDER_PHONE_NUMBER_ID/messages'; const ifStrNode = (id, name, pos, left, right) => ({ id, name, type: 'n8n-nodes-base.if', typeVersion: 2, position: pos, parameters: { conditions: { combinator:'and', conditions:[{ leftValue: left, rightValue: right, operator: { type:'string', operation:'equals' } }]}, options: {} } }); const httpNode = (id, name, pos, jsonBody) => ({ id, name, type: 'n8n-nodes-base.httpRequest', typeVersion: 4.2, position: pos, credentials: credential, parameters: { method: 'POST', url: waUrl, authentication: 'genericCredentialType', genericAuthType: 'httpHeaderAuth', sendBody: true, specifyBody: 'json', jsonBody, options: {} } }); const clientBody = `={{ JSON.stringify({messaging_product:"whatsapp",to:$json.to,type:"text",text:{body:$json.messageText}}) }}`; const alvaroBody = `={{ JSON.stringify({messaging_product:"whatsapp",to:$json.alvaroPhone,type:"text",text:{body:$json.specialistMsg}}) }}`; const fedBody = `={{ JSON.stringify({messaging_product:"whatsapp",to:$json.federicoPhone,type:"text",text:{body:$json.specialistMsg}}) }}`; return { name: 'Prisa Yachts — WhatsApp Bot', nodes: [ // 1 Webhook { id: 'n-wh', name: 'WhatsApp Webhook', type: 'n8n-nodes-base.webhook', typeVersion: 2, position: [200, 300], parameters: { httpMethod:'POST', path:'prisa-whatsapp', responseMode:'onReceived', options:{} } }, // 2 State machine { id: 'n-sm', name: 'State Machine', type: 'n8n-nodes-base.code', typeVersion: 2, position: [450, 300], parameters: { mode:'runOnceForAllItems', jsCode: STATE_MACHINE_CODE } }, // 3 IF: is webhook_verify? ifStrNode('n-ifv', 'Is Verify?', [700, 300], '={{ $json.action }}', 'webhook_verify'), // 4 Respond with challenge { id: 'n-respond', name: 'Respond — Verify', type: 'n8n-nodes-base.respondToWebhook', typeVersion: 1.1, position: [950, 140], parameters: { respondWith: 'text', responseBody: '={{ $json.challenge }}', options: { responseCode: 200 } } }, // 5 IF: is send_to_client? ifStrNode('n-ifs', 'Is Send?', [950, 380], '={{ $json.action }}', 'send_to_client'), // 6 Send to client httpNode('n-hsc', 'Send WhatsApp — Client', [1200, 280], clientBody), // 7 Ignore (no-op) { id:'n-nop', name:'Ignore', type:'n8n-nodes-base.noOp', typeVersion:1, position:[1200,520], parameters:{} }, // 8 IF notifySpecialist { id: 'n-ifn', name: 'Notify Specialist?', type: 'n8n-nodes-base.if', typeVersion: 2, position: [1450, 280], parameters: { conditions: { combinator:'and', conditions:[{ leftValue:'={{ $json.notifySpecialist }}', rightValue: true, operator:{ type:'boolean', operation:'equals' } }]}, options: {} } }, // 9 IF routing === 'both' → true: both, false: check alvaro/fed ifStrNode('n-ifb', 'Is Both?', [1700, 200], '={{ $json.routing }}', 'both'), // 10 IF routing === 'alvaro' → true: alvaro only, false: federico only ifStrNode('n-ifa', 'Is Alvaro?', [1700, 420], '={{ $json.routing }}', 'alvaro'), // 11,12 HTTP nodes httpNode('n-hal', 'Send WhatsApp — Alvaro', [1950, 180], alvaroBody), httpNode('n-hfe', 'Send WhatsApp — Federico', [1950, 420], fedBody), ], connections: { 'WhatsApp Webhook': { main: [[{ node:'State Machine', type:'main', index:0 }]] }, 'State Machine': { main: [[{ node:'Is Verify?', type:'main', index:0 }]] }, 'Is Verify?': { main: [ [{ node:'Respond — Verify', type:'main', index:0 }], // true [{ node:'Is Send?', type:'main', index:0 }] // false ]}, 'Is Send?': { main: [ [{ node:'Send WhatsApp — Client', type:'main', index:0 }], // true [{ node:'Ignore', type:'main', index:0 }] // false ]}, 'Send WhatsApp — Client': { main: [[{ node:'Notify Specialist?', type:'main', index:0 }]] }, 'Notify Specialist?': { main: [ [{ node:'Is Both?', type:'main', index:0 }], // true → route [] // false → end ]}, 'Is Both?': { main: [ // true → send to BOTH simultaneously [{ node:'Send WhatsApp — Alvaro', type:'main', index:0 }, { node:'Send WhatsApp — Federico', type:'main', index:0 }], [{ node:'Is Alvaro?', type:'main', index:0 }] // false → check which one ]}, 'Is Alvaro?': { main: [ [{ node:'Send WhatsApp — Alvaro', type:'main', index:0 }], // true [{ node:'Send WhatsApp — Federico', type:'main', index:0 }] // false (must be federico) ]} }, settings: { executionOrder: 'v1' } }; } /* ──────────────────────────────────────────────────────────── main */ async function cleanup() { const wfs = await apiCall('GET', '/workflows?limit=50'); for (const wf of (wfs.data?.data || [])) { if (wf.name && wf.name.includes('Prisa Yachts')) { await apiCall('DELETE', `/workflows/${wf.id}`); console.log(` 🗑 Deleted workflow: ${wf.id}`); } } const creds = await apiCall('GET', '/credentials?limit=50'); for (const c of (creds.data?.data || [])) { if (c.name === 'WhatsApp_Prisa') { await apiCall('DELETE', `/credentials/${c.id}`); console.log(` 🗑 Deleted credential: ${c.id}`); } } } async function main() { console.log('\n🚀 Prisa Yachts — WhatsApp Bot Deployment\n'); console.log('0/4 Cleaning up previous attempts...'); await cleanup(); console.log(' ✅ Clean'); // 1. Credential console.log('1/4 Creating WhatsApp credential...'); const credRes = await apiCall('POST', '/credentials', { name: 'WhatsApp_Prisa', type: 'httpHeaderAuth', data: { name: 'Authorization', value: 'Bearer PLACEHOLDER_PENDING_META_APPROVAL' } }); if (![200,201].includes(credRes.status)) { console.error(' ❌ Credential error:', JSON.stringify(credRes.data, null, 2)); process.exit(1); } const credId = credRes.data.id; console.log(` ✅ Credential created id=${credId}`); // 2. Workflow console.log('2/4 Creating workflow...'); const wfRes = await apiCall('POST', '/workflows', buildWorkflow(credId)); if (![200,201].includes(wfRes.status)) { console.error(' ❌ Workflow error:', JSON.stringify(wfRes.data, null, 2)); process.exit(1); } const wfId = wfRes.data.id; console.log(` ✅ Workflow created id=${wfId}`); // 3. Activate console.log('3/4 Activating workflow...'); const actRes = await apiCall('POST', `/workflows/${wfId}/activate`); if (actRes.status !== 200) { console.warn(' ⚠️ Activation returned', actRes.status, JSON.stringify(actRes.data)); } else { console.log(' ✅ Workflow ACTIVE'); } // 4. Confirm console.log('4/4 Verifying...'); const chk = await apiCall('GET', `/workflows/${wfId}`); console.log(` ✅ active = ${chk.data?.active}`); console.log(` ══════════════════════════════════════════════════════ ✅ DEPLOYMENT COMPLETE ══════════════════════════════════════════════════════ Webhook URL → https://n8n.crewinghunters.com/webhook/prisa-whatsapp Workflow ID → ${wfId} Credential → WhatsApp_Prisa (id=${credId}) ══════════════════════════════════════════════════════ NEXT STEPS — after Meta approves your WhatsApp account ══════════════════════════════════════════════════════ 1. n8n → Settings → Credentials → WhatsApp_Prisa Change value from PLACEHOLDER_PENDING_META_APPROVAL to Bearer 2. Open workflow "Prisa Yachts — WhatsApp Bot" In these 3 nodes, change the URL: • Send WhatsApp — Client • Send WhatsApp — Alvaro • Send WhatsApp — Federico Replace PLACEHOLDER_PHONE_NUMBER_ID With 3. Meta Developer Portal → WhatsApp → Configuration Webhook URL : https://n8n.crewinghunters.com/webhook/prisa-whatsapp Verify Token : any string (e.g. "prisayachts2025") Subscribe to : messages 4. Send "hola" to +17863963315 from any WhatsApp to test. ══════════════════════════════════════════════════════ ROUTING ══════════════════════════════════════════════════════ 1 Engines & Mechanical → Alvaro +19546554084 2 Electrical & Electronics → Alvaro +19546554084 3 Teak Deck Recovery → Federico +17542093375 4 Captaining & Crewing → Federico +17542093375 5 Yacht Care & Detailing → Federico +17542093375 6 Crew Placement & Staffing → Federico +17542093375 7 Other / Otro → BOTH ══════════════════════════════════════════════════════ `); } main().catch(e => { console.error('Fatal:', e); process.exit(1); });