feat: listen LemonSqueezy webhooks

This commit is contained in:
Ahmed Bouhuolia
2024-04-14 11:55:36 +02:00
parent 693ae61141
commit a9748b23c0
22 changed files with 106 additions and 1015 deletions

View File

@@ -0,0 +1,109 @@
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
import { ServiceError } from '@/exceptions';
import { Inject, Service } from 'typedi';
import {
compareSignatures,
configureLemonSqueezy,
createHmacSignature,
webhookHasData,
webhookHasMeta,
} from './utils';
import { Plan } from '@/system/models';
import { Subscription } from './Subscription';
@Service()
export class LemonSqueezyWebhooks {
@Inject()
private subscriptionService: Subscription;
/**
* handle the LemonSqueezy webhooks.
* @param {string} rawBody
* @param {string} signature
* @returns {Promise<void>}
*/
public async handlePostWebhook(
rawData: any,
data: Record<string, any>,
signature: string
): Promise<void> {
configureLemonSqueezy();
if (!process.env.LEMONSQUEEZY_WEBHOOK_SECRET) {
throw new ServiceError('Lemon Squeezy Webhook Secret not set in .env');
}
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
const hmacSignature = createHmacSignature(secret, rawData);
if (!compareSignatures(hmacSignature, signature)) {
throw new Error('Invalid signature');
}
// Type guard to check if the object has a 'meta' property.
if (webhookHasMeta(data)) {
// Non-blocking call to process the webhook event.
void this.processWebhookEvent(data);
}
throw new Error('Data invalid');
}
/**
* This action will process a webhook event in the database.
*/
async processWebhookEvent(eventBody) {
let processingError = '';
const webhookEvent = eventBody.meta.event_name;
const userId = eventBody.meta.custom_data?.user_id;
const tenantId = eventBody.meta.custom_data?.tenant_id;
if (!webhookHasMeta(eventBody)) {
processingError = "Event body is missing the 'meta' property.";
} else if (webhookHasData(eventBody)) {
if (webhookEvent.startsWith('subscription_payment_')) {
// Save subscription invoices; eventBody is a SubscriptionInvoice
// Not implemented.
} else if (webhookEvent.startsWith('subscription_')) {
// Save subscription events; obj is a Subscription
const attributes = eventBody.data.attributes;
const variantId = attributes.variant_id as string;
// We assume that the Plan table is up to date.
const plan = await Plan.query().findOne('slug', 'essentials-yearly');
if (!plan) {
processingError = `Plan with variantId ${variantId} not found.`;
} else {
// Update the subscription in the database.
const priceId = attributes.first_subscription_item.price_id;
// Get the price data from Lemon Squeezy.
const priceData = await getPrice(priceId);
if (priceData.error) {
processingError = `Failed to get the price data for the subscription ${eventBody.data.id}.`;
}
const isUsageBased =
attributes.first_subscription_item.is_usage_based;
const price = isUsageBased
? priceData.data?.data.attributes.unit_price_decimal
: priceData.data?.data.attributes.unit_price;
if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion(
tenantId,
'pro-yearly',
'year',
1
);
}
}
} else if (webhookEvent.startsWith('order_')) {
// Save orders; eventBody is a "Order"
/* Not implemented */
} else if (webhookEvent.startsWith('license_')) {
// Save license keys; eventBody is a "License key"
/* Not implemented */
}
}
}
}