Files

538 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 17 / Responde 17\`;
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 <YOUR_ACCESS_TOKEN>
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 <YOUR_PHONE_NUMBER_ID>
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); });