import { Request, Response } from 'express';
import crypto from 'crypto';
import {
createCustomer,
getCustomerByEmail,
getCustomerByHubSpotId,
updateCustomerStatus,
logWebhook,
markWebhookProcessed,
} from '../db/supabase.js';
// Verify HubSpot webhook signature
function verifyHubSpotSignature(
signature: string,
body: string,
secret: string
): boolean {
const hash = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return signature === hash;
}
// Handle HubSpot payment webhooks
export async function handleHubSpotWebhook(
req: Request,
res: Response
): Promise<void> {
const webhookSecret = process.env.HUBSPOT_WEBHOOK_SECRET;
// Log the webhook for debugging
const logEntry = {
source: 'hubspot',
event_type: req.body.subscriptionType || req.body.eventType || null,
payload: req.body,
processed: false,
error: null,
};
const { data: logData } = await logWebhook(logEntry) as any;
const logId = logData?.[0]?.id;
try {
// Verify signature if secret is configured
if (webhookSecret) {
const signature = req.headers['x-hubspot-signature'] as string;
const rawBody = JSON.stringify(req.body);
if (!signature || !verifyHubSpotSignature(signature, rawBody, webhookSecret)) {
res.status(401).json({ error: 'Invalid signature' });
if (logId) {
await markWebhookProcessed(logId, 'Invalid signature');
}
return;
}
}
// Parse the webhook event
// HubSpot sends different formats - we need to handle both payments and deal events
const eventType = req.body.subscriptionType || req.body.eventType;
const objectId = req.body.objectId;
const properties = req.body.properties || req.body;
// Extract customer info
const email = properties.email || properties.hs_email;
const contactId = properties.hs_object_id || objectId;
if (!email) {
console.warn('No email found in webhook payload');
res.status(400).json({ error: 'Missing email in payload' });
if (logId) {
await markWebhookProcessed(logId, 'Missing email');
}
return;
}
// Handle different event types
switch (eventType) {
case 'deal.creation':
case 'deal.propertyChange':
case 'payment.succeeded':
case 'subscription.created':
// New subscription or successful payment - create or reactivate customer
let customer = await getCustomerByEmail(email);
if (!customer) {
customer = await createCustomer(email, contactId?.toString());
console.log(`Created new customer: ${email}`);
} else if (customer.subscription_status !== 'active') {
await updateCustomerStatus(customer.id, 'active');
console.log(`Reactivated customer: ${email}`);
}
res.status(200).json({
success: true,
message: 'Customer activated',
access_token: customer?.access_token,
});
break;
case 'deal.deletion':
case 'payment.failed':
case 'subscription.cancelled':
case 'subscription.deleted':
// Subscription cancelled or payment failed - deactivate
customer = await getCustomerByEmail(email);
if (customer) {
const newStatus = eventType.includes('payment') ? 'past_due' : 'cancelled';
await updateCustomerStatus(customer.id, newStatus);
console.log(`Updated customer ${email} to ${newStatus}`);
}
res.status(200).json({
success: true,
message: 'Customer status updated',
});
break;
default:
console.log(`Unhandled event type: ${eventType}`);
res.status(200).json({
success: true,
message: 'Event received but not processed',
});
}
if (logId) {
await markWebhookProcessed(logId);
}
} catch (error: any) {
console.error('Error processing HubSpot webhook:', error);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
if (logId) {
await markWebhookProcessed(logId, error.message);
}
}
}