@conversionpros/aiva 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/bin/aiva.js +26 -14
  2. package/lib/bluebubbles.js +145 -0
  3. package/lib/config-gen.js +253 -0
  4. package/lib/config.js +1 -1
  5. package/lib/constants.js +72 -0
  6. package/lib/launch-agent.js +112 -0
  7. package/lib/prerequisites.js +236 -0
  8. package/lib/process.js +59 -145
  9. package/lib/setup.js +224 -194
  10. package/lib/validate.js +194 -0
  11. package/package.json +7 -32
  12. package/auto-deploy.js +0 -190
  13. package/cli-sync.js +0 -126
  14. package/d2a-prompt-template.txt +0 -106
  15. package/diagnostics-api.js +0 -304
  16. package/docs/ara-dedup-fix-scope.md +0 -112
  17. package/docs/ara-fix-round2-scope.md +0 -61
  18. package/docs/ara-greeting-fix-scope.md +0 -70
  19. package/docs/calendar-date-fix-scope.md +0 -28
  20. package/docs/getting-started.md +0 -115
  21. package/docs/network-architecture-rollout-scope.md +0 -43
  22. package/docs/scope-google-oauth-integration.md +0 -351
  23. package/docs/settings-page-scope.md +0 -50
  24. package/docs/xai-imagine-scope.md +0 -116
  25. package/docs/xai-voice-integration-scope.md +0 -115
  26. package/docs/xai-voice-tools-scope.md +0 -165
  27. package/email-router.js +0 -512
  28. package/follow-up-handler.js +0 -606
  29. package/gateway-monitor.js +0 -158
  30. package/google-email.js +0 -379
  31. package/google-oauth.js +0 -310
  32. package/grok-imagine.js +0 -97
  33. package/health-reporter.js +0 -287
  34. package/invisible-prefix-base.txt +0 -206
  35. package/invisible-prefix-owner.txt +0 -26
  36. package/invisible-prefix-slim.txt +0 -10
  37. package/invisible-prefix.txt +0 -43
  38. package/knowledge-base.js +0 -472
  39. package/lib/cli.js +0 -19
  40. package/lib/server.js +0 -42
  41. package/meta-capi.js +0 -206
  42. package/meta-leads.js +0 -411
  43. package/notion-oauth.js +0 -323
  44. package/public/agent-config.html +0 -241
  45. package/public/aiva-avatar-anime.png +0 -0
  46. package/public/css/docs.css.bak +0 -688
  47. package/public/css/onboarding.css +0 -543
  48. package/public/diagrams/claude-subscription-pool.html +0 -329
  49. package/public/diagrams/claude-subscription-pool.png +0 -0
  50. package/public/docs-icon.png +0 -0
  51. package/public/escalation.html +0 -237
  52. package/public/group-config.html +0 -300
  53. package/public/icon-192.png +0 -0
  54. package/public/icon-512.png +0 -0
  55. package/public/icons/agents.svg +0 -1
  56. package/public/icons/attach.svg +0 -1
  57. package/public/icons/characters.svg +0 -1
  58. package/public/icons/chat.svg +0 -1
  59. package/public/icons/docs.svg +0 -1
  60. package/public/icons/heartbeat.svg +0 -1
  61. package/public/icons/messages.svg +0 -1
  62. package/public/icons/mic.svg +0 -1
  63. package/public/icons/notes.svg +0 -1
  64. package/public/icons/settings.svg +0 -1
  65. package/public/icons/tasks.svg +0 -1
  66. package/public/images/onboarding/p0-communication-layer.png +0 -0
  67. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  68. package/public/images/onboarding/p0-learning-model.png +0 -0
  69. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  70. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  71. package/public/images/onboarding/p4-context-compounds.png +0 -0
  72. package/public/images/onboarding/p4-message-router.png +0 -0
  73. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  74. package/public/images/onboarding/p4-send-messages.png +0 -0
  75. package/public/images/onboarding/p6-be-precise.png +0 -0
  76. package/public/images/onboarding/p6-review-escalations.png +0 -0
  77. package/public/images/onboarding/p6-voice-input.png +0 -0
  78. package/public/images/onboarding/p7-completion.png +0 -0
  79. package/public/index.html +0 -11594
  80. package/public/js/onboarding.js +0 -699
  81. package/public/manifest.json +0 -24
  82. package/public/messages-v2.html +0 -2824
  83. package/public/permission-approve.html.bak +0 -107
  84. package/public/permissions.html +0 -150
  85. package/public/styles/design-system.css +0 -68
  86. package/router-db.js +0 -604
  87. package/router-utils.js +0 -28
  88. package/router-v2/adapters/imessage.js +0 -191
  89. package/router-v2/adapters/quo.js +0 -82
  90. package/router-v2/adapters/whatsapp.js +0 -192
  91. package/router-v2/contact-manager.js +0 -234
  92. package/router-v2/conversation-engine.js +0 -498
  93. package/router-v2/data/knowledge-base.json +0 -176
  94. package/router-v2/data/router-v2.db +0 -0
  95. package/router-v2/data/router-v2.db-shm +0 -0
  96. package/router-v2/data/router-v2.db-wal +0 -0
  97. package/router-v2/data/router.db +0 -0
  98. package/router-v2/db.js +0 -457
  99. package/router-v2/escalation-bridge.js +0 -540
  100. package/router-v2/follow-up-engine.js +0 -347
  101. package/router-v2/index.js +0 -441
  102. package/router-v2/ingestion.js +0 -213
  103. package/router-v2/knowledge-base.js +0 -231
  104. package/router-v2/lead-qualifier.js +0 -152
  105. package/router-v2/learning-loop.js +0 -202
  106. package/router-v2/outbound-sender.js +0 -160
  107. package/router-v2/package.json +0 -13
  108. package/router-v2/permission-gate.js +0 -86
  109. package/router-v2/playbook.js +0 -177
  110. package/router-v2/prompts/base.js +0 -52
  111. package/router-v2/prompts/first-contact.js +0 -38
  112. package/router-v2/prompts/lead-qualification.js +0 -37
  113. package/router-v2/prompts/scheduling.js +0 -72
  114. package/router-v2/prompts/style-overrides.js +0 -22
  115. package/router-v2/scheduler.js +0 -301
  116. package/router-v2/scripts/migrate-v1-to-v2.js +0 -215
  117. package/router-v2/scripts/seed-faq.js +0 -67
  118. package/router-v2/seed-knowledge-base.js +0 -39
  119. package/router-v2/utils/ai.js +0 -129
  120. package/router-v2/utils/phone.js +0 -52
  121. package/router-v2/utils/response-validator.js +0 -98
  122. package/router-v2/utils/sanitize.js +0 -222
  123. package/router.js +0 -5005
  124. package/routes/google-calendar.js +0 -186
  125. package/scripts/deploy.sh +0 -62
  126. package/scripts/macos-calendar.sh +0 -232
  127. package/scripts/onboard-device.sh +0 -466
  128. package/server.js +0 -5131
  129. package/start.sh +0 -24
  130. package/templates/AGENTS.md +0 -548
  131. package/templates/IDENTITY.md +0 -15
  132. package/templates/docs-agents.html +0 -132
  133. package/templates/docs-app.html +0 -130
  134. package/templates/docs-home.html +0 -83
  135. package/templates/docs-imessage.html +0 -121
  136. package/templates/docs-tasks.html +0 -123
  137. package/templates/docs-tips.html +0 -175
  138. package/templates/getting-started.html +0 -809
  139. package/templates/invisible-prefix-base.txt +0 -171
  140. package/templates/invisible-prefix-owner.txt +0 -282
  141. package/templates/invisible-prefix.txt +0 -338
  142. package/templates/manifest.json +0 -61
  143. package/templates/memory-org/clients.md +0 -7
  144. package/templates/memory-org/credentials.md +0 -9
  145. package/templates/memory-org/devices.md +0 -7
  146. package/templates/updates.html +0 -464
  147. package/tts-proxy.js +0 -96
  148. package/voice-call-local.js +0 -731
  149. package/voice-call.js +0 -732
  150. package/wa-listener.js +0 -354
