diff --git a/app/api/inngest/functions/stripe_handle_checkout_completed.ts b/app/api/inngest/functions/stripe_handle_checkout_completed.ts deleted file mode 100644 index 46804dd9..00000000 --- a/app/api/inngest/functions/stripe_handle_checkout_completed.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { inngest } from "../client"; -import { getStripe } from "stripe/client"; -import { supabaseServerClient } from "supabase/serverClient"; -import { PRODUCT_DEFINITION, parseEntitlements } from "stripe/products"; - -export const stripe_handle_checkout_completed = inngest.createFunction( - { id: "stripe-handle-checkout-completed" }, - { event: "stripe/checkout.session.completed" }, - async ({ event, step }) => { - const session = await step.run("fetch-checkout-session", async () => { - const s = await getStripe().checkout.sessions.retrieve( - event.data.sessionId, - { expand: ["subscription"] }, - ); - const sub = - typeof s.subscription === "object" ? s.subscription : null; - const periodEnd = sub?.items.data[0]?.current_period_end ?? 0; - const lookupKey = sub?.items.data[0]?.price.lookup_key ?? null; - - return { - identityId: s.client_reference_id, - customerId: s.customer as string, - subId: sub?.id ?? null, - subStatus: sub?.status ?? null, - periodEnd, - lookupKey, - }; - }); - - if (!session.identityId || !session.subId) { - throw new Error("Missing client_reference_id or subscription"); - } - - await step.run("upsert-subscription-and-entitlements", async () => { - const entitlements = parseEntitlements(PRODUCT_DEFINITION.metadata); - - await supabaseServerClient.from("user_subscriptions").upsert( - { - identity_id: session.identityId!, - stripe_customer_id: session.customerId, - stripe_subscription_id: session.subId!, - plan: session.lookupKey, - status: session.subStatus, - current_period_end: new Date( - session.periodEnd * 1000, - ).toISOString(), - updated_at: new Date().toISOString(), - }, - { onConflict: "identity_id" }, - ); - - for (const key of Object.keys(entitlements)) { - await supabaseServerClient.from("user_entitlements").upsert( - { - identity_id: session.identityId!, - entitlement_key: key, - granted_at: new Date().toISOString(), - expires_at: new Date( - session.periodEnd * 1000, - ).toISOString(), - source: `stripe:${session.subId}`, - }, - { onConflict: "identity_id,entitlement_key" }, - ); - } - }); - - return { success: true }; - }, -); diff --git a/app/api/inngest/functions/stripe_handle_invoice_payment_failed.ts b/app/api/inngest/functions/stripe_handle_invoice_payment_failed.ts deleted file mode 100644 index ff9bead7..00000000 --- a/app/api/inngest/functions/stripe_handle_invoice_payment_failed.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { inngest } from "../client"; -import { supabaseServerClient } from "supabase/serverClient"; - -export const stripe_handle_invoice_payment_failed = inngest.createFunction( - { id: "stripe-handle-invoice-payment-failed" }, - { event: "stripe/invoice.payment.failed" }, - async ({ event, step }) => { - await step.run("mark-subscription-past-due", async () => { - if (event.data.subscriptionId) { - await supabaseServerClient - .from("user_subscriptions") - .update({ - status: "past_due", - updated_at: new Date().toISOString(), - }) - .eq("stripe_subscription_id", event.data.subscriptionId); - } - }); - - // Entitlements remain valid until expires_at - return { success: true }; - }, -); diff --git a/app/api/inngest/functions/stripe_handle_subscription_deleted.ts b/app/api/inngest/functions/stripe_handle_subscription_deleted.ts deleted file mode 100644 index 1ebd839f..00000000 --- a/app/api/inngest/functions/stripe_handle_subscription_deleted.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { inngest } from "../client"; -import { supabaseServerClient } from "supabase/serverClient"; - -export const stripe_handle_subscription_deleted = inngest.createFunction( - { id: "stripe-handle-subscription-deleted" }, - { event: "stripe/customer.subscription.deleted" }, - async ({ event, step }) => { - await step.run("mark-subscription-canceled", async () => { - await supabaseServerClient - .from("user_subscriptions") - .update({ - status: "canceled", - updated_at: new Date().toISOString(), - }) - .eq("stripe_subscription_id", event.data.subscriptionId); - }); - - // Entitlements expire naturally via expires_at — no need to delete them - return { success: true }; - }, -); diff --git a/app/api/inngest/functions/stripe_handle_subscription_updated.ts b/app/api/inngest/functions/stripe_handle_subscription_updated.ts deleted file mode 100644 index 74e9b346..00000000 --- a/app/api/inngest/functions/stripe_handle_subscription_updated.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { inngest } from "../client"; -import { getStripe } from "stripe/client"; -import { supabaseServerClient } from "supabase/serverClient"; -import { PRODUCT_DEFINITION, parseEntitlements } from "stripe/products"; - -export const stripe_handle_subscription_updated = inngest.createFunction( - { id: "stripe-handle-subscription-updated" }, - { event: "stripe/customer.subscription.updated" }, - async ({ event, step }) => { - const subData = await step.run("fetch-subscription", async () => { - const sub = await getStripe().subscriptions.retrieve( - event.data.subscriptionId, - ); - const periodEnd = sub.items.data[0]?.current_period_end ?? 0; - const lookupKey = sub.items.data[0]?.price.lookup_key ?? null; - - return { - id: sub.id, - customerId: sub.customer as string, - status: sub.cancel_at_period_end ? "canceling" : sub.status, - periodEnd, - lookupKey, - metadataIdentityId: sub.metadata.identity_id ?? null, - }; - }); - - await step.run("update-subscription-and-entitlements", async () => { - const entitlements = parseEntitlements(PRODUCT_DEFINITION.metadata); - - // Find the identity by stripe_customer_id - const { data: existingSub } = await supabaseServerClient - .from("user_subscriptions") - .select("identity_id") - .eq("stripe_customer_id", subData.customerId) - .single(); - - const identityId = existingSub?.identity_id ?? subData.metadataIdentityId; - - if (!identityId) { - console.warn( - `No subscription record for customer ${subData.customerId} and no identity_id in metadata`, - ); - return; - } - - // Upsert subscription record - await supabaseServerClient - .from("user_subscriptions") - .upsert( - { - identity_id: identityId, - stripe_customer_id: subData.customerId, - stripe_subscription_id: subData.id, - status: subData.status, - plan: subData.lookupKey, - current_period_end: new Date( - subData.periodEnd * 1000, - ).toISOString(), - updated_at: new Date().toISOString(), - }, - { onConflict: "identity_id" }, - ); - - // Upsert entitlements for all entitlements from this subscription - for (const key of Object.keys(entitlements)) { - await supabaseServerClient.from("user_entitlements").upsert( - { - identity_id: identityId, - entitlement_key: key, - granted_at: new Date().toISOString(), - expires_at: new Date(subData.periodEnd * 1000).toISOString(), - source: `stripe:${subData.id}`, - }, - { onConflict: "identity_id,entitlement_key" }, - ); - } - }); - - return { success: true }; - }, -); diff --git a/app/api/inngest/route.tsx b/app/api/inngest/route.tsx index bb3e0967..a74f1d98 100644 --- a/app/api/inngest/route.tsx +++ b/app/api/inngest/route.tsx @@ -13,10 +13,6 @@ import { check_oauth_session, } from "./functions/cleanup_expired_oauth_sessions"; import { write_records_to_pds } from "./functions/write_records_to_pds"; -import { stripe_handle_checkout_completed } from "./functions/stripe_handle_checkout_completed"; -import { stripe_handle_subscription_updated } from "./functions/stripe_handle_subscription_updated"; -import { stripe_handle_subscription_deleted } from "./functions/stripe_handle_subscription_deleted"; -import { stripe_handle_invoice_payment_failed } from "./functions/stripe_handle_invoice_payment_failed"; import { sync_document_metadata } from "./functions/sync_document_metadata"; export const { GET, POST, PUT } = serve({ @@ -33,10 +29,6 @@ export const { GET, POST, PUT } = serve({ cleanup_expired_oauth_sessions, check_oauth_session, write_records_to_pds, - stripe_handle_checkout_completed, - stripe_handle_subscription_updated, - stripe_handle_subscription_deleted, - stripe_handle_invoice_payment_failed, sync_document_metadata, ], }); diff --git a/app/api/webhooks/stripe/route.ts b/app/api/webhooks/stripe/route.ts index 6ffc1ecf..576790ba 100644 --- a/app/api/webhooks/stripe/route.ts +++ b/app/api/webhooks/stripe/route.ts @@ -1,6 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { getStripe } from "stripe/client"; import { inngest } from "app/api/inngest/client"; +import { handleCheckoutCompleted } from "./handle_checkout_completed"; +import { handleSubscriptionUpdated } from "./handle_subscription_updated"; +import { handleSubscriptionDeleted } from "./handle_subscription_deleted"; +import { handleInvoicePaymentFailed } from "./handle_invoice_payment_failed"; export async function POST(req: NextRequest) { const body = await req.text(); @@ -22,27 +26,36 @@ export async function POST(req: NextRequest) { } switch (event.type) { - case "checkout.session.completed": + case "checkout.session.completed": { + const sessionId = event.data.object.id; await inngest.send({ name: "stripe/checkout.session.completed", - data: { sessionId: event.data.object.id }, + data: { sessionId }, }); + await handleCheckoutCompleted(sessionId); break; + } case "customer.subscription.created": - case "customer.subscription.updated": + case "customer.subscription.updated": { + const subscriptionId = event.data.object.id; await inngest.send({ name: "stripe/customer.subscription.updated", - data: { subscriptionId: event.data.object.id }, + data: { subscriptionId }, }); + await handleSubscriptionUpdated(subscriptionId); break; + } - case "customer.subscription.deleted": + case "customer.subscription.deleted": { + const subscriptionId = event.data.object.id; await inngest.send({ name: "stripe/customer.subscription.deleted", - data: { subscriptionId: event.data.object.id }, + data: { subscriptionId }, }); + await handleSubscriptionDeleted(subscriptionId); break; + } case "invoice.payment_failed": { const invoice = event.data.object; @@ -61,6 +74,7 @@ export async function POST(req: NextRequest) { customerId: invoice.customer as string, }, }); + await handleInvoicePaymentFailed(subId); break; } }