integrate backend with paddle
This commit is contained in:
79
app/settings/webhook-handlers/subscription-cancelled.ts
Normal file
79
app/settings/webhook-handlers/subscription-cancelled.ts
Normal file
@ -0,0 +1,79 @@
|
||||
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-cancelled" });
|
||||
|
||||
export const subscriptionCancelled: 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 status = body.status;
|
||||
const currency = body.currency;
|
||||
const unitPrice = body.unit_price;
|
||||
|
||||
await db.subscription.update({
|
||||
where: { paddleSubscriptionId },
|
||||
data: {
|
||||
paddleSubscriptionId,
|
||||
paddlePlanId: planId,
|
||||
paddleCheckoutId,
|
||||
status,
|
||||
lastEventTime,
|
||||
currency,
|
||||
unitPrice,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).end();
|
||||
};
|
||||
|
||||
const bodySchema = z.object({
|
||||
alert_id: z.string(),
|
||||
alert_name: z.string(),
|
||||
cancellation_effective_date: z.string(),
|
||||
checkout_id: z.string(),
|
||||
currency: z.string(),
|
||||
email: z.string(),
|
||||
event_time: z.string(),
|
||||
linked_subscriptions: z.string(),
|
||||
marketing_consent: z.string(),
|
||||
passthrough: z.string(),
|
||||
quantity: z.string(),
|
||||
status: z.enum(["active", "trialing", "past_due", "paused", "deleted"]),
|
||||
subscription_id: z.string(),
|
||||
subscription_plan_id: z.string(),
|
||||
unit_price: z.string(),
|
||||
user_id: z.string(),
|
||||
p_signature: z.string(),
|
||||
});
|
132
app/settings/webhook-handlers/subscription-created.ts
Normal file
132
app/settings/webhook-handlers/subscription-created.ts
Normal file
@ -0,0 +1,132 @@
|
||||
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-created" });
|
||||
|
||||
export const subscriptionCreatedHandler: 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 { organizationId } = JSON.parse(body.passthrough);
|
||||
const organization = await db.organization.findFirst({
|
||||
where: { id: organizationId },
|
||||
include: {
|
||||
subscription: true,
|
||||
memberships: {
|
||||
include: { user: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!organization) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const orgOwner = organization.memberships.find((membership) => membership.role === MembershipRole.OWNER);
|
||||
const email = orgOwner!.user!.email;
|
||||
const paddleCheckoutId = body.checkout_id;
|
||||
const paddleSubscriptionId = body.subscription_id;
|
||||
const planId = body.subscription_plan_id;
|
||||
const nextBillDate = new Date(body.next_bill_date);
|
||||
const status = body.status;
|
||||
const lastEventTime = new Date(body.event_time);
|
||||
const updateUrl = body.update_url;
|
||||
const cancelUrl = body.cancel_url;
|
||||
const currency = body.currency;
|
||||
const unitPrice = body.unit_price;
|
||||
|
||||
if (!!organization.subscription) {
|
||||
await db.subscription.update({
|
||||
where: { paddleSubscriptionId: organization.subscription.paddleSubscriptionId },
|
||||
data: {
|
||||
paddleSubscriptionId,
|
||||
paddlePlanId: planId,
|
||||
paddleCheckoutId,
|
||||
nextBillDate,
|
||||
status,
|
||||
lastEventTime,
|
||||
updateUrl,
|
||||
cancelUrl,
|
||||
currency,
|
||||
unitPrice,
|
||||
},
|
||||
});
|
||||
|
||||
sendEmail({
|
||||
subject: "Welcome back to Shellphone",
|
||||
body: "Welcome back to Shellphone",
|
||||
recipients: [email],
|
||||
}).catch((error) => {
|
||||
logger.error(error, "/api/subscription/webhook");
|
||||
});
|
||||
} else {
|
||||
await db.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: {
|
||||
subscription: {
|
||||
create: {
|
||||
paddleSubscriptionId,
|
||||
paddlePlanId: planId,
|
||||
paddleCheckoutId,
|
||||
nextBillDate,
|
||||
status,
|
||||
lastEventTime,
|
||||
updateUrl,
|
||||
cancelUrl,
|
||||
currency,
|
||||
unitPrice,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sendEmail({
|
||||
subject: "Welcome to Shellphone",
|
||||
body: `Welcome to Shellphone`,
|
||||
recipients: [email],
|
||||
}).catch((error) => {
|
||||
logger.error(error, "/api/webhook/subscription");
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
next_bill_date: z.string(),
|
||||
passthrough: z.string(),
|
||||
quantity: z.string(),
|
||||
source: z.string(),
|
||||
status: z.enum(["active", "trialing", "past_due", "paused", "deleted"]),
|
||||
subscription_id: z.string(),
|
||||
subscription_plan_id: z.string(),
|
||||
unit_price: z.string(),
|
||||
update_url: z.string(),
|
||||
user_id: z.string(),
|
||||
p_signature: z.string(),
|
||||
});
|
100
app/settings/webhook-handlers/subscription-payment-succeeded.ts
Normal file
100
app/settings/webhook-handlers/subscription-payment-succeeded.ts
Normal file
@ -0,0 +1,100 @@
|
||||
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(),
|
||||
});
|
118
app/settings/webhook-handlers/subscription-updated.ts
Normal file
118
app/settings/webhook-handlers/subscription-updated.ts
Normal file
@ -0,0 +1,118 @@
|
||||
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(),
|
||||
});
|
Reference in New Issue
Block a user