- Chat messages now render bold, italic, links, and line breaks - Bare URLs auto-linked, XSS-safe via HTML escaping before markdown - New leads create Opportunity with stage NEW in Twenty CRM - Applied to both chatbot-api and email-api (contact + quote forms) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
6.6 KiB
JavaScript
173 lines
6.6 KiB
JavaScript
import express from 'express';
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
const TWENTY_CRM_URL = process.env.TWENTY_CRM_URL || 'https://crm.ai-impress.com';
|
|
const TWENTY_CRM_API_KEY = process.env.TWENTY_CRM_API_KEY || '';
|
|
|
|
async function createLeadInCRM({ fullName, workEmail, companyName, jobTitle, phoneNumber, need, source }) {
|
|
if (!TWENTY_CRM_API_KEY || !workEmail) return;
|
|
const headers = { Authorization: `Bearer ${TWENTY_CRM_API_KEY}`, 'Content-Type': 'application/json' };
|
|
try {
|
|
// Split name
|
|
const parts = fullName.trim().split(/\s+/);
|
|
const firstName = parts[0] || '';
|
|
const lastName = parts.slice(1).join(' ') || '';
|
|
|
|
// Create or find company
|
|
let companyId = null;
|
|
if (companyName) {
|
|
const compRes = await fetch(`${TWENTY_CRM_URL}/rest/companies`, { headers, method: 'GET' });
|
|
// Search not reliable, just create
|
|
const createComp = await fetch(`${TWENTY_CRM_URL}/rest/companies`, {
|
|
method: 'POST', headers, body: JSON.stringify({ name: companyName }),
|
|
});
|
|
if (createComp.status === 201) {
|
|
const cd = await createComp.json();
|
|
companyId = cd.data?.createCompany?.id;
|
|
}
|
|
}
|
|
|
|
// Create person
|
|
const personBody = {
|
|
name: { firstName, lastName },
|
|
emails: { primaryEmail: workEmail, additionalEmails: [] },
|
|
};
|
|
if (companyId) personBody.companyId = companyId;
|
|
if (jobTitle) personBody.jobTitle = jobTitle;
|
|
if (phoneNumber) personBody.phones = {
|
|
primaryPhoneNumber: phoneNumber, primaryPhoneCountryCode: '', primaryPhoneCallingCode: '', additionalPhones: [],
|
|
};
|
|
|
|
const personRes = await fetch(`${TWENTY_CRM_URL}/rest/people`, {
|
|
method: 'POST', headers, body: JSON.stringify(personBody),
|
|
});
|
|
if (personRes.status === 201) {
|
|
const pd = await personRes.json();
|
|
const personId = pd.data?.createPerson?.id;
|
|
// Create note
|
|
if (personId && need) {
|
|
const noteRes = await fetch(`${TWENTY_CRM_URL}/rest/notes`, {
|
|
method: 'POST', headers, body: JSON.stringify({ title: `${source}: ${need}` }),
|
|
});
|
|
if (noteRes.status === 201) {
|
|
const nd = await noteRes.json();
|
|
const noteId = nd.data?.createNote?.id;
|
|
if (noteId) {
|
|
await fetch(`${TWENTY_CRM_URL}/rest/noteTargets`, {
|
|
method: 'POST', headers, body: JSON.stringify({ noteId, personId }),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
console.log(`CRM lead created: ${personId} (${fullName})`);
|
|
// Create opportunity in NEW stage
|
|
const oppBody = { name: `${fullName} — ${(need || 'Contact form').substring(0, 50)}`, stage: 'NEW' };
|
|
if (companyId) oppBody.companyId = companyId;
|
|
if (personId) oppBody.pointOfContactId = personId;
|
|
await fetch(`${TWENTY_CRM_URL}/rest/opportunities`, {
|
|
method: 'POST', headers, body: JSON.stringify(oppBody),
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('CRM error:', err.message);
|
|
}
|
|
}
|
|
|
|
app.post('/api/contact', async (req, res) => {
|
|
const { fullName, workEmail, companyName, jobTitle, automationNeed, phoneNumber } = req.body;
|
|
|
|
if (!fullName || !workEmail) {
|
|
return res.status(400).json({ error: 'Missing required fields' });
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('https://api.resend.com/emails', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
from: 'AImpress Website <noreply@ai-impress.com>',
|
|
to: ['hello@ai-impress.com'],
|
|
subject: `New lead: ${fullName} — ${companyName || 'N/A'}`,
|
|
html: `
|
|
<h2>New Contact Form Submission</h2>
|
|
<p><strong>Name:</strong> ${fullName}</p>
|
|
<p><strong>Email:</strong> ${workEmail}</p>
|
|
<p><strong>Company:</strong> ${companyName || 'N/A'}</p>
|
|
<p><strong>Job Title:</strong> ${jobTitle || 'N/A'}</p>
|
|
<p><strong>Automation Need:</strong> ${automationNeed || 'N/A'}</p>
|
|
<p><strong>Phone:</strong> ${phoneNumber || 'N/A'}</p>
|
|
`,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.text();
|
|
console.error('Resend error:', err);
|
|
return res.status(502).json({ error: 'Email delivery failed' });
|
|
}
|
|
|
|
// Create lead in CRM (async, don't block response)
|
|
createLeadInCRM({ fullName, workEmail, companyName, jobTitle, phoneNumber, need: automationNeed, source: 'Contact form' });
|
|
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('Email error:', err);
|
|
res.status(500).json({ error: 'Internal error' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/quote', async (req, res) => {
|
|
const { fullName, workEmail, companyName, jobTitle, phoneNumber, service, projectDescription } = req.body;
|
|
|
|
if (!fullName || !workEmail || !service || !projectDescription) {
|
|
return res.status(400).json({ error: 'Missing required fields' });
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('https://api.resend.com/emails', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
from: 'AImpress Website <noreply@ai-impress.com>',
|
|
to: ['hello@ai-impress.com'],
|
|
subject: `Quote request: ${fullName} — ${service}`,
|
|
html: `
|
|
<h2>New Quote Request</h2>
|
|
<p><strong>Name:</strong> ${fullName}</p>
|
|
<p><strong>Email:</strong> ${workEmail}</p>
|
|
<p><strong>Company:</strong> ${companyName || 'N/A'}</p>
|
|
<p><strong>Job Title:</strong> ${jobTitle || 'N/A'}</p>
|
|
<p><strong>Phone:</strong> ${phoneNumber || 'N/A'}</p>
|
|
<hr>
|
|
<p><strong>Service:</strong> ${service}</p>
|
|
<p><strong>Project Description:</strong></p>
|
|
<p>${projectDescription.replace(/\n/g, '<br>')}</p>
|
|
`,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.text();
|
|
console.error('Resend error:', err);
|
|
return res.status(502).json({ error: 'Email delivery failed' });
|
|
}
|
|
|
|
// Create lead in CRM (async, don't block response)
|
|
createLeadInCRM({ fullName, workEmail, companyName, jobTitle, phoneNumber, need: `${service}: ${projectDescription}`, source: 'Quote request' });
|
|
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('Email error:', err);
|
|
res.status(500).json({ error: 'Internal error' });
|
|
}
|
|
});
|
|
|
|
app.listen(3001, () => console.log('Email API listening on :3001'));
|