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(),
 | |
| });
 | 
