From 899a604d6d7eafe2ea8cd1fac47aee7ea552abd2 Mon Sep 17 00:00:00 2001 From: MarconLP <13001502+MarconLP@users.noreply.github.com> Date: Sun, 23 Apr 2023 19:52:14 +0200 Subject: [PATCH] add stripe / payment events --- src/pages/api/webhooks/stripe.ts | 16 ++++ src/server/api/routers/stripe.ts | 128 +++++++++++++++----------- src/server/stripe-webhook-handlers.ts | 34 +++++++ 3 files changed, 123 insertions(+), 55 deletions(-) diff --git a/src/pages/api/webhooks/stripe.ts b/src/pages/api/webhooks/stripe.ts index 828bb9a..a7d4c8c 100644 --- a/src/pages/api/webhooks/stripe.ts +++ b/src/pages/api/webhooks/stripe.ts @@ -9,6 +9,7 @@ import { handleSubscriptionCreatedOrUpdated, } from "~/server/stripe-webhook-handlers"; import { stripe } from "~/server/stripe"; +import { posthog } from "~/server/posthog"; // Stripe requires the raw body to construct the event. export const config = { @@ -41,6 +42,7 @@ export default async function handler( event, stripe, prisma, + posthog, }); break; case "customer.subscription.created": @@ -48,6 +50,7 @@ export default async function handler( await handleSubscriptionCreatedOrUpdated({ event, prisma, + posthog, }); break; case "customer.subscription.updated": @@ -55,6 +58,7 @@ export default async function handler( await handleSubscriptionCreatedOrUpdated({ event, prisma, + posthog, }); break; case "invoice.payment_failed": @@ -63,6 +67,17 @@ export default async function handler( // Use this webhook to notify your user that their payment has // 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 + + 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; case "customer.subscription.deleted": // handle subscription cancelled automatically based @@ -70,6 +85,7 @@ export default async function handler( await handleSubscriptionCanceled({ event, prisma, + posthog, }); break; default: diff --git a/src/server/api/routers/stripe.ts b/src/server/api/routers/stripe.ts index 84edf28..65a0df9 100644 --- a/src/server/api/routers/stripe.ts +++ b/src/server/api/routers/stripe.ts @@ -6,9 +6,64 @@ import { z } from "zod"; export const stripeRouter = createTRPCRouter({ createCheckoutSession: protectedProcedure .input(z.object({ billedAnnually: z.boolean() })) - .mutation(async ({ ctx, input }) => { - const { stripe, session, prisma, req } = ctx; + .mutation( + 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({ prisma, stripe, @@ -24,63 +79,26 @@ export const stripeRouter = createTRPCRouter({ ? `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, - }, - }, - }); + const stripeBillingPortalSession = + await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: `${baseUrl}/videos`, + }); - if (!checkoutSession) { - throw new Error("Could not create checkout session"); + if (!stripeBillingPortalSession) { + throw new Error("Could not create billing portal session"); } - return { checkoutUrl: checkoutSession.url }; - }), - createBillingPortalSession: protectedProcedure.mutation(async ({ ctx }) => { - const { stripe, session, prisma, req } = ctx; - - 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`, + posthog.capture({ + distinctId: session.user.id, + event: "visiting billing portal page", + properties: { + stripeSubscriptionStatus: session.user.stripeSubscriptionStatus, + }, }); + void posthog.shutdownAsync(); - if (!stripeBillingPortalSession) { - throw new Error("Could not create billing portal session"); + return { billingPortalUrl: stripeBillingPortalSession.url }; } - - return { billingPortalUrl: stripeBillingPortalSession.url }; - }), + ), }); diff --git a/src/server/stripe-webhook-handlers.ts b/src/server/stripe-webhook-handlers.ts index aae52ba..397adf6 100644 --- a/src/server/stripe-webhook-handlers.ts +++ b/src/server/stripe-webhook-handlers.ts @@ -1,5 +1,6 @@ import type { PrismaClient } from "@prisma/client"; 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 export const getOrCreateStripeCustomerIdForUser = async ({ @@ -54,10 +55,12 @@ export const handleInvoicePaid = async ({ event, stripe, prisma, + posthog, }: { event: Stripe.Event; stripe: Stripe; prisma: PrismaClient; + posthog: PostHog; }) => { const invoice = event.data.object as Stripe.Invoice; const subscriptionId = invoice.subscription; @@ -76,14 +79,27 @@ export const handleInvoicePaid = async ({ 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 ({ event, prisma, + posthog, }: { event: Stripe.Event; prisma: PrismaClient; + posthog: PostHog; }) => { const subscription = event.data.object as Stripe.Subscription; const userId = subscription.metadata.userId; @@ -98,14 +114,24 @@ export const handleSubscriptionCreatedOrUpdated = async ({ stripeSubscriptionStatus: subscription.status, }, }); + + if (userId && subscription.status) { + posthog.capture({ + distinctId: userId, + event: "stripe subscription created or updated", + }); + void posthog.shutdownAsync(); + } }; export const handleSubscriptionCanceled = async ({ event, prisma, + posthog, }: { event: Stripe.Event; prisma: PrismaClient; + posthog: PostHog; }) => { const subscription = event.data.object as Stripe.Subscription; const userId = subscription.metadata.userId; @@ -120,4 +146,12 @@ export const handleSubscriptionCanceled = async ({ stripeSubscriptionStatus: null, }, }); + + if (userId && subscription.status) { + posthog.capture({ + distinctId: userId, + event: "stripe subscription cancelled", + }); + void posthog.shutdownAsync(); + } };