add stripe / payment events
This commit is contained in:
parent
d778f61f33
commit
899a604d6d
3 changed files with 123 additions and 55 deletions
|
|
@ -9,6 +9,7 @@ import {
|
||||||
handleSubscriptionCreatedOrUpdated,
|
handleSubscriptionCreatedOrUpdated,
|
||||||
} from "~/server/stripe-webhook-handlers";
|
} from "~/server/stripe-webhook-handlers";
|
||||||
import { stripe } from "~/server/stripe";
|
import { stripe } from "~/server/stripe";
|
||||||
|
import { posthog } from "~/server/posthog";
|
||||||
|
|
||||||
// Stripe requires the raw body to construct the event.
|
// Stripe requires the raw body to construct the event.
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|
@ -41,6 +42,7 @@ export default async function handler(
|
||||||
event,
|
event,
|
||||||
stripe,
|
stripe,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.created":
|
case "customer.subscription.created":
|
||||||
|
|
@ -48,6 +50,7 @@ export default async function handler(
|
||||||
await handleSubscriptionCreatedOrUpdated({
|
await handleSubscriptionCreatedOrUpdated({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.updated":
|
case "customer.subscription.updated":
|
||||||
|
|
@ -55,6 +58,7 @@ export default async function handler(
|
||||||
await handleSubscriptionCreatedOrUpdated({
|
await handleSubscriptionCreatedOrUpdated({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "invoice.payment_failed":
|
case "invoice.payment_failed":
|
||||||
|
|
@ -63,6 +67,17 @@ export default async function handler(
|
||||||
// Use this webhook to notify your user that their payment has
|
// Use this webhook to notify your user that their payment has
|
||||||
// failed and to retrieve new card details.
|
// failed and to retrieve new card details.
|
||||||
// Can also have Stripe send an email to the customer notifying them of the failure. See settings: https://dashboard.stripe.com/settings/billing/automatic
|
// Can also have Stripe send an email to the customer notifying them of the failure. See settings: https://dashboard.stripe.com/settings/billing/automatic
|
||||||
|
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
const userId = subscription.metadata.userId;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe invoice.payment_failed",
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.deleted":
|
case "customer.subscription.deleted":
|
||||||
// handle subscription cancelled automatically based
|
// handle subscription cancelled automatically based
|
||||||
|
|
@ -70,6 +85,7 @@ export default async function handler(
|
||||||
await handleSubscriptionCanceled({
|
await handleSubscriptionCanceled({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,64 @@ import { z } from "zod";
|
||||||
export const stripeRouter = createTRPCRouter({
|
export const stripeRouter = createTRPCRouter({
|
||||||
createCheckoutSession: protectedProcedure
|
createCheckoutSession: protectedProcedure
|
||||||
.input(z.object({ billedAnnually: z.boolean() }))
|
.input(z.object({ billedAnnually: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(
|
||||||
const { stripe, session, prisma, req } = ctx;
|
async ({ ctx: { prisma, stripe, session, req, posthog }, input }) => {
|
||||||
|
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||||
|
prisma,
|
||||||
|
stripe,
|
||||||
|
userId: session.user?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!customerId) {
|
||||||
|
throw new Error("Could not create customer");
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl =
|
||||||
|
env.NODE_ENV === "development"
|
||||||
|
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||||
|
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||||
|
|
||||||
|
const checkoutSession = await stripe.checkout.sessions.create({
|
||||||
|
customer: customerId,
|
||||||
|
client_reference_id: session.user?.id,
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
allow_promotion_codes: true,
|
||||||
|
mode: "subscription",
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: input.billedAnnually
|
||||||
|
? env.STRIPE_ANNUAL_PRICE_ID
|
||||||
|
: env.STRIPE_MONTHLY_PRICE_ID,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
success_url: `${baseUrl}/videos?checkoutSuccess=true`,
|
||||||
|
cancel_url: `${baseUrl}/videos?checkoutCanceled=true`,
|
||||||
|
subscription_data: {
|
||||||
|
metadata: {
|
||||||
|
userId: session.user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!checkoutSession) {
|
||||||
|
throw new Error("Could not create checkout session");
|
||||||
|
}
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "visiting checkout page",
|
||||||
|
properties: {
|
||||||
|
billingCycle: input.billedAnnually ? "annual" : "monthly",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
|
return { checkoutUrl: checkoutSession.url };
|
||||||
|
}
|
||||||
|
),
|
||||||
|
createBillingPortalSession: protectedProcedure.mutation(
|
||||||
|
async ({ ctx: { stripe, session, prisma, req, posthog } }) => {
|
||||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||||
prisma,
|
prisma,
|
||||||
stripe,
|
stripe,
|
||||||
|
|
@ -24,63 +79,26 @@ export const stripeRouter = createTRPCRouter({
|
||||||
? `http://${req.headers.host ?? "localhost:3000"}`
|
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||||
|
|
||||||
const checkoutSession = await stripe.checkout.sessions.create({
|
const stripeBillingPortalSession =
|
||||||
customer: customerId,
|
await stripe.billingPortal.sessions.create({
|
||||||
client_reference_id: session.user?.id,
|
customer: customerId,
|
||||||
payment_method_types: ["card"],
|
return_url: `${baseUrl}/videos`,
|
||||||
allow_promotion_codes: true,
|
});
|
||||||
mode: "subscription",
|
|
||||||
line_items: [
|
|
||||||
{
|
|
||||||
price: input.billedAnnually
|
|
||||||
? env.STRIPE_ANNUAL_PRICE_ID
|
|
||||||
: env.STRIPE_MONTHLY_PRICE_ID,
|
|
||||||
quantity: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
success_url: `${baseUrl}/videos?checkoutSuccess=true`,
|
|
||||||
cancel_url: `${baseUrl}/videos?checkoutCanceled=true`,
|
|
||||||
subscription_data: {
|
|
||||||
metadata: {
|
|
||||||
userId: session.user?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!checkoutSession) {
|
if (!stripeBillingPortalSession) {
|
||||||
throw new Error("Could not create checkout session");
|
throw new Error("Could not create billing portal session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { checkoutUrl: checkoutSession.url };
|
posthog.capture({
|
||||||
}),
|
distinctId: session.user.id,
|
||||||
createBillingPortalSession: protectedProcedure.mutation(async ({ ctx }) => {
|
event: "visiting billing portal page",
|
||||||
const { stripe, session, prisma, req } = ctx;
|
properties: {
|
||||||
|
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
},
|
||||||
prisma,
|
|
||||||
stripe,
|
|
||||||
userId: session.user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customerId) {
|
|
||||||
throw new Error("Could not create customer");
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl =
|
|
||||||
env.NODE_ENV === "development"
|
|
||||||
? `http://${req.headers.host ?? "localhost:3000"}`
|
|
||||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
|
||||||
|
|
||||||
const stripeBillingPortalSession =
|
|
||||||
await stripe.billingPortal.sessions.create({
|
|
||||||
customer: customerId,
|
|
||||||
return_url: `${baseUrl}/videos`,
|
|
||||||
});
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
|
||||||
if (!stripeBillingPortalSession) {
|
return { billingPortalUrl: stripeBillingPortalSession.url };
|
||||||
throw new Error("Could not create billing portal session");
|
|
||||||
}
|
}
|
||||||
|
),
|
||||||
return { billingPortalUrl: stripeBillingPortalSession.url };
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { PrismaClient } from "@prisma/client";
|
import type { PrismaClient } from "@prisma/client";
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
|
import { type PostHog } from "posthog-node";
|
||||||
|
|
||||||
// retrieves a Stripe customer id for a given user if it exists or creates a new one
|
// retrieves a Stripe customer id for a given user if it exists or creates a new one
|
||||||
export const getOrCreateStripeCustomerIdForUser = async ({
|
export const getOrCreateStripeCustomerIdForUser = async ({
|
||||||
|
|
@ -54,10 +55,12 @@ export const handleInvoicePaid = async ({
|
||||||
event,
|
event,
|
||||||
stripe,
|
stripe,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
}: {
|
}: {
|
||||||
event: Stripe.Event;
|
event: Stripe.Event;
|
||||||
stripe: Stripe;
|
stripe: Stripe;
|
||||||
prisma: PrismaClient;
|
prisma: PrismaClient;
|
||||||
|
posthog: PostHog;
|
||||||
}) => {
|
}) => {
|
||||||
const invoice = event.data.object as Stripe.Invoice;
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
const subscriptionId = invoice.subscription;
|
const subscriptionId = invoice.subscription;
|
||||||
|
|
@ -76,14 +79,27 @@ export const handleInvoicePaid = async ({
|
||||||
stripeSubscriptionStatus: subscription.status,
|
stripeSubscriptionStatus: subscription.status,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userId && subscription.status) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe invoice.paid",
|
||||||
|
properties: {
|
||||||
|
stripeSubscriptionStatus: subscription.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleSubscriptionCreatedOrUpdated = async ({
|
export const handleSubscriptionCreatedOrUpdated = async ({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
}: {
|
}: {
|
||||||
event: Stripe.Event;
|
event: Stripe.Event;
|
||||||
prisma: PrismaClient;
|
prisma: PrismaClient;
|
||||||
|
posthog: PostHog;
|
||||||
}) => {
|
}) => {
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
const userId = subscription.metadata.userId;
|
const userId = subscription.metadata.userId;
|
||||||
|
|
@ -98,14 +114,24 @@ export const handleSubscriptionCreatedOrUpdated = async ({
|
||||||
stripeSubscriptionStatus: subscription.status,
|
stripeSubscriptionStatus: subscription.status,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userId && subscription.status) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe subscription created or updated",
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleSubscriptionCanceled = async ({
|
export const handleSubscriptionCanceled = async ({
|
||||||
event,
|
event,
|
||||||
prisma,
|
prisma,
|
||||||
|
posthog,
|
||||||
}: {
|
}: {
|
||||||
event: Stripe.Event;
|
event: Stripe.Event;
|
||||||
prisma: PrismaClient;
|
prisma: PrismaClient;
|
||||||
|
posthog: PostHog;
|
||||||
}) => {
|
}) => {
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
const userId = subscription.metadata.userId;
|
const userId = subscription.metadata.userId;
|
||||||
|
|
@ -120,4 +146,12 @@ export const handleSubscriptionCanceled = async ({
|
||||||
stripeSubscriptionStatus: null,
|
stripeSubscriptionStatus: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userId && subscription.status) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId: userId,
|
||||||
|
event: "stripe subscription cancelled",
|
||||||
|
});
|
||||||
|
void posthog.shutdownAsync();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue