feat: n8n initial commit — JavaScript (Node.js) n8n workflow automation + axios/cheerio/puppeteer/xlsx

This commit is contained in:
2026-07-03 12:15:42 -04:00
commit f4731fa87e
11 changed files with 3561 additions and 0 deletions
@@ -0,0 +1,537 @@
// 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); });