package/meta-capi.js DELETED
@@ -1,206 +0,0 @@
1
- /**
2
- * Meta Conversions API (CAPI) Module
3
- * Sends server-side events to Meta for optimization signals.
4
- */
5
- const crypto = require('crypto');
6
- const https = require('https');
7
-
8
- const PIXEL_ID = process.env.META_PIXEL_ID || '1257861836285895';
9
- const ACCESS_TOKEN = process.env.META_ACCESS_TOKEN;
10
- const API_VERSION = 'v21.0';
11
-
12
- if (!ACCESS_TOKEN) {
13
- console.warn('[meta-capi] META_ACCESS_TOKEN not set - CAPI events will fail');
14
- }
15
-
16
- function sha256(value) {
17
- if (!value) return undefined;
18
- return crypto.createHash('sha256').update(String(value).toLowerCase().trim()).digest('hex');
19
- }
20
-
21
- function generateEventId() {
22
- return crypto.randomUUID();
23
- }
24
-
25
- /**
26
- * Send an event to Meta Conversions API.
27
- * @param {string} eventName - Lead, Schedule, Purchase, ViewContent, etc.
28
- * @param {object} eventData - { sourceUrl, customData, actionSource }
29
- * @param {object} userData - { email, phone, firstName, lastName, ... } (raw, will be hashed)
30
- * @returns {Promise<object>}
31
- */
32
- async function sendEvent(eventName, eventData = {}, userData = {}) {
33
- const eventId = generateEventId();
34
- const eventTime = Math.floor(Date.now() / 1000);
35
-
36
- const hashedUserData = {};
37
- if (userData.email) hashedUserData.em = [sha256(userData.email)];
38
- if (userData.phone) hashedUserData.ph = [sha256(userData.phone.replace(/\D/g, ''))];
39
- if (userData.firstName) hashedUserData.fn = [sha256(userData.firstName)];
40
- if (userData.lastName) hashedUserData.ln = [sha256(userData.lastName)];
41
- if (userData.city) hashedUserData.ct = [sha256(userData.city)];
42
- if (userData.state) hashedUserData.st = [sha256(userData.state)];
43
- if (userData.zip) hashedUserData.zp = [sha256(userData.zip)];
44
- if (userData.country) hashedUserData.country = [sha256(userData.country)];
45
- if (userData.clientIpAddress) hashedUserData.client_ip_address = userData.clientIpAddress;
46
- if (userData.clientUserAgent) hashedUserData.client_user_agent = userData.clientUserAgent;
47
- if (userData.fbc) hashedUserData.fbc = userData.fbc;
48
- if (userData.fbp) hashedUserData.fbp = userData.fbp;
49
-
50
- const payload = {
51
- data: [
52
- {
53
- event_name: eventName,
54
- event_time: eventTime,
55
- event_id: eventId,
56
- action_source: eventData.actionSource || 'website',
57
- event_source_url: eventData.sourceUrl || 'https://www.conversionmarketingpros.com',
58
- user_data: hashedUserData,
59
- ...(eventData.customData ? { custom_data: eventData.customData } : {}),
60
- },
61
- ],
62
- ...(eventData.testEventCode ? { test_event_code: eventData.testEventCode } : {}),
63
- };
64
-
65
- const body = JSON.stringify(payload);
66
- const token = ACCESS_TOKEN;
67
-
68
- return new Promise((resolve, reject) => {
69
- const req = https.request(
70
- {
71
- hostname: 'graph.facebook.com',
72
- path: `/${API_VERSION}/${PIXEL_ID}/events?access_token=${encodeURIComponent(token)}`,
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
75
- },
76
- (res) => {
77
- let data = '';
78
- res.on('data', (chunk) => (data += chunk));
79
- res.on('end', () => {
80
- try {
81
- const parsed = JSON.parse(data);
82
- console.log(`[meta-capi] ${eventName} sent | event_id=${eventId} | status=${res.statusCode} | events_received=${parsed.events_received ?? '?'}`);
83
- resolve({ ok: res.statusCode === 200, eventId, response: parsed });
84
- } catch (e) {
85
- console.error(`[meta-capi] Parse error: ${data}`);
86
- reject(e);
87
- }
88
- });
89
- }
90
- );
91
- req.on('error', reject);
92
- req.write(body);
93
- req.end();
94
- });
95
- }
96
-
97
- /**
98
- * Express router factory for CAPI webhook endpoints.
99
- */
100
- function createRouter(express) {
101
- const router = express.Router();
102
-
103
- // Auth middleware - accept internal header or webhook secret
104
- const WEBHOOK_SECRET = process.env.META_CAPI_WEBHOOK_SECRET || 'capi-webhook-2026';
105
- router.use((req, res, next) => {
106
- if (
107
- req.headers['x-aiva-internal'] === 'true' ||
108
- req.headers['x-webhook-secret'] === WEBHOOK_SECRET ||
109
- req.method === 'GET' // allow test endpoint
110
- ) {
111
- return next();
112
- }
113
- return res.status(401).json({ error: 'Unauthorized' });
114
- });
115
-
116
- // Extract contact info from GHL webhook payload
117
- function extractContact(body) {
118
- // GHL sends contact data in various shapes
119
- const contact = body.contact || body;
120
- return {
121
- email: contact.email || body.email,
122
- phone: contact.phone || body.phone,
123
- firstName: contact.firstName || contact.first_name || body.firstName || body.first_name,
124
- lastName: contact.lastName || contact.last_name || body.lastName || body.last_name,
125
- city: contact.city || body.city,
126
- state: contact.state || body.state,
127
- country: contact.country || body.country,
128
- };
129
- }
130
-
131
- // POST /api/meta/capi/lead
132
- router.post('/lead', async (req, res) => {
133
- try {
134
- const userData = extractContact(req.body);
135
- const result = await sendEvent('Lead', {
136
- sourceUrl: req.body.source_url || 'https://www.conversionmarketingpros.com',
137
- actionSource: 'website',
138
- }, userData);
139
- console.log(`[meta-capi] Lead webhook processed | email=${userData.email || 'none'}`);
140
- res.json({ ok: true, eventId: result.eventId });
141
- } catch (err) {
142
- console.error('[meta-capi] Lead error:', err.message);
143
- res.status(500).json({ error: err.message });
144
- }
145
- });
146
-
147
- // POST /api/meta/capi/schedule
148
- router.post('/schedule', async (req, res) => {
149
- try {
150
- const userData = extractContact(req.body);
151
- const result = await sendEvent('Schedule', {
152
- sourceUrl: req.body.source_url || 'https://www.conversionmarketingpros.com',
153
- actionSource: 'website',
154
- }, userData);
155
- console.log(`[meta-capi] Schedule webhook processed | email=${userData.email || 'none'}`);
156
- res.json({ ok: true, eventId: result.eventId });
157
- } catch (err) {
158
- console.error('[meta-capi] Schedule error:', err.message);
159
- res.status(500).json({ error: err.message });
160
- }
161
- });
162
-
163
- // POST /api/meta/capi/purchase
164
- router.post('/purchase', async (req, res) => {
165
- try {
166
- const userData = extractContact(req.body);
167
- const result = await sendEvent('Purchase', {
168
- sourceUrl: req.body.source_url || 'https://www.conversionmarketingpros.com',
169
- actionSource: 'system_generated',
170
- customData: {
171
- currency: 'USD',
172
- value: req.body.value || req.body.monetary_value || 0,
173
- },
174
- }, userData);
175
- console.log(`[meta-capi] Purchase webhook processed | email=${userData.email || 'none'} | value=${req.body.value || 0}`);
176
- res.json({ ok: true, eventId: result.eventId });
177
- } catch (err) {
178
- console.error('[meta-capi] Purchase error:', err.message);
179
- res.status(500).json({ error: err.message });
180
- }
181
- });
182
-
183
- // GET /api/meta/capi/test
184
- router.get('/test', async (req, res) => {
185
- try {
186
- const result = await sendEvent('Lead', {
187
- sourceUrl: 'https://www.conversionmarketingpros.com/test',
188
- actionSource: 'website',
189
- testEventCode: req.query.test_code || undefined,
190
- }, {
191
- email: 'test@example.com',
192
- firstName: 'Test',
193
- lastName: 'User',
194
- phone: '+15551234567',
195
- });
196
- res.json({ ok: true, eventId: result.eventId, pixelId: PIXEL_ID, response: result.response });
197
- } catch (err) {
198
- console.error('[meta-capi] Test error:', err.message);
199
- res.status(500).json({ error: err.message });
200
- }
201
- });
202
-
203
- return router;
204
- }
205
-
206
- module.exports = { sendEvent, createRouter, sha256, PIXEL_ID: PIXEL_ID };
package/meta-leads.js DELETED
@@ -1,411 +0,0 @@
1
- /**
2
- * Meta Lead Form Integration Module
3
- * Handles real-time webhook + polling for Meta lead forms,
4
- * processes leads through GHL (upsert, pipeline, SMS, email),
5
- * and fires CAPI events back to Meta.
6
- */
7
- const https = require('https');
8
- const { sendEvent } = require('./meta-capi');
9
-
10
- // --- Config (env vars with fallbacks) ---
11
- const GHL_TOKEN = process.env.GHL_PIT_TOKEN || 'pit-49079755-6cc5-44b2-b4d5-5195e662ed96';
12
- const GHL_LOCATION_ID = process.env.GHL_LOCATION_ID || 'aZ66KgncoFQZ6kHEJXzl';
13
- const GHL_PIPELINE_ID = process.env.GHL_PIPELINE_ID || 'LFU6adJ5XgO6VzH7wU02';
14
- const GHL_STAGE_FORM_SUBMITTED = process.env.GHL_STAGE_FORM_SUBMITTED || 'e3eb0f2f-3604-4610-a4fe-58519349a7d7';
15
-
16
- const META_ACCESS_TOKEN = process.env.META_ACCESS_TOKEN;
17
- const META_API_VERSION = 'v21.0';
18
- const META_VERIFY_TOKEN = 'cmp_lead_verify_2026';
19
-
20
- const AIVA_FORM_ID = '1502539338103793';
21
- const SOFTWARE_FORM_ID = '1855096478341715';
22
-
23
- const BOOKING_LINK = 'https://api.leadconnectorhq.com/widget/booking/brandons-personal-calendar-1';
24
-
25
- // --- Form config map ---
26
- const FORM_CONFIG = {
27
- [AIVA_FORM_ID]: {
28
- offer: 'aiva',
29
- tags: ['meta-lead', 'aiva-offer', 'facebook-ads'],
30
- sms: (firstName) =>
31
- `Hey ${firstName}! This is Brandon from Conversion Pros. Thanks for your interest in AIVA - your AI personal assistant. I'd love to show you a quick demo. What time works best for a 15-min call this week?`,
32
- emailSubject: 'Your AI Assistant Demo - Next Steps',
33
- emailBody: (firstName) =>
34
- `Hey ${firstName},\n\nThanks so much for your interest in AIVA. I'm really excited to show you what your own AI personal assistant can do for your business.\n\nI'd love to hop on a quick 15-minute call to walk you through a live demo and answer any questions you have.\n\nYou can grab a time that works for you here:\n${BOOKING_LINK}\n\nLooking forward to connecting!\n\nBrandon Burgan\nConversion Marketing Pros`,
35
- },
36
- [SOFTWARE_FORM_ID]: {
37
- offer: 'software',
38
- tags: ['meta-lead', 'software-offer', 'facebook-ads'],
39
- sms: (firstName) =>
40
- `Hey ${firstName}! This is Brandon from Conversion Pros. Thanks for requesting your free business audit. I'd love to learn more about your business and show you what's possible. What time works best for a quick call this week?`,
41
- emailSubject: 'Your Free Business Audit - Next Steps',
42
- emailBody: (firstName) =>
43
- `Hey ${firstName},\n\nThanks for requesting your free business audit. I'm looking forward to learning more about your business and showing you some quick wins we can get you.\n\nLet's set up a quick call so I can ask a few questions and put together a custom plan for you.\n\nYou can book a time here:\n${BOOKING_LINK}\n\nTalk soon!\n\nBrandon Burgan\nConversion Marketing Pros`,
44
- },
45
- };
46
-
47
- // --- GHL API helpers ---
48
-
49
- function ghlRequest(method, path, body) {
50
- return new Promise((resolve, reject) => {
51
- const data = body ? JSON.stringify(body) : null;
52
- const req = https.request(
53
- {
54
- hostname: 'services.leadconnectorhq.com',
55
- path,
56
- method,
57
- headers: {
58
- Authorization: `Bearer ${GHL_TOKEN}`,
59
- Version: '2021-07-28',
60
- 'Content-Type': 'application/json',
61
- ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
62
- },
63
- },
64
- (res) => {
65
- let raw = '';
66
- res.on('data', (chunk) => (raw += chunk));
67
- res.on('end', () => {
68
- try {
69
- const parsed = JSON.parse(raw);
70
- if (res.statusCode >= 400) {
71
- console.error(`[meta-leads] GHL ${method} ${path} ${res.statusCode}:`, raw);
72
- return reject(new Error(`GHL ${res.statusCode}: ${raw}`));
73
- }
74
- resolve(parsed);
75
- } catch (e) {
76
- reject(new Error(`GHL parse error: ${raw}`));
77
- }
78
- });
79
- }
80
- );
81
- req.on('error', reject);
82
- if (data) req.write(data);
83
- req.end();
84
- });
85
- }
86
-
87
- async function upsertContact({ firstName, lastName, email, phone, tags, source }) {
88
- const body = {
89
- firstName,
90
- lastName,
91
- locationId: GHL_LOCATION_ID,
92
- source: source || 'Facebook Ads',
93
- };
94
- if (email) body.email = email;
95
- if (phone) body.phone = phone;
96
- if (tags && tags.length) body.tags = tags;
97
-
98
- const result = await ghlRequest('POST', '/contacts/upsert', body);
99
- const contactId = result.contact?.id || result.id;
100
- console.log(`[meta-leads] Contact upserted: ${contactId} (${firstName} ${lastName})`);
101
- return { contactId, contact: result.contact || result };
102
- }
103
-
104
- async function createOpportunity({ contactId, firstName, lastName, offerLabel }) {
105
- const body = {
106
- pipelineId: GHL_PIPELINE_ID,
107
- locationId: GHL_LOCATION_ID,
108
- stageId: GHL_STAGE_FORM_SUBMITTED,
109
- contactId,
110
- name: `${offerLabel} Lead - ${firstName} ${lastName}`,
111
- status: 'open',
112
- };
113
- const result = await ghlRequest('POST', '/opportunities/upsert', body);
114
- const oppId = result.opportunity?.id || result.id;
115
- console.log(`[meta-leads] Opportunity created: ${oppId}`);
116
- return result;
117
- }
118
-
119
- async function sendSMS(contactId, message) {
120
- const result = await ghlRequest('POST', '/conversations/messages', {
121
- type: 'SMS',
122
- contactId,
123
- message,
124
- });
125
- console.log(`[meta-leads] SMS sent to contactId=${contactId}`);
126
- return result;
127
- }
128
-
129
- async function sendEmail(contactId, subject, body) {
130
- const result = await ghlRequest('POST', '/conversations/messages', {
131
- type: 'Email',
132
- contactId,
133
- subject,
134
- message: body,
135
- emailFrom: 'brandon@conversionmarketingpros.com',
136
- });
137
- console.log(`[meta-leads] Email sent to contactId=${contactId}`);
138
- return result;
139
- }
140
-
141
- // --- Lead parsing ---
142
-
143
- function parseMetaLeadData(leadData) {
144
- // Meta sends field_data as array of { name, values }
145
- const fields = {};
146
- if (Array.isArray(leadData.field_data)) {
147
- for (const f of leadData.field_data) {
148
- fields[f.name] = f.values?.[0] || '';
149
- }
150
- }
151
-
152
- // Split full_name if present
153
- let firstName = fields.first_name || '';
154
- let lastName = fields.last_name || '';
155
- if (!firstName && fields.full_name) {
156
- const parts = fields.full_name.trim().split(/\s+/);
157
- firstName = parts[0] || '';
158
- lastName = parts.slice(1).join(' ') || '';
159
- }
160
-
161
- return {
162
- firstName,
163
- lastName,
164
- email: fields.email || '',
165
- phone: fields.phone_number || fields.phone || '',
166
- leadId: leadData.id,
167
- createdTime: leadData.created_time,
168
- rawFields: fields,
169
- };
170
- }
171
-
172
- // --- Main processing pipeline ---
173
-
174
- async function processLead(leadData, formId) {
175
- const config = FORM_CONFIG[formId];
176
- if (!config) {
177
- console.warn(`[meta-leads] Unknown form ID: ${formId}, using AIVA defaults`);
178
- }
179
- const cfg = config || FORM_CONFIG[AIVA_FORM_ID];
180
- const offerLabel = cfg.offer === 'aiva' ? 'AIVA' : 'Software';
181
-
182
- const parsed = parseMetaLeadData(leadData);
183
- const { firstName, lastName, email, phone } = parsed;
184
-
185
- console.log(`[meta-leads] Processing ${offerLabel} lead: ${firstName} ${lastName} | email=${email} | phone=${phone}`);
186
-
187
- const results = { steps: [] };
188
-
189
- // 1. Upsert contact in GHL
190
- try {
191
- const { contactId } = await upsertContact({
192
- firstName,
193
- lastName,
194
- email,
195
- phone,
196
- tags: cfg.tags,
197
- source: 'Facebook Ads',
198
- });
199
- results.contactId = contactId;
200
- results.steps.push('contact_upserted');
201
-
202
- // 2. Create opportunity
203
- try {
204
- await createOpportunity({ contactId, firstName, lastName, offerLabel });
205
- results.steps.push('opportunity_created');
206
- } catch (err) {
207
- console.error(`[meta-leads] Opportunity creation failed:`, err.message);
208
- results.steps.push('opportunity_failed');
209
- }
210
-
211
- // 3. Send SMS (only if phone present)
212
- if (phone) {
213
- try {
214
- await sendSMS(contactId, cfg.sms(firstName || 'there'));
215
- results.steps.push('sms_sent');
216
- } catch (err) {
217
- console.error(`[meta-leads] SMS failed:`, err.message);
218
- results.steps.push('sms_failed');
219
- }
220
- } else {
221
- results.steps.push('sms_skipped_no_phone');
222
- }
223
-
224
- // 4. Send email (only if email present)
225
- if (email) {
226
- try {
227
- await sendEmail(contactId, cfg.emailSubject, cfg.emailBody(firstName || 'there'));
228
- results.steps.push('email_sent');
229
- } catch (err) {
230
- console.error(`[meta-leads] Email failed:`, err.message);
231
- results.steps.push('email_failed');
232
- }
233
- } else {
234
- results.steps.push('email_skipped_no_email');
235
- }
236
-
237
- // 5. Fire CAPI Lead event back to Meta
238
- try {
239
- const capiResult = await sendEvent('Lead', {
240
- sourceUrl: 'https://www.conversionmarketingpros.com',
241
- actionSource: 'system_generated',
242
- }, {
243
- email,
244
- phone,
245
- firstName,
246
- lastName,
247
- });
248
- results.capiEventId = capiResult.eventId;
249
- results.steps.push('capi_fired');
250
- } catch (err) {
251
- console.error(`[meta-leads] CAPI event failed:`, err.message);
252
- results.steps.push('capi_failed');
253
- }
254
- } catch (err) {
255
- console.error(`[meta-leads] Contact upsert failed:`, err.message);
256
- results.steps.push('contact_upsert_failed');
257
- results.error = err.message;
258
- }
259
-
260
- console.log(`[meta-leads] Pipeline complete for ${firstName} ${lastName}: ${results.steps.join(', ')}`);
261
- return results;
262
- }
263
-
264
- // --- Meta Lead Retrieval (polling) ---
265
-
266
- function metaGraphRequest(path) {
267
- return new Promise((resolve, reject) => {
268
- https
269
- .get(`https://graph.facebook.com/${META_API_VERSION}${path}&access_token=${encodeURIComponent(META_ACCESS_TOKEN)}`, (res) => {
270
- let data = '';
271
- res.on('data', (chunk) => (data += chunk));
272
- res.on('end', () => {
273
- try {
274
- const parsed = JSON.parse(data);
275
- if (parsed.error) {
276
- console.error(`[meta-leads] Graph API error:`, parsed.error);
277
- return reject(new Error(parsed.error.message));
278
- }
279
- resolve(parsed);
280
- } catch (e) {
281
- reject(new Error(`Meta Graph parse error: ${data}`));
282
- }
283
- });
284
- })
285
- .on('error', reject);
286
- });
287
- }
288
-
289
- async function pollFormLeads(formId) {
290
- console.log(`[meta-leads] Polling form ${formId} for leads...`);
291
- const result = await metaGraphRequest(`/${formId}/leads?`);
292
- return result.data || [];
293
- }
294
-
295
- // --- Express Router ---
296
-
297
- function createRouter(express) {
298
- const router = express.Router();
299
-
300
- // GET - Meta webhook verification
301
- router.get('/webhook', (req, res) => {
302
- const mode = req.query['hub.mode'];
303
- const token = req.query['hub.verify_token'];
304
- const challenge = req.query['hub.challenge'];
305
-
306
- if (mode === 'subscribe' && token === META_VERIFY_TOKEN) {
307
- console.log('[meta-leads] Webhook verified');
308
- return res.status(200).send(challenge);
309
- }
310
- console.warn('[meta-leads] Webhook verification failed');
311
- return res.status(403).send('Forbidden');
312
- });
313
-
314
- // POST - Meta sends lead data here
315
- router.post('/webhook', async (req, res) => {
316
- // Respond immediately to Meta (they require fast 200)
317
- res.status(200).json({ status: 'received' });
318
-
319
- try {
320
- const body = req.body;
321
- console.log('[meta-leads] Webhook received:', JSON.stringify(body).slice(0, 500));
322
-
323
- // Meta sends: { object: "page", entry: [{ id, time, changes: [{ field: "leadgen", value: { ... } }] }] }
324
- if (body.object === 'page' && Array.isArray(body.entry)) {
325
- for (const entry of body.entry) {
326
- const changes = entry.changes || [];
327
- for (const change of changes) {
328
- if (change.field === 'leadgen') {
329
- const leadgenData = change.value;
330
- const formId = String(leadgenData.form_id || '');
331
- const leadgenId = leadgenData.leadgen_id;
332
-
333
- // Fetch full lead data from Meta
334
- try {
335
- const leadDetail = await metaGraphRequest(`/${leadgenId}?`);
336
- await processLead(leadDetail, formId);
337
- } catch (err) {
338
- console.error(`[meta-leads] Failed to fetch/process lead ${leadgenId}:`, err.message);
339
- }
340
- }
341
- }
342
- }
343
- }
344
- } catch (err) {
345
- console.error('[meta-leads] Webhook processing error:', err.message);
346
- }
347
- });
348
-
349
- // GET - Poll both forms for recent leads
350
- router.get('/poll', async (req, res) => {
351
- try {
352
- if (!META_ACCESS_TOKEN) {
353
- return res.status(500).json({ error: 'META_ACCESS_TOKEN not configured' });
354
- }
355
-
356
- const [aivaLeads, softwareLeads] = await Promise.all([
357
- pollFormLeads(AIVA_FORM_ID).catch((e) => { console.error('[meta-leads] AIVA poll error:', e.message); return []; }),
358
- pollFormLeads(SOFTWARE_FORM_ID).catch((e) => { console.error('[meta-leads] Software poll error:', e.message); return []; }),
359
- ]);
360
-
361
- // Process toggle - only process if ?process=true (safety)
362
- const shouldProcess = req.query.process === 'true';
363
- const results = { aiva: { count: aivaLeads.length, leads: aivaLeads }, software: { count: softwareLeads.length, leads: softwareLeads }, processed: [] };
364
-
365
- if (shouldProcess) {
366
- for (const lead of aivaLeads) {
367
- try {
368
- const r = await processLead(lead, AIVA_FORM_ID);
369
- results.processed.push({ leadId: lead.id, formId: AIVA_FORM_ID, ...r });
370
- } catch (err) {
371
- results.processed.push({ leadId: lead.id, formId: AIVA_FORM_ID, error: err.message });
372
- }
373
- }
374
- for (const lead of softwareLeads) {
375
- try {
376
- const r = await processLead(lead, SOFTWARE_FORM_ID);
377
- results.processed.push({ leadId: lead.id, formId: SOFTWARE_FORM_ID, ...r });
378
- } catch (err) {
379
- results.processed.push({ leadId: lead.id, formId: SOFTWARE_FORM_ID, error: err.message });
380
- }
381
- }
382
- }
383
-
384
- res.json(results);
385
- } catch (err) {
386
- console.error('[meta-leads] Poll error:', err.message);
387
- res.status(500).json({ error: err.message });
388
- }
389
- });
390
-
391
- // GET - Health/status check
392
- router.get('/status', (req, res) => {
393
- res.json({
394
- ok: true,
395
- forms: {
396
- aiva: AIVA_FORM_ID,
397
- software: SOFTWARE_FORM_ID,
398
- },
399
- pipeline: {
400
- id: GHL_PIPELINE_ID,
401
- stageId: GHL_STAGE_FORM_SUBMITTED,
402
- },
403
- verifyToken: META_VERIFY_TOKEN,
404
- metaTokenConfigured: !!META_ACCESS_TOKEN,
405
- });
406
- });
407
-
408
- return router;
409
- }
410
-
411
- module.exports = { createRouter, processLead, parseMetaLeadData, upsertContact, createOpportunity };