* return 200 asap to paddle and queue webhook received
* paddle ids to int
This commit is contained in:
parent
188c028667
commit
771fea4d7b
42
app/settings/api/queue/subscription-cancelled.ts
Normal file
42
app/settings/api/queue/subscription-cancelled.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { NotFoundError } from "blitz";
|
||||||
|
import { Queue } from "quirrel/blitz";
|
||||||
|
import type { PaddleSdkSubscriptionCancelledEvent } from "@devoxa/paddle-sdk";
|
||||||
|
|
||||||
|
import db from "db";
|
||||||
|
import appLogger from "integrations/logger";
|
||||||
|
import { translateSubscriptionStatus } from "integrations/paddle";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ queue: "subscription-cancelled" });
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
event: PaddleSdkSubscriptionCancelledEvent<{ organizationId: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionCancelledQueue = Queue<Payload>("api/queue/subscription-cancelled", async ({ event }) => {
|
||||||
|
const paddleSubscriptionId = event.subscriptionId;
|
||||||
|
const subscription = await db.subscription.findFirst({ where: { paddleSubscriptionId } });
|
||||||
|
if (!subscription) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEventTime = event.eventTime;
|
||||||
|
const isEventOlderThanLastUpdate = subscription.lastEventTime > lastEventTime;
|
||||||
|
if (isEventOlderThanLastUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.subscription.update({
|
||||||
|
where: { paddleSubscriptionId },
|
||||||
|
data: {
|
||||||
|
paddleSubscriptionId,
|
||||||
|
paddlePlanId: event.productId,
|
||||||
|
paddleCheckoutId: event.checkoutId,
|
||||||
|
status: translateSubscriptionStatus(event.status),
|
||||||
|
lastEventTime,
|
||||||
|
currency: event.currency,
|
||||||
|
unitPrice: event.unitPrice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default subscriptionCancelledQueue;
|
99
app/settings/api/queue/subscription-created.ts
Normal file
99
app/settings/api/queue/subscription-created.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { NotFoundError } from "blitz";
|
||||||
|
import { Queue } from "quirrel/blitz";
|
||||||
|
import type { PaddleSdkSubscriptionCreatedEvent } from "@devoxa/paddle-sdk";
|
||||||
|
|
||||||
|
import db, { MembershipRole } from "db";
|
||||||
|
import appLogger from "integrations/logger";
|
||||||
|
import { sendEmail } from "integrations/ses";
|
||||||
|
import { translateSubscriptionStatus } from "integrations/paddle";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ queue: "subscription-created" });
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
event: PaddleSdkSubscriptionCreatedEvent<{ organizationId: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-created", async ({ event }) => {
|
||||||
|
const { organizationId } = event.metadata;
|
||||||
|
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 = event.checkoutId;
|
||||||
|
const paddleSubscriptionId = event.subscriptionId;
|
||||||
|
const planId = event.productId;
|
||||||
|
const nextBillDate = event.nextPaymentDate;
|
||||||
|
const status = translateSubscriptionStatus(event.status);
|
||||||
|
const lastEventTime = event.eventTime;
|
||||||
|
const updateUrl = event.updateUrl;
|
||||||
|
const cancelUrl = event.cancelUrl;
|
||||||
|
const currency = event.currency;
|
||||||
|
const unitPrice = event.unitPrice;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
} 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default subscriptionCreatedQueue;
|
47
app/settings/api/queue/subscription-payment-succeeded.ts
Normal file
47
app/settings/api/queue/subscription-payment-succeeded.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { NotFoundError } from "blitz";
|
||||||
|
import { Queue } from "quirrel/blitz";
|
||||||
|
import { PaddleSdkSubscriptionPaymentSucceededEvent } from "@devoxa/paddle-sdk";
|
||||||
|
|
||||||
|
import db from "db";
|
||||||
|
import appLogger from "integrations/logger";
|
||||||
|
import type { Metadata } from "integrations/paddle";
|
||||||
|
import { translateSubscriptionStatus } from "integrations/paddle";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ queue: "subscription-payment-succeeded" });
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
event: PaddleSdkSubscriptionPaymentSucceededEvent<Metadata>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionPaymentSucceededQueue = Queue<Payload>(
|
||||||
|
"api/queue/subscription-payment-succeeded",
|
||||||
|
async ({ event }) => {
|
||||||
|
const paddleSubscriptionId = event.subscriptionId;
|
||||||
|
const subscription = await db.subscription.findFirst({ where: { paddleSubscriptionId } });
|
||||||
|
if (!subscription) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEventTime = event.eventTime;
|
||||||
|
const isEventOlderThanLastUpdate = subscription.lastEventTime > lastEventTime;
|
||||||
|
if (isEventOlderThanLastUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.subscription.update({
|
||||||
|
where: { paddleSubscriptionId },
|
||||||
|
data: {
|
||||||
|
paddleSubscriptionId,
|
||||||
|
paddlePlanId: event.productId,
|
||||||
|
paddleCheckoutId: event.checkoutId,
|
||||||
|
nextBillDate: event.nextPaymentDate,
|
||||||
|
status: translateSubscriptionStatus(event.status),
|
||||||
|
lastEventTime,
|
||||||
|
currency: event.currency,
|
||||||
|
unitPrice: event.unitPrice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default subscriptionPaymentSucceededQueue;
|
71
app/settings/api/queue/subscription-updated.ts
Normal file
71
app/settings/api/queue/subscription-updated.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { NotFoundError } from "blitz";
|
||||||
|
import { Queue } from "quirrel/blitz";
|
||||||
|
import { PaddleSdkSubscriptionUpdatedEvent } from "@devoxa/paddle-sdk";
|
||||||
|
|
||||||
|
import db, { MembershipRole } from "db";
|
||||||
|
import appLogger from "integrations/logger";
|
||||||
|
import { sendEmail } from "integrations/ses";
|
||||||
|
import type { Metadata } from "integrations/paddle";
|
||||||
|
import { translateSubscriptionStatus } from "integrations/paddle";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ module: "subscription-updated" });
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
event: PaddleSdkSubscriptionUpdatedEvent<Metadata>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionUpdatedQueue = Queue<Payload>("api/queue/subscription-updated", async ({ event }) => {
|
||||||
|
const paddleSubscriptionId = event.subscriptionId;
|
||||||
|
const subscription = await db.subscription.findFirst({
|
||||||
|
where: { paddleSubscriptionId },
|
||||||
|
include: {
|
||||||
|
organization: {
|
||||||
|
include: {
|
||||||
|
memberships: {
|
||||||
|
include: { user: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!subscription) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEventTime = event.eventTime;
|
||||||
|
const isEventOlderThanLastUpdate = subscription.lastEventTime > lastEventTime;
|
||||||
|
if (isEventOlderThanLastUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgOwner = subscription.organization!.memberships.find(
|
||||||
|
(membership) => membership.role === MembershipRole.OWNER,
|
||||||
|
);
|
||||||
|
const email = orgOwner!.user!.email;
|
||||||
|
const planId = event.productId;
|
||||||
|
await db.subscription.update({
|
||||||
|
where: { paddleSubscriptionId },
|
||||||
|
data: {
|
||||||
|
paddleSubscriptionId,
|
||||||
|
paddlePlanId: planId,
|
||||||
|
paddleCheckoutId: event.checkoutId,
|
||||||
|
nextBillDate: event.nextPaymentDate,
|
||||||
|
status: translateSubscriptionStatus(event.status),
|
||||||
|
lastEventTime,
|
||||||
|
updateUrl: event.updateUrl,
|
||||||
|
cancelUrl: event.cancelUrl,
|
||||||
|
currency: event.currency,
|
||||||
|
unitPrice: event.unitPrice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sendEmail({
|
||||||
|
subject: "Thanks for your purchase",
|
||||||
|
body: "Thanks for your purchase",
|
||||||
|
recipients: [email],
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default subscriptionUpdatedQueue;
|
@ -1,47 +1,39 @@
|
|||||||
import type { BlitzApiHandler, BlitzApiRequest, BlitzApiResponse } from "blitz";
|
import type { BlitzApiHandler } from "blitz";
|
||||||
import { getConfig } from "blitz";
|
import type { Queue } from "quirrel/blitz";
|
||||||
import { PaddleSdk, stringifyMetadata } from "@devoxa/paddle-sdk";
|
import type {
|
||||||
|
PaddleSdkSubscriptionCancelledEvent,
|
||||||
|
PaddleSdkSubscriptionCreatedEvent,
|
||||||
|
PaddleSdkSubscriptionPaymentSucceededEvent,
|
||||||
|
PaddleSdkSubscriptionUpdatedEvent,
|
||||||
|
} from "@devoxa/paddle-sdk";
|
||||||
|
import { PaddleSdkWebhookEventType } from "@devoxa/paddle-sdk";
|
||||||
|
|
||||||
import type { ApiError } from "../../../core/types";
|
import type { ApiError } from "app/core/types";
|
||||||
import { subscriptionCreatedHandler } from "../../webhook-handlers/subscription-created";
|
import subscriptionCreatedQueue from "../queue/subscription-created";
|
||||||
import { subscriptionPaymentSucceededHandler } from "../../webhook-handlers/subscription-payment-succeeded";
|
import subscriptionPaymentSucceededQueue from "../queue/subscription-payment-succeeded";
|
||||||
import { subscriptionCancelled } from "../../webhook-handlers/subscription-cancelled";
|
import subscriptionCancelledQueue from "../queue/subscription-cancelled";
|
||||||
import { subscriptionUpdated } from "../../webhook-handlers/subscription-updated";
|
import subscriptionUpdatedQueue from "../queue/subscription-updated";
|
||||||
import appLogger from "../../../../integrations/logger";
|
import appLogger from "integrations/logger";
|
||||||
|
import { paddleSdk } from "integrations/paddle";
|
||||||
|
|
||||||
type SupportedWebhook =
|
type Events<TMetadata = { organizationId: string }> =
|
||||||
| "subscription_created"
|
| PaddleSdkSubscriptionCreatedEvent<TMetadata>
|
||||||
| "subscription_cancelled"
|
| PaddleSdkSubscriptionUpdatedEvent<TMetadata>
|
||||||
| "subscription_payment_succeeded"
|
| PaddleSdkSubscriptionCancelledEvent<TMetadata>
|
||||||
| "subscription_updated";
|
| PaddleSdkSubscriptionPaymentSucceededEvent<TMetadata>;
|
||||||
const supportedWebhooks: SupportedWebhook[] = [
|
|
||||||
"subscription_created",
|
|
||||||
"subscription_cancelled",
|
|
||||||
"subscription_payment_succeeded",
|
|
||||||
"subscription_updated",
|
|
||||||
];
|
|
||||||
|
|
||||||
const handlers: Record<SupportedWebhook, BlitzApiHandler> = {
|
type SupportedEventType = Events["eventType"];
|
||||||
subscription_created: subscriptionCreatedHandler,
|
|
||||||
subscription_payment_succeeded: subscriptionPaymentSucceededHandler,
|
const queues: Record<SupportedEventType, Queue<{ event: Events }>> = {
|
||||||
subscription_cancelled: subscriptionCancelled,
|
[PaddleSdkWebhookEventType.SUBSCRIPTION_CREATED]: subscriptionCreatedQueue,
|
||||||
subscription_updated: subscriptionUpdated,
|
[PaddleSdkWebhookEventType.SUBSCRIPTION_PAYMENT_SUCCEEDED]: subscriptionPaymentSucceededQueue,
|
||||||
|
[PaddleSdkWebhookEventType.SUBSCRIPTION_CANCELLED]: subscriptionCancelledQueue,
|
||||||
|
[PaddleSdkWebhookEventType.SUBSCRIPTION_UPDATED]: subscriptionUpdatedQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
function isSupportedWebhook(webhook: any): webhook is SupportedWebhook {
|
|
||||||
return supportedWebhooks.includes(webhook);
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = appLogger.child({ route: "/api/subscription/webhook" });
|
const logger = appLogger.child({ route: "/api/subscription/webhook" });
|
||||||
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
|
|
||||||
const paddleSdk = new PaddleSdk({
|
|
||||||
publicKey: serverRuntimeConfig.paddle.publicKey,
|
|
||||||
vendorId: publicRuntimeConfig.paddle.vendorId,
|
|
||||||
vendorAuthCode: serverRuntimeConfig.paddle.apiKey,
|
|
||||||
metadataCodec: stringifyMetadata(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default async function webhook(req: BlitzApiRequest, res: BlitzApiResponse) {
|
const webhook: BlitzApiHandler = async (req, res) => {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
const statusCode = 405;
|
const statusCode = 405;
|
||||||
const apiError: ApiError = {
|
const apiError: ApiError = {
|
||||||
@ -55,23 +47,21 @@ export default async function webhook(req: BlitzApiRequest, res: BlitzApiRespons
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!paddleSdk.verifyWebhookEvent(req.body)) {
|
const event = paddleSdk.parseWebhookEvent(req.body);
|
||||||
const statusCode = 500;
|
const alertName = event.eventType;
|
||||||
const apiError: ApiError = {
|
|
||||||
statusCode,
|
|
||||||
errorMessage: "Webhook event is invalid",
|
|
||||||
};
|
|
||||||
logger.error(apiError);
|
|
||||||
|
|
||||||
return res.status(statusCode).send(apiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertName = req.body.alert_name;
|
|
||||||
logger.info(`Received ${alertName} webhook`);
|
logger.info(`Received ${alertName} webhook`);
|
||||||
logger.info(req.body);
|
logger.info(event);
|
||||||
if (isSupportedWebhook(alertName)) {
|
if (isSupportedWebhook(alertName)) {
|
||||||
return handlers[alertName](req, res);
|
await queues[alertName].enqueue({ event: event as Events }, { id: event.eventId.toString() });
|
||||||
|
|
||||||
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).end();
|
return res.status(400).end();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default webhook;
|
||||||
|
|
||||||
|
function isSupportedWebhook(eventType: PaddleSdkWebhookEventType): eventType is SupportedEventType {
|
||||||
|
return Object.keys(queues).includes(eventType);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export default function Plans() {
|
|||||||
<div className="mt-6 flex flex-row-reverse flex-wrap-reverse gap-x-4">
|
<div className="mt-6 flex flex-row-reverse flex-wrap-reverse gap-x-4">
|
||||||
{pricing.tiers.map((tier) => {
|
{pricing.tiers.map((tier) => {
|
||||||
const isCurrentTier =
|
const isCurrentTier =
|
||||||
!subscription?.paddlePlanId && tier.planId === "free"
|
!subscription?.paddlePlanId && tier.planId === -1
|
||||||
? true
|
? true
|
||||||
: subscription?.paddlePlanId === tier.planId;
|
: subscription?.paddlePlanId === tier.planId;
|
||||||
const cta = isCurrentTier ? "Current plan" : !!subscription ? `Switch to ${tier.title}` : "Subscribe";
|
const cta = isCurrentTier ? "Current plan" : !!subscription ? `Switch to ${tier.title}` : "Subscribe";
|
||||||
@ -94,7 +94,7 @@ const pricing = {
|
|||||||
tiers: [
|
tiers: [
|
||||||
{
|
{
|
||||||
title: "Free",
|
title: "Free",
|
||||||
planId: "free",
|
planId: -1,
|
||||||
price: 0,
|
price: 0,
|
||||||
frequency: "",
|
frequency: "",
|
||||||
description: "The essentials to let you try Shellphone.",
|
description: "The essentials to let you try Shellphone.",
|
||||||
@ -105,7 +105,7 @@ const pricing = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Monthly",
|
title: "Monthly",
|
||||||
planId: "727540",
|
planId: 727540,
|
||||||
price: 15,
|
price: 15,
|
||||||
frequency: "/month",
|
frequency: "/month",
|
||||||
description: "Text and call anyone, anywhere in the world.",
|
description: "Text and call anyone, anywhere in the world.",
|
||||||
@ -116,7 +116,7 @@ const pricing = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Yearly",
|
title: "Yearly",
|
||||||
planId: "727544",
|
planId: 727544,
|
||||||
price: 12.5,
|
price: 12.5,
|
||||||
frequency: "/month",
|
frequency: "/month",
|
||||||
description: "Text and call anyone, anywhere in the world, all year long.",
|
description: "Text and call anyone, anywhere in the world, all year long.",
|
||||||
|
@ -37,7 +37,7 @@ export default function useSubscription({ initialData }: Params = {}) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
type BuyParams = {
|
type BuyParams = {
|
||||||
planId: string;
|
planId: number;
|
||||||
coupon?: string;
|
coupon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export default function useSubscription({ initialData }: Params = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChangePlanParams = {
|
type ChangePlanParams = {
|
||||||
planId: string;
|
planId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function changePlan({ planId }: ChangePlanParams) {
|
async function changePlan({ planId }: ChangePlanParams) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { NotFoundError, resolver } from "blitz";
|
import { NotFoundError, resolver } from "blitz";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { updateSubscriptionPlan } from "../../../integrations/paddle";
|
import db from "db";
|
||||||
import db from "../../../db";
|
import { updateSubscriptionPlan } from "integrations/paddle";
|
||||||
|
|
||||||
const Body = z.object({
|
const Body = z.object({
|
||||||
planId: z.string(),
|
planId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ planId }, ctx) => {
|
export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ planId }, ctx) => {
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
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(),
|
|
||||||
});
|
|
@ -1,132 +0,0 @@
|
|||||||
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(),
|
|
||||||
});
|
|
@ -1,100 +0,0 @@
|
|||||||
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(),
|
|
||||||
});
|
|
@ -1,118 +0,0 @@
|
|||||||
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(),
|
|
||||||
});
|
|
22
db/migrations/20211001210138_paddle_ids_to_int/migration.sql
Normal file
22
db/migrations/20211001210138_paddle_ids_to_int/migration.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `Subscription` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- A unique constraint covering the columns `[paddleSubscriptionId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Changed the type of `paddleSubscriptionId` on the `Subscription` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
- Changed the type of `paddlePlanId` on the `Subscription` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
- Changed the type of `unitPrice` on the `Subscription` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_pkey",
|
||||||
|
DROP COLUMN "paddleSubscriptionId",
|
||||||
|
ADD COLUMN "paddleSubscriptionId" INTEGER NOT NULL,
|
||||||
|
DROP COLUMN "paddlePlanId",
|
||||||
|
ADD COLUMN "paddlePlanId" INTEGER NOT NULL,
|
||||||
|
DROP COLUMN "unitPrice",
|
||||||
|
ADD COLUMN "unitPrice" INTEGER NOT NULL,
|
||||||
|
ADD CONSTRAINT "Subscription_pkey" PRIMARY KEY ("paddleSubscriptionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Subscription_paddleSubscriptionId_key" ON "Subscription"("paddleSubscriptionId");
|
@ -39,14 +39,14 @@ model Subscription {
|
|||||||
createdAt DateTime @default(now()) @db.Timestamptz
|
createdAt DateTime @default(now()) @db.Timestamptz
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz
|
updatedAt DateTime @updatedAt @db.Timestamptz
|
||||||
|
|
||||||
paddleSubscriptionId String @id @unique
|
paddleSubscriptionId Int @id @unique
|
||||||
paddlePlanId String
|
paddlePlanId Int
|
||||||
paddleCheckoutId String
|
paddleCheckoutId String
|
||||||
status SubscriptionStatus
|
status SubscriptionStatus
|
||||||
updateUrl String
|
updateUrl String
|
||||||
cancelUrl String
|
cancelUrl String
|
||||||
currency String
|
currency String
|
||||||
unitPrice String
|
unitPrice Int
|
||||||
nextBillDate DateTime @db.Date
|
nextBillDate DateTime @db.Date
|
||||||
lastEventTime DateTime @db.Timestamp
|
lastEventTime DateTime @db.Timestamp
|
||||||
|
|
||||||
|
@ -1,11 +1,43 @@
|
|||||||
import { getConfig } from "blitz";
|
import { getConfig } from "blitz";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
|
import type { PaddleSdkSubscriptionCreatedEvent } from "@devoxa/paddle-sdk";
|
||||||
|
import { PaddleSdk, PaddleSdkSubscriptionStatus, stringifyMetadata } from "@devoxa/paddle-sdk";
|
||||||
|
|
||||||
|
import { SubscriptionStatus } from "db";
|
||||||
|
|
||||||
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const vendor_id = publicRuntimeConfig.paddle.vendorId;
|
const vendor_id = publicRuntimeConfig.paddle.vendorId;
|
||||||
const vendor_auth_code = serverRuntimeConfig.paddle.apiKey;
|
const vendor_auth_code = serverRuntimeConfig.paddle.apiKey;
|
||||||
|
|
||||||
|
export const paddleSdk = new PaddleSdk({
|
||||||
|
publicKey: serverRuntimeConfig.paddle.publicKey,
|
||||||
|
vendorId: vendor_id,
|
||||||
|
vendorAuthCode: vendor_auth_code,
|
||||||
|
metadataCodec: stringifyMetadata(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Metadata = { organizationId: string };
|
||||||
|
|
||||||
|
export function translateSubscriptionStatus(
|
||||||
|
status: PaddleSdkSubscriptionCreatedEvent<unknown>["status"],
|
||||||
|
): SubscriptionStatus {
|
||||||
|
switch (status) {
|
||||||
|
case PaddleSdkSubscriptionStatus.ACTIVE:
|
||||||
|
return SubscriptionStatus.active;
|
||||||
|
case PaddleSdkSubscriptionStatus.CANCELLED:
|
||||||
|
return SubscriptionStatus.deleted;
|
||||||
|
case PaddleSdkSubscriptionStatus.PAUSED:
|
||||||
|
return SubscriptionStatus.paused;
|
||||||
|
case PaddleSdkSubscriptionStatus.PAST_DUE:
|
||||||
|
return SubscriptionStatus.past_due;
|
||||||
|
case PaddleSdkSubscriptionStatus.TRIALING:
|
||||||
|
return SubscriptionStatus.trialing;
|
||||||
|
default:
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const client = got.extend({
|
const client = got.extend({
|
||||||
prefixUrl: "https://vendors.paddle.com/api/2.0",
|
prefixUrl: "https://vendors.paddle.com/api/2.0",
|
||||||
});
|
});
|
||||||
@ -22,7 +54,7 @@ async function request<T>(path: string, data: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetPaymentsParams = {
|
type GetPaymentsParams = {
|
||||||
subscriptionId: string;
|
subscriptionId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getPayments({ subscriptionId }: GetPaymentsParams) {
|
export async function getPayments({ subscriptionId }: GetPaymentsParams) {
|
||||||
@ -62,8 +94,8 @@ export async function getPayments({ subscriptionId }: GetPaymentsParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSubscriptionPlanParams = {
|
type UpdateSubscriptionPlanParams = {
|
||||||
subscriptionId: string;
|
subscriptionId: number;
|
||||||
planId: string;
|
planId: number;
|
||||||
prorate?: boolean;
|
prorate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,7 +109,7 @@ export async function updateSubscriptionPlan({ subscriptionId, planId, prorate =
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelPaddleSubscription({ subscriptionId }: { subscriptionId: string }) {
|
export async function cancelPaddleSubscription({ subscriptionId }: { subscriptionId: number }) {
|
||||||
const { body } = await request("subscription/users_cancel", { subscription_id: subscriptionId });
|
const { body } = await request("subscription/users_cancel", { subscription_id: subscriptionId });
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
|
Loading…
Reference in New Issue
Block a user