Files
whyrating/packages/billing/src/providers/lemon-squeezy/webhook/index.ts
2026-02-04 01:55:00 +01:00

84 lines
2.3 KiB
TypeScript

import { HttpStatusCode } from "@turbostarter/shared/constants";
import { logger } from "@turbostarter/shared/logger";
import { HttpException } from "@turbostarter/shared/utils";
import { checkoutStatusChangeHandler } from "../checkout";
import { env } from "../env";
import { subscriptionStatusChangeHandler } from "../subscription";
import { LEMON_SQUEEZY_SIGNATURE_HEADER } from "./constants";
import { validateSignature } from "./signing";
import { webhookHasData, webhookHasMeta } from "./type-guards";
import type { BillingProviderStrategy } from "../../types";
export const webhookHandler: BillingProviderStrategy["webhookHandler"] = async (
req,
callbacks,
) => {
const body = await req.text();
const sig = req.headers.get(LEMON_SQUEEZY_SIGNATURE_HEADER);
if (!sig) {
throw new HttpException(HttpStatusCode.BAD_REQUEST, {
code: "billing:error.webhook.signatureNotFound",
});
}
await validateSignature(sig, env.LEMON_SQUEEZY_SIGNING_SECRET, body);
const data = JSON.parse(body);
if (!webhookHasMeta(data)) {
throw new HttpException(HttpStatusCode.BAD_REQUEST, {
code: "billing:error.webhook.metaInvalid",
});
}
const type = data.meta.event_name;
logger.info(`🔔 Webhook received: ${type}`);
if (!webhookHasData(data)) {
throw new HttpException(HttpStatusCode.BAD_REQUEST, {
code: "billing:error.webhook.dataInvalid",
});
}
await callbacks?.onEvent?.(data);
switch (type) {
case "subscription_created":
await callbacks?.onSubscriptionCreated?.(data.data.id);
await subscriptionStatusChangeHandler({
id: data.data.id,
});
break;
case "subscription_updated":
await callbacks?.onSubscriptionUpdated?.(data.data.id);
await subscriptionStatusChangeHandler({
id: data.data.id,
});
break;
case "subscription_expired":
await callbacks?.onSubscriptionDeleted?.(data.data.id);
await subscriptionStatusChangeHandler({
id: data.data.id,
});
break;
case "order_created":
await callbacks?.onCheckoutSessionCompleted?.(data.data.id);
await checkoutStatusChangeHandler({
id: data.data.id,
});
break;
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};