feat: plan limits

This commit is contained in:
Mythie
2023-10-15 20:26:32 +11:00
parent f75f191a9a
commit c343e8a221
31 changed files with 750 additions and 272 deletions

View File

@ -0,0 +1,224 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { buffer } from 'micro';
import { match } from 'ts-pattern';
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { prisma } from '@documenso/prisma';
import { onSubscriptionDeleted } from './on-subscription-deleted';
import { onSubscriptionUpdated } from './on-subscription-updated';
type StripeWebhookResponse = {
success: boolean;
message: string;
};
export const stripeWebhookHandler = async (
req: NextApiRequest,
res: NextApiResponse<StripeWebhookResponse>,
) => {
try {
const isBillingEnabled = await getFlag('app_billing');
if (!isBillingEnabled) {
return res.status(500).json({
success: false,
message: 'Billing is disabled',
});
}
const signature =
typeof req.headers['stripe-signature'] === 'string' ? req.headers['stripe-signature'] : '';
if (!signature) {
return res.status(400).json({
success: false,
message: 'No signature found in request',
});
}
const body = await buffer(req);
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET,
);
await match(event.type)
.with('checkout.session.completed', async () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const session = event.data.object as Stripe.Checkout.Session;
const userId = Number(session.client_reference_id);
const subscriptionId =
typeof session.subscription === 'string'
? session.subscription
: session.subscription?.id;
if (!subscriptionId || Number.isNaN(userId)) {
return res.status(500).json({
success: false,
message: 'Invalid session',
});
}
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await onSubscriptionUpdated({ userId, subscription });
return res.status(200).json({
success: true,
message: 'Webhook received',
});
})
.with('customer.subscription.updated', async () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const subscription = event.data.object as Stripe.Subscription;
const customerId =
typeof subscription.customer === 'string'
? subscription.customer
: subscription.customer.id;
const result = await prisma.subscription.findFirst({
select: {
userId: true,
},
where: {
customerId,
},
});
if (!result?.userId) {
return res.status(500).json({
success: false,
message: 'User not found',
});
}
await onSubscriptionUpdated({ userId: result.userId, subscription });
return res.status(200).json({
success: true,
message: 'Webhook received',
});
})
.with('invoice.payment_succeeded', async () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const invoice = event.data.object as Stripe.Invoice;
if (invoice.billing_reason !== 'subscription_cycle') {
return res.status(200).json({
success: true,
message: 'Webhook received',
});
}
const customerId =
typeof invoice.customer === 'string' ? invoice.customer : invoice.customer?.id;
const subscriptionId =
typeof invoice.subscription === 'string'
? invoice.subscription
: invoice.subscription?.id;
if (!customerId || !subscriptionId) {
return res.status(500).json({
success: false,
message: 'Invalid invoice',
});
}
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const result = await prisma.subscription.findFirst({
select: {
userId: true,
},
where: {
customerId,
},
});
if (!result?.userId) {
return res.status(500).json({
success: false,
message: 'User not found',
});
}
await onSubscriptionUpdated({ userId: result.userId, subscription });
return res.status(200).json({
success: true,
message: 'Webhook received',
});
})
.with('invoice.payment_failed', async () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const invoice = event.data.object as Stripe.Invoice;
const customerId =
typeof invoice.customer === 'string' ? invoice.customer : invoice.customer?.id;
const subscriptionId =
typeof invoice.subscription === 'string'
? invoice.subscription
: invoice.subscription?.id;
if (!customerId || !subscriptionId) {
return res.status(500).json({
success: false,
message: 'Invalid invoice',
});
}
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const result = await prisma.subscription.findFirst({
select: {
userId: true,
},
where: {
customerId,
},
});
if (!result?.userId) {
return res.status(500).json({
success: false,
message: 'User not found',
});
}
await onSubscriptionUpdated({ userId: result.userId, subscription });
})
.with('customer.subscription.deleted', async () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const subscription = event.data.object as Stripe.Subscription;
await onSubscriptionDeleted({ subscription });
return res.status(200).json({
success: true,
message: 'Webhook received',
});
})
.otherwise(() => {
return res.status(200).json({
success: true,
message: 'Webhook received',
});
});
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
message: 'Unknown error',
});
}
};

View File

@ -0,0 +1,21 @@
import { Stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type OnSubscriptionDeletedOptions = {
subscription: Stripe.Subscription;
};
export const onSubscriptionDeleted = async ({ subscription }: OnSubscriptionDeletedOptions) => {
const customerId =
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id;
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.INACTIVE,
},
});
};

View File

@ -0,0 +1,44 @@
import { match } from 'ts-pattern';
import { Stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type OnSubscriptionUpdatedOptions = {
userId: number;
subscription: Stripe.Subscription;
};
export const onSubscriptionUpdated = async ({
userId,
subscription,
}: OnSubscriptionUpdatedOptions) => {
const customerId =
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id;
const status = match(subscription.status)
.with('active', () => SubscriptionStatus.ACTIVE)
.with('past_due', () => SubscriptionStatus.PAST_DUE)
.otherwise(() => SubscriptionStatus.INACTIVE);
await prisma.subscription.upsert({
where: {
customerId,
},
create: {
customerId,
status: status,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
userId,
},
update: {
customerId,
status: status,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
},
});
};