119 lines
3.0 KiB
TypeScript
119 lines
3.0 KiB
TypeScript
import type { BlitzApiHandler } from "blitz";
|
|
import { NotFoundError } from "blitz";
|
|
import { z } from "zod";
|
|
|
|
import type { ApiError } from "../../core/types";
|
|
import db, { MembershipRole } from "db";
|
|
import appLogger from "../../../integrations/logger";
|
|
import { sendEmail } from "../../../integrations/ses";
|
|
|
|
const logger = appLogger.child({ module: "subscription-updated" });
|
|
|
|
export const subscriptionUpdated: BlitzApiHandler = async (req, res) => {
|
|
const validationResult = bodySchema.safeParse(req.body);
|
|
if (!validationResult.success) {
|
|
const statusCode = 400;
|
|
const apiError: ApiError = {
|
|
statusCode,
|
|
errorMessage: "Body is malformed",
|
|
};
|
|
logger.error(validationResult.error, "/api/subscription/webhook");
|
|
|
|
res.status(statusCode).send(apiError);
|
|
return;
|
|
}
|
|
|
|
const body = validationResult.data;
|
|
const paddleSubscriptionId = body.subscription_id;
|
|
const subscription = await db.subscription.findFirst({
|
|
where: { paddleSubscriptionId },
|
|
include: {
|
|
organization: {
|
|
include: {
|
|
memberships: {
|
|
include: { user: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!subscription) {
|
|
throw new NotFoundError();
|
|
}
|
|
|
|
const lastEventTime = new Date(body.event_time);
|
|
const isEventOlderThanLastUpdate = subscription.lastEventTime > lastEventTime;
|
|
if (isEventOlderThanLastUpdate) {
|
|
res.status(200).end();
|
|
return;
|
|
}
|
|
|
|
const orgOwner = subscription.organization!.memberships.find(
|
|
(membership) => membership.role === MembershipRole.OWNER,
|
|
);
|
|
const email = orgOwner!.user!.email;
|
|
const paddleCheckoutId = body.checkout_id;
|
|
const planId = body.subscription_plan_id;
|
|
const nextBillDate = new Date(body.next_bill_date);
|
|
const status = body.status;
|
|
const updateUrl = body.update_url;
|
|
const cancelUrl = body.cancel_url;
|
|
const currency = body.currency;
|
|
const unitPrice = body.new_unit_price;
|
|
|
|
await db.subscription.update({
|
|
where: { paddleSubscriptionId },
|
|
data: {
|
|
paddleSubscriptionId,
|
|
paddlePlanId: planId,
|
|
paddleCheckoutId,
|
|
nextBillDate,
|
|
status,
|
|
lastEventTime,
|
|
updateUrl,
|
|
cancelUrl,
|
|
currency,
|
|
unitPrice,
|
|
},
|
|
});
|
|
|
|
sendEmail({
|
|
subject: "Thanks for your purchase",
|
|
body: "Thanks for your purchase",
|
|
recipients: [email],
|
|
}).catch((error) => {
|
|
logger.error(error, "/api/subscription/webhook");
|
|
});
|
|
|
|
return res.status(200).end();
|
|
};
|
|
|
|
const bodySchema = z.object({
|
|
alert_id: z.string(),
|
|
alert_name: z.string(),
|
|
cancel_url: z.string(),
|
|
checkout_id: z.string(),
|
|
currency: z.string(),
|
|
email: z.string(),
|
|
event_time: z.string(),
|
|
linked_subscriptions: z.string(),
|
|
marketing_consent: z.string(),
|
|
new_price: z.string(),
|
|
new_quantity: z.string(),
|
|
new_unit_price: z.string(),
|
|
next_bill_date: z.string(),
|
|
old_next_bill_date: z.string(),
|
|
old_price: z.string(),
|
|
old_quantity: z.string(),
|
|
old_status: z.string(),
|
|
old_subscription_plan_id: z.string(),
|
|
old_unit_price: z.string(),
|
|
passthrough: z.string(),
|
|
status: z.enum(["active", "trialing", "past_due", "paused", "deleted"]),
|
|
subscription_id: z.string(),
|
|
subscription_plan_id: z.string(),
|
|
update_url: z.string(),
|
|
user_id: z.string(),
|
|
p_signature: z.string(),
|
|
});
|