101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
|
import type { BlitzApiHandler } from "blitz";
|
||
|
import { NotFoundError } from "blitz";
|
||
|
import { z } from "zod";
|
||
|
|
||
|
import type { ApiError } from "../../core/types";
|
||
|
import db from "db";
|
||
|
import appLogger from "../../../integrations/logger";
|
||
|
|
||
|
const logger = appLogger.child({ module: "subscription-payment-succeeded" });
|
||
|
|
||
|
export const subscriptionPaymentSucceededHandler: 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 } });
|
||
|
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 paddleCheckoutId = body.checkout_id;
|
||
|
const planId = body.subscription_plan_id;
|
||
|
const nextBillDate = new Date(body.next_bill_date);
|
||
|
const status = body.status;
|
||
|
const currency = body.currency;
|
||
|
const unitPrice = body.unit_price;
|
||
|
|
||
|
await db.subscription.update({
|
||
|
where: { paddleSubscriptionId },
|
||
|
data: {
|
||
|
paddleSubscriptionId,
|
||
|
paddlePlanId: planId,
|
||
|
paddleCheckoutId,
|
||
|
nextBillDate,
|
||
|
status,
|
||
|
lastEventTime,
|
||
|
currency,
|
||
|
unitPrice,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
return res.status(200).end();
|
||
|
};
|
||
|
|
||
|
const bodySchema = z.object({
|
||
|
alert_id: z.string(),
|
||
|
alert_name: z.string(),
|
||
|
balance_currency: z.string(),
|
||
|
balance_earnings: z.string(),
|
||
|
balance_fee: z.string(),
|
||
|
balance_gross: z.string(),
|
||
|
balance_tax: z.string(),
|
||
|
checkout_id: z.string(),
|
||
|
country: z.string(),
|
||
|
coupon: z.string(),
|
||
|
currency: z.string(),
|
||
|
customer_name: z.string(),
|
||
|
earnings: z.string(),
|
||
|
email: z.string(),
|
||
|
event_time: z.string(),
|
||
|
fee: z.string(),
|
||
|
initial_payment: z.string(),
|
||
|
instalments: z.string(),
|
||
|
marketing_consent: z.string(),
|
||
|
next_bill_date: z.string(),
|
||
|
next_payment_amount: z.string(),
|
||
|
order_id: z.string(),
|
||
|
passthrough: z.string(),
|
||
|
payment_method: z.string(),
|
||
|
payment_tax: z.string(),
|
||
|
plan_name: z.string(),
|
||
|
quantity: z.string(),
|
||
|
receipt_url: z.string(),
|
||
|
sale_gross: z.string(),
|
||
|
status: z.enum(["active", "trialing", "past_due", "paused", "deleted"]),
|
||
|
subscription_id: z.string(),
|
||
|
subscription_payment_id: z.string(),
|
||
|
subscription_plan_id: z.string(),
|
||
|
unit_price: z.string(),
|
||
|
user_id: z.string(),
|
||
|
p_signature: z.string(),
|
||
|
});
|