@mahe_pkm/buzl-capi 0.1.2

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.
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Automated Verification & Self-Testing Engine
3
+ * Validates HTML tags, form hooks, script existence, and endpoint health
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const https = require('https');
8
+ const http = require('http');
9
+
10
+ /**
11
+ * Ping an HTTP/HTTPS URL
12
+ */
13
+ function pingUrl(urlStr) {
14
+ return new Promise((resolve) => {
15
+ try {
16
+ const parsed = new URL(urlStr);
17
+ const client = parsed.protocol === 'https:' ? https : http;
18
+
19
+ const req = client.get(urlStr, { timeout: 4000 }, (res) => {
20
+ // If Google Apps Script redirects to Google Accounts login, access is restricted
21
+ if (res.headers && res.headers.location && res.headers.location.includes('accounts.google.com')) {
22
+ resolve({ ok: false, statusCode: 401, error: 'Authentication required. In Google Apps Script, set "Who has access: Anyone".' });
23
+ return;
24
+ }
25
+ // Google Apps Script redirects 302 to script.googleusercontent.com
26
+ if (res.statusCode >= 200 && res.statusCode < 400) {
27
+ resolve({ ok: true, statusCode: res.statusCode });
28
+ } else {
29
+ resolve({ ok: false, statusCode: res.statusCode });
30
+ }
31
+ });
32
+
33
+ req.on('timeout', () => {
34
+ req.destroy();
35
+ resolve({ ok: false, error: 'Request timed out after 4000ms' });
36
+ });
37
+
38
+ req.on('error', (err) => {
39
+ resolve({ ok: false, error: err.message });
40
+ });
41
+ } catch (e) {
42
+ resolve({ ok: false, error: 'Invalid URL format' });
43
+ }
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Run verification tests on modified files and configuration
49
+ */
50
+ async function runVerification(rootDir, htmlFiles, config) {
51
+ const testResults = [];
52
+
53
+ function record(name, passed, detail) {
54
+ testResults.push({ name, passed, detail: detail || '' });
55
+ }
56
+
57
+ // 1. Check runtime script existence
58
+ const runtimePath = path.join(rootDir, 'assets', 'js', 'buzl-tracking.js');
59
+ const runtimeExists = fs.existsSync(runtimePath);
60
+ record('Runtime script deployed (assets/js/buzl-tracking.js)', runtimeExists);
61
+
62
+ // 2. Check each HTML file
63
+ for (const filePath of htmlFiles) {
64
+ const relName = path.relative(rootDir, filePath);
65
+ const content = fs.readFileSync(filePath, 'utf8');
66
+
67
+ // Structural integrity
68
+ const hasHead = /<head[\s>]/i.test(content) && /<\/head>/i.test(content);
69
+ record(`[${relName}] Valid <head> block integrity`, hasHead);
70
+
71
+ const hasBody = /<body[\s>]/i.test(content) && /<\/body>/i.test(content);
72
+ record(`[${relName}] Valid <body> block integrity`, hasBody);
73
+
74
+ // GTM Verification
75
+ if (config.gtmId) {
76
+ const hasGtmHead = content.includes(config.gtmId);
77
+ const hasGtmBody = content.includes(`googletagmanager.com/ns.html?id=${config.gtmId}`);
78
+ record(`[${relName}] GTM Head snippet matches ${config.gtmId}`, hasGtmHead);
79
+ record(`[${relName}] GTM Body noscript fallback matches ${config.gtmId}`, hasGtmBody);
80
+
81
+ // Duplication check
82
+ const gtmCount = (content.match(new RegExp(config.gtmId, 'g')) || []).length;
83
+ record(`[${relName}] No duplicate GTM tags (head + noscript count <= 2)`, gtmCount <= 2);
84
+ }
85
+
86
+ // Meta Pixel Verification
87
+ if (config.metaPixelId) {
88
+ const hasMetaHead = content.includes(`fbq("init", "${config.metaPixelId}")`) || content.includes(config.metaPixelId);
89
+ const hasMetaNoscript = content.includes(`facebook.com/tr?id=${config.metaPixelId}`);
90
+ record(`[${relName}] Meta Pixel Head code configured with ${config.metaPixelId}`, hasMetaHead);
91
+ record(`[${relName}] Meta Pixel Noscript fallback present`, hasMetaNoscript);
92
+ }
93
+
94
+ // Runtime script tag link
95
+ const hasScriptLink = content.includes('buzl-tracking.js');
96
+ record(`[${relName}] Runtime tracker linked before </body>`, hasScriptLink);
97
+
98
+ // Form Hook Check
99
+ const hasForms = /<form[\s>]/i.test(content);
100
+ if (hasForms) {
101
+ const hasTrackAttr = /<form[^>]*data-buzl-track="true"/i.test(content);
102
+ record(`[${relName}] Forms tagged with data-buzl-track attribute`, hasTrackAttr);
103
+ }
104
+ }
105
+
106
+ // 3. Google Apps Script Webhook Ping
107
+ if (config.googleSheetUrl) {
108
+ const pingRes = await pingUrl(config.googleSheetUrl);
109
+ if (pingRes.ok) {
110
+ record(`Google Apps Script endpoint reachable (HTTP ${pingRes.statusCode})`, true);
111
+ } else {
112
+ let detail = pingRes.error || `HTTP ${pingRes.statusCode}`;
113
+ if (pingRes.statusCode === 404) {
114
+ const isPlaceholder = /AKfycbx_eY7V34n2Gz3h125JmO6r89Q2k|placeholder|example|AKfycbz_test/i.test(config.googleSheetUrl);
115
+ detail = isPlaceholder
116
+ ? 'HTTP 404 (Placeholder script ID detected. Deploy Buzl_GoogleAppsScript_Template.gs in your Google Sheet via Deploy > New deployment > Web app > Anyone, and paste your live /exec URL)'
117
+ : 'HTTP 404 (Script not found on Google. In your Google Sheet, click Deploy > Manage deployments and verify a Web app is deployed with "Who has access: Anyone")';
118
+ } else if (pingRes.statusCode === 401 || pingRes.statusCode === 403) {
119
+ detail = `HTTP ${pingRes.statusCode} (Access denied. In your Google Sheet, ensure Web app deployment has "Who has access: Anyone")`;
120
+ }
121
+ record(`Google Apps Script endpoint reachability`, false, detail);
122
+ }
123
+ }
124
+
125
+ const passedCount = testResults.filter(t => t.passed).length;
126
+ const failedCount = testResults.filter(t => !t.passed).length;
127
+
128
+ return {
129
+ total: testResults.length,
130
+ passedCount,
131
+ failedCount,
132
+ allPassed: failedCount === 0,
133
+ tests: testResults
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Dispatch a live synthetic test lead to all configured channels
139
+ */
140
+ async function testDispatch(arg1, arg2 = {}, arg3 = {}) {
141
+ let config = arg1;
142
+ let sampleLead = arg2;
143
+ let rootDir = '';
144
+
145
+ // Polymorphic support: if called as (rootDir, config, sampleLead)
146
+ if (typeof arg1 === 'string' && typeof arg2 === 'object') {
147
+ rootDir = arg1;
148
+ config = arg2;
149
+ sampleLead = arg3;
150
+ }
151
+ if (!config) config = {};
152
+ if (!sampleLead) sampleLead = {};
153
+
154
+ const leadId = sampleLead.leadId || ('test-lead-' + Date.now());
155
+ const name = sampleLead.name || 'Test Lead';
156
+ const phone = sampleLead.phone || config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '';
157
+ const location = sampleLead.location || config.siteLocation || '';
158
+ const service = sampleLead.service || 'General Inquiry';
159
+
160
+ const results = {
161
+ leadId,
162
+ timestamp: new Date().toISOString(),
163
+ channels: {}
164
+ };
165
+
166
+ // 1. Test Google Sheets Sync
167
+ const googleSheetUrl = config.googleSheetUrl || (config.sheets && config.sheets.url) || '';
168
+ if (googleSheetUrl) {
169
+ try {
170
+ const sheetPayload = {
171
+ timestamp: new Date().toISOString(),
172
+ leadId: leadId,
173
+ name: name,
174
+ location: location,
175
+ siteLocation: config.siteLocation || location,
176
+ phone: phone,
177
+ service: service,
178
+ email: sampleLead.email || config.notificationEmail || 'test-lead@example.com',
179
+ source: service,
180
+ utm: { source: 'live_test_button', medium: 'gui_test', campaign: 'buzl_verification' },
181
+ eventSourceUrl: sampleLead.eventSourceUrl || 'http://localhost:3333/test',
182
+ landingPageUrl: sampleLead.landingPageUrl || 'http://localhost:3333/',
183
+ pagePath: sampleLead.pagePath || '/test',
184
+ userAgent: 'Buzl-Test-Agent/1.0',
185
+ rawFields: Object.assign({ service: service, isTest: true }, sampleLead.rawFields || {})
186
+ };
187
+
188
+ const res = await fetch(googleSheetUrl, {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json' },
191
+ body: JSON.stringify(sheetPayload)
192
+ });
193
+
194
+ const bodyText = await res.text();
195
+ let parsed = null;
196
+ try { parsed = JSON.parse(bodyText); } catch (e) {}
197
+
198
+ let statusMsg = (parsed && parsed.message) ? `${parsed.message} (Row ${parsed.row || 'N/A'})` : `HTTP ${res.status}`;
199
+ if (res.status === 404) {
200
+ const isPlaceholder = /AKfycbx_eY7V34n2Gz3h125JmO6r89Q2k|placeholder|example|AKfycbz_test/i.test(googleSheetUrl);
201
+ statusMsg = isPlaceholder
202
+ ? 'Placeholder URL detected. Deploy Buzl_GoogleAppsScript_Template.gs and paste your real /exec URL.'
203
+ : 'HTTP 404: Script not found on Google. Verify deployment in Google Sheet with access: Anyone.';
204
+ } else if (res.status === 401 || res.status === 403) {
205
+ statusMsg = `HTTP ${res.status}: Access Denied. Ensure Web app deployment access is set to 'Anyone'.`;
206
+ }
207
+
208
+ results.channels.googleSheets = {
209
+ tested: true,
210
+ ok: res.ok || (parsed && parsed.status === 'success'),
211
+ status: res.status,
212
+ message: statusMsg
213
+ };
214
+ } catch (err) {
215
+ results.channels.googleSheets = {
216
+ tested: true,
217
+ ok: false,
218
+ status: 0,
219
+ message: err.message
220
+ };
221
+ }
222
+ } else {
223
+ results.channels.googleSheets = { tested: false, message: 'Google Sheets not configured or disabled' };
224
+ }
225
+
226
+ // 2. Test Buzl CAPI
227
+ const buzlCapi = config.buzlCapi || {};
228
+ const capiEndpoint = buzlCapi.endpoint || config.buzlCapiEndpoint || '';
229
+ const authUser = buzlCapi.authUser || config.buzlCapiUser || '';
230
+ const authPass = buzlCapi.authPass || config.buzlCapiPass || '';
231
+
232
+ if (capiEndpoint && authUser) {
233
+ try {
234
+ const resolvedDomain = sampleLead.domain ||
235
+ config.domain ||
236
+ (config.siteUrl ? new URL(config.siteUrl).hostname : '') ||
237
+ (rootDir ? path.basename(rootDir).toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') : '') ||
238
+ 'landing-page';
239
+
240
+ const capiPayload = {
241
+ leadId: leadId,
242
+ domain: resolvedDomain,
243
+ eventName: 'Lead',
244
+ eventTime: Math.floor(Date.now() / 1000),
245
+ actionSource: 'website',
246
+ eventSourceUrl: sampleLead.eventSourceUrl || 'http://localhost:3333/test',
247
+ landingPageUrl: sampleLead.landingPageUrl || 'http://localhost:3333/',
248
+ contact: { name: name, phone: phone, location: location },
249
+ source: service,
250
+ utm: { source: 'live_test_button', medium: 'gui_test' }
251
+ };
252
+
253
+ const basicAuth = Buffer.from(`${authUser}:${authPass}`).toString('base64');
254
+ const res = await fetch(capiEndpoint, {
255
+ method: 'POST',
256
+ headers: {
257
+ 'Content-Type': 'application/json',
258
+ 'Authorization': `Basic ${basicAuth}`
259
+ },
260
+ body: JSON.stringify(capiPayload)
261
+ });
262
+
263
+ const text = await res.text();
264
+ results.channels.buzlCapi = {
265
+ tested: true,
266
+ ok: res.ok,
267
+ status: res.status,
268
+ message: res.ok ? `Lead accepted by Buzl CAPI (HTTP ${res.status})` : `Failed: HTTP ${res.status} - ${text.slice(0, 100)}`
269
+ };
270
+ } catch (err) {
271
+ results.channels.buzlCapi = {
272
+ tested: true,
273
+ ok: false,
274
+ status: 0,
275
+ message: err.message
276
+ };
277
+ }
278
+ } else {
279
+ results.channels.buzlCapi = { tested: false, message: 'Buzl CAPI not configured or disabled' };
280
+ }
281
+
282
+ // 3. Test Zoho CRM
283
+ const zoho = config.zoho || {};
284
+ const zohoEndpoint = zoho.endpoint || config.zohoEndpoint || '';
285
+ const zohoXnqsjsdp = zoho.xnQsjsdp || config.zohoXnqsjsdp || '';
286
+
287
+ if (zohoEndpoint && zohoXnqsjsdp) {
288
+ try {
289
+ const params = new URLSearchParams();
290
+ params.append('xnQsjsdp', zohoXnqsjsdp);
291
+ if (zoho.xmIwtLD || config.zohoXmiwtld) params.append('xmIwtLD', zoho.xmIwtLD || config.zohoXmiwtld);
292
+ params.append('actionType', 'TGVhZHM=');
293
+ params.append('Last Name', name);
294
+ params.append('Phone', phone);
295
+ params.append('City', location);
296
+ params.append('Lead Source', service);
297
+
298
+ const res = await fetch(zohoEndpoint, {
299
+ method: 'POST',
300
+ body: params
301
+ });
302
+
303
+ results.channels.zoho = {
304
+ tested: true,
305
+ ok: res.ok,
306
+ status: res.status,
307
+ message: `Dispatched to Zoho (HTTP ${res.status})`
308
+ };
309
+ } catch (err) {
310
+ results.channels.zoho = {
311
+ tested: true,
312
+ ok: false,
313
+ status: 0,
314
+ message: err.message
315
+ };
316
+ }
317
+ } else {
318
+ results.channels.zoho = { tested: false, message: 'Zoho CRM not configured or disabled' };
319
+ }
320
+
321
+ return results;
322
+ }
323
+
324
+ /**
325
+ * Test a specific individual form submission
326
+ */
327
+ async function testIndividualForm(rootDir, formId, formFields = {}, config = {}, extraOpts = {}) {
328
+ const leadId = 'test-' + (formId || 'form').replace(/[^a-zA-Z0-9_-]/g, '') + '-' + Date.now();
329
+
330
+ const name = formFields.name || formFields.fullName || 'Test Lead';
331
+ const phone = formFields.phone || formFields.mobile || config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '';
332
+ const location = formFields.location || formFields.city || config.siteLocation || '';
333
+ const service = formFields.service || formFields.subject || formFields.inquiry || formId || 'General Inquiry';
334
+ const pagePath = extraOpts.pagePath || formFields.pagePath || '/';
335
+
336
+ const sampleLead = {
337
+ leadId,
338
+ name,
339
+ phone,
340
+ location,
341
+ service,
342
+ source: formId || 'Individual Form Test',
343
+ eventSourceUrl: `http://localhost:3333/${pagePath.replace(/^\//, '')}`,
344
+ landingPageUrl: 'http://localhost:3333/',
345
+ pagePath: pagePath,
346
+ rawFields: Object.assign({}, formFields, { formId, pagePath, isTest: true })
347
+ };
348
+
349
+ return await testDispatch(rootDir, config, sampleLead);
350
+ }
351
+
352
+ module.exports = {
353
+ runVerification,
354
+ pingUrl,
355
+ testDispatch,
356
+ testIndividualForm
357
+ };