84 lines
2.3 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
};
|