📸 Sync stripe subscriptions with space data (#1777)

This commit is contained in:
Luke Vella 2025-06-15 14:04:26 +02:00 committed by GitHub
parent 87ab11834a
commit 2fe17e7f32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 74 additions and 38 deletions

View file

@ -1,7 +1,7 @@
import type { Stripe } from "@rallly/billing";
import { prisma } from "@rallly/database";
import { getDefaultSpace, getSpace } from "@/features/spaces/queries";
import { getSpace } from "@/features/spaces/queries";
import { subscriptionMetadataSchema } from "@/features/subscription/schema";
import {
getExpandedSubscription,
@ -22,7 +22,7 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) {
const res = subscriptionMetadataSchema.safeParse(subscription.metadata);
if (!res.success) {
throw new Error("Missing user ID");
throw new Error("Invalid subscription metadata");
}
const userId = res.data.userId;
@ -39,37 +39,16 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) {
throw new Error(`User with ID ${userId} not found`);
}
let spaceId: string;
const space = await getSpace({ id: res.data.spaceId });
// The space should be in the metadata,
// but if it's not, we fallback to the default space.
// This is temporary while we haven't run a data migration
// to add the spaceId to the metadata for all existing subscriptions
if (res.data.spaceId) {
const space = await getSpace({ id: res.data.spaceId });
if (!space) {
throw new Error(`Space with ID ${res.data.spaceId} not found`);
}
if (space.ownerId !== userId) {
throw new Error(
`Space with ID ${res.data.spaceId} does not belong to user ${userId}`,
);
}
spaceId = space.id;
} else {
// TODO: Remove this fallback once all subscriptions have
// a spaceId in their metadata
const space = await getDefaultSpace({ ownerId: userId });
if (!space) {
throw new Error(`Default space with owner ID ${userId} not found`);
}
spaceId = space.id;
if (space.ownerId !== userId) {
throw new Error(
`Space with ID ${res.data.spaceId} does not belong to user ${userId}`,
);
}
const spaceId = space.id;
// If user already has a subscription, update it or replace it
if (existingUser.subscription) {
// Update the existing subscription with new data

View file

@ -26,7 +26,7 @@ export async function onCustomerSubscriptionUpdated(event: Stripe.Event) {
const res = subscriptionMetadataSchema.safeParse(subscription.metadata);
if (!res.success) {
throw new Error("Missing user ID");
throw new Error("Invalid subscription metadata");
}
// Update the subscription in the database

View file

@ -28,11 +28,9 @@ export async function getDefaultSpace({ ownerId }: { ownerId: string }) {
}
export async function getSpace({ id }: { id: string }) {
const space = await prisma.space.findUnique({
return await prisma.space.findUniqueOrThrow({
where: {
id,
},
});
return space;
}

View file

@ -11,7 +11,7 @@ export type SubscriptionCheckoutMetadata = z.infer<
export const subscriptionMetadataSchema = z.object({
userId: z.string(),
spaceId: z.string().optional(),
spaceId: z.string(),
});
export type SubscriptionMetadata = z.infer<typeof subscriptionMetadataSchema>;

View file

@ -7,9 +7,10 @@
"./*": "./src/*.ts"
},
"scripts": {
"checkout-expiry": "dotenv -e ../../.env -- tsx ./src/scripts/checkout-expiry.ts",
"subscription-data-sync": "dotenv -e ../../.env -- tsx ./src/scripts/subscription-data-sync.ts",
"sync-payment-methods": "dotenv -e ../../.env -- tsx ./src/scripts/sync-payment-methods.ts",
"checkout-expiry": "dotenv -e .env -- pnpx tsx ./src/scripts/checkout-expiry.ts",
"subscription-data-sync": "dotenv -e .env -- pnpx tsx ./src/scripts/subscription-data-sync.ts",
"sync-payment-methods": "dotenv -e .env -- pnpx tsx ./src/scripts/sync-payment-methods.ts",
"sync-space-subscription": "dotenv -e .env -- pnpx tsx ./src/scripts/sync-space-subscription.ts",
"type-check": "tsc --pretty --noEmit"
},
"dependencies": {

View file

@ -0,0 +1,58 @@
import { prisma } from "@rallly/database";
import { stripe } from "../lib/stripe";
(async function syncSpaceSubscription() {
let processed = 0;
let failed = 0;
const subscriptions = await prisma.subscription.findMany({
where: {
active: true,
},
select: {
id: true,
userId: true,
},
});
console.info(`🚀 Syncing ${subscriptions.length} subscriptions...`);
for (const subscription of subscriptions) {
const space = await prisma.space.findFirst({
where: {
ownerId: subscription.userId,
},
orderBy: {
createdAt: "asc",
},
});
if (!space) {
console.info(`Space not found for user ${subscription.userId}`);
continue;
}
try {
await stripe.subscriptions.update(subscription.id, {
metadata: {
userId: subscription.userId,
spaceId: space.id,
},
});
console.info(
`✅ Space subscription synced for subscription ${subscription.id}`,
);
processed++;
} catch (error) {
console.error(
`❌ Failed to sync space subscription for subscription ${subscription.id}:`,
error,
);
failed++;
}
}
console.info(`📊 Sync complete: ${processed} processed, ${failed} failed`);
})();