import { Controller, Post, Get, Body, Query, Logger, HttpException } from '@nestjs/common'; import { WidgetService } from './widget.service'; import { createHmac } from 'crypto'; @Controller('api/webhook') export class WebhooksController { private readonly logger = new Logger(WebhooksController.name); private readonly googleWebhookKey: string; private readonly fbVerifyToken: string; constructor(private readonly widget: WidgetService) { this.googleWebhookKey = process.env.GOOGLE_WEBHOOK_KEY ?? ''; this.fbVerifyToken = process.env.FB_VERIFY_TOKEN ?? 'helix-engage-verify'; } // ─── Facebook / Instagram Lead Ads ─── // Webhook verification (Meta sends GET to verify endpoint) @Get('facebook') verifyFacebook( @Query('hub.mode') mode: string, @Query('hub.verify_token') token: string, @Query('hub.challenge') challenge: string, ) { if (mode === 'subscribe' && token === this.fbVerifyToken) { this.logger.log('[FB] Webhook verified'); return challenge; } throw new HttpException('Verification failed', 403); } // Receive leads from Facebook/Instagram @Post('facebook') async facebookLead(@Body() body: any) { this.logger.log(`[FB] Webhook received: ${JSON.stringify(body).substring(0, 200)}`); if (body.object !== 'page') { return { status: 'ignored', reason: 'not a page event' }; } let leadsCreated = 0; for (const entry of body.entry ?? []) { for (const change of entry.changes ?? []) { if (change.field !== 'leadgen') continue; const leadData = change.value; const leadgenId = leadData.leadgen_id; const formId = leadData.form_id; const pageId = leadData.page_id; this.logger.log(`[FB] Lead received: leadgen_id=${leadgenId} form_id=${formId} page_id=${pageId}`); // Fetch full lead data from Meta Graph API const lead = await this.fetchFacebookLead(leadgenId); if (!lead) { this.logger.warn(`[FB] Could not fetch lead data for ${leadgenId}`); continue; } const name = this.extractFbField(lead, 'full_name') ?? 'Facebook Lead'; const phone = this.extractFbField(lead, 'phone_number') ?? ''; const email = this.extractFbField(lead, 'email') ?? ''; try { await this.widget.createLead({ name, phone: phone.replace(/[^0-9+]/g, ''), interest: `Facebook Ad (form: ${formId})`, message: email ? `Email: ${email}` : undefined, captchaToken: 'webhook-bypass', }); leadsCreated++; this.logger.log(`[FB] Lead created: ${name} (${phone})`); } catch (err: any) { this.logger.error(`[FB] Lead creation failed: ${err.message}`); } } } return { status: 'ok', leadsCreated }; } private async fetchFacebookLead(leadgenId: string): Promise { const accessToken = process.env.FB_PAGE_ACCESS_TOKEN; if (!accessToken) { this.logger.warn('[FB] FB_PAGE_ACCESS_TOKEN not set — cannot fetch lead details'); return null; } try { const res = await fetch(`https://graph.facebook.com/v21.0/${leadgenId}?access_token=${accessToken}`); if (!res.ok) return null; return res.json(); } catch { return null; } } private extractFbField(lead: any, fieldName: string): string | null { const fields = lead.field_data ?? []; const field = fields.find((f: any) => f.name === fieldName); return field?.values?.[0] ?? null; } // ─── Google Ads Lead Form ─── @Post('google') async googleLead(@Body() body: any) { this.logger.log(`[GOOGLE] Webhook received: ${JSON.stringify(body).substring(0, 200)}`); // Verify webhook key if configured if (this.googleWebhookKey && body.google_key) { const expected = createHmac('sha256', this.googleWebhookKey) .update(body.lead_id ?? '') .digest('hex'); // Google sends the key directly, not as HMAC — just compare if (body.google_key !== this.googleWebhookKey) { this.logger.warn('[GOOGLE] Invalid webhook key'); throw new HttpException('Invalid webhook key', 403); } } const isTest = body.is_test === true; const leadId = body.lead_id; const campaignId = body.campaign_id; const formId = body.form_id; // Extract user data from column data const userData = body.user_column_data ?? []; const name = this.extractGoogleField(userData, 'FULL_NAME') ?? this.extractGoogleField(userData, 'FIRST_NAME') ?? 'Google Ad Lead'; const phone = this.extractGoogleField(userData, 'PHONE_NUMBER') ?? ''; const email = this.extractGoogleField(userData, 'EMAIL') ?? ''; const city = this.extractGoogleField(userData, 'CITY') ?? ''; this.logger.log(`[GOOGLE] Lead: ${name} | ${phone} | campaign=${campaignId} | test=${isTest}`); try { const result = await this.widget.createLead({ name, phone: phone.replace(/[^0-9+]/g, ''), interest: `Google Ad${isTest ? ' (TEST)' : ''} (campaign: ${campaignId ?? 'unknown'})`, message: [email, city].filter(Boolean).join(', ') || undefined, captchaToken: 'webhook-bypass', }); this.logger.log(`[GOOGLE] Lead created: ${result.leadId}${isTest ? ' (test)' : ''}`); return { status: 'ok', leadId: result.leadId, isTest }; } catch (err: any) { this.logger.error(`[GOOGLE] Lead creation failed: ${err.message}`); throw new HttpException('Lead creation failed', 500); } } private extractGoogleField(columnData: any[], fieldName: string): string | null { const field = columnData.find((f: any) => f.column_id === fieldName); return field?.string_value ?? null; } // ─── Ozonetel WhatsApp Callback ─── // Configure in Ozonetel: Chat Callback URL → https://engage-api.srv1477139.hstgr.cloud/api/webhook/whatsapp // Payload format will be adapted once Ozonetel confirms their schema @Post('whatsapp') async whatsappLead(@Body() body: any) { this.logger.log(`[WHATSAPP] Webhook received: ${JSON.stringify(body).substring(0, 300)}`); const phone = body.from ?? body.caller_id ?? body.phone ?? body.customerNumber ?? ''; const name = body.name ?? body.customerName ?? ''; const message = body.message ?? body.text ?? body.body ?? ''; if (!phone) { this.logger.warn('[WHATSAPP] No phone number in payload'); return { status: 'ignored', reason: 'no phone number' }; } try { const result = await this.widget.createLead({ name: name || 'WhatsApp Lead', phone: phone.replace(/[^0-9+]/g, ''), interest: 'WhatsApp Enquiry', message: message || undefined, captchaToken: 'webhook-bypass', }); this.logger.log(`[WHATSAPP] Lead created: ${result.leadId} (${phone})`); return { status: 'ok', leadId: result.leadId }; } catch (err: any) { this.logger.error(`[WHATSAPP] Lead creation failed: ${err.message}`); return { status: 'error', message: err.message }; } } // ─── Ozonetel SMS Callback ─── // Configure in Ozonetel: SMS Callback URL → https://engage-api.srv1477139.hstgr.cloud/api/webhook/sms @Post('sms') async smsLead(@Body() body: any) { this.logger.log(`[SMS] Webhook received: ${JSON.stringify(body).substring(0, 300)}`); const phone = body.from ?? body.caller_id ?? body.phone ?? body.senderNumber ?? ''; const name = body.name ?? ''; const message = body.message ?? body.text ?? body.body ?? ''; if (!phone) { this.logger.warn('[SMS] No phone number in payload'); return { status: 'ignored', reason: 'no phone number' }; } try { const result = await this.widget.createLead({ name: name || 'SMS Lead', phone: phone.replace(/[^0-9+]/g, ''), interest: 'SMS Enquiry', message: message || undefined, captchaToken: 'webhook-bypass', }); this.logger.log(`[SMS] Lead created: ${result.leadId} (${phone})`); return { status: 'ok', leadId: result.leadId }; } catch (err: any) { this.logger.error(`[SMS] Lead creation failed: ${err.message}`); return { status: 'error', message: err.message }; } } }