feat(cloud): enable upgrading new and existing workspaces onto team/pro

This commit is contained in:
Henry
2026-04-17 21:45:04 +01:00
parent 25490b2223
commit 9815b5f273
2 changed files with 117 additions and 104 deletions

View File

@@ -20,13 +20,13 @@ const workspaceSlugSchema = z
interface CheckoutSessionRequest { interface CheckoutSessionRequest {
successUrl: string; successUrl: string;
cancelUrl: string; cancelUrl: string;
plan: "team" | "pro";
billing?: string; billing?: string;
workspacePublicId?: string; workspacePublicId?: string;
slug?: string; slug?: string;
workspaceName?: string; workspaceName?: string;
workspaceDescription?: string; workspaceDescription?: string;
workspaceSlug?: string; workspaceSlug?: string;
plan?: string;
} }
export default withRateLimit( export default withRateLimit(
@@ -38,7 +38,6 @@ export default withRateLimit(
return res.status(405).json({ error: "Method not allowed" }); return res.status(405).json({ error: "Method not allowed" });
} }
try {
const { user, db } = await createNextApiContext(req); const { user, db } = await createNextApiContext(req);
if (!user) { if (!user) {
@@ -49,6 +48,7 @@ export default withRateLimit(
const { const {
successUrl, successUrl,
cancelUrl, cancelUrl,
plan,
billing, billing,
workspacePublicId, workspacePublicId,
slug, slug,
@@ -57,7 +57,7 @@ export default withRateLimit(
workspaceSlug, workspaceSlug,
} = body; } = body;
if (!successUrl || !cancelUrl) { if (!successUrl || !cancelUrl || !plan) {
return res.status(400).json({ error: "Missing required fields" }); return res.status(400).json({ error: "Missing required fields" });
} }
@@ -97,7 +97,7 @@ export default withRateLimit(
} }
const subscription = await subscriptionRepo.create(db, { const subscription = await subscriptionRepo.create(db, {
plan: "pro", plan: plan,
referenceId: workspacePublicId, referenceId: workspacePublicId,
userId: user.id, userId: user.id,
stripeCustomerId: user.stripeCustomerId ?? "", stripeCustomerId: user.stripeCustomerId ?? "",
@@ -115,9 +115,9 @@ export default withRateLimit(
const isTeam = body.plan === "team"; const isTeam = body.plan === "team";
const annualPriceId = isTeam const annualPriceId = isTeam
? (process.env.STRIPE_TEAM_PLAN_ANNUAL_PRICE_ID ?? ? (process.env.STRIPE_TEAM_PLAN_YEARLY_PRICE_ID ??
process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID) process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID)
: (process.env.STRIPE_PRO_PLAN_ANNUAL_PRICE_ID ?? : (process.env.STRIPE_PRO_PLAN_YEARLY_PRICE_ID ??
process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID); process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID);
const monthlyPriceId = isTeam const monthlyPriceId = isTeam
? process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID ? process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID
@@ -141,14 +141,11 @@ export default withRateLimit(
...(workspaceName && { workspaceName }), ...(workspaceName && { workspaceName }),
...(workspaceDescription && { workspaceDescription }), ...(workspaceDescription && { workspaceDescription }),
...(subscriptionId && { subscriptionId: String(subscriptionId) }), ...(subscriptionId && { subscriptionId: String(subscriptionId) }),
plan: body.plan ?? "pro", plan: plan,
isNewWorkspace: workspaceName ? "true" : "false", isNewWorkspace: workspaceName ? "true" : "false",
}, },
}); });
return res.status(200).json({ url: session.url }); return res.status(200).json({ url: session.url });
} catch (error) {
return res.status(500).json({ error: "Error creating checkout session" });
}
}), }),
); );

View File

@@ -20,8 +20,11 @@ export function withApiLogging(
const requestId = randomUUID(); const requestId = randomUUID();
const route = req.url?.split("?")[0] ?? "unknown"; const route = req.url?.split("?")[0] ?? "unknown";
const input = { const input = {
...(req.query && Object.keys(req.query).length > 0 && { query: req.query }), ...(req.query &&
...(req.body && typeof req.body === "object" && Object.keys(req.body).length > 0 && { body: req.body }), Object.keys(req.query).length > 0 && { query: req.query }),
...(req.body &&
typeof req.body === "object" &&
Object.keys(req.body).length > 0 && { body: req.body }),
}; };
let statusCode = 200; let statusCode = 200;
@@ -41,7 +44,16 @@ export function withApiLogging(
// unauthenticated or auth unavailable // unauthenticated or auth unavailable
} }
let handlerError: unknown;
try {
await handler(req, res); await handler(req, res);
} catch (err) {
handlerError = err;
statusCode = 500;
if (!res.headersSent) {
res.status(500).json({ error: "Internal server error" });
}
}
const duration = Date.now() - start; const duration = Date.now() - start;
const meta = { const meta = {
@@ -53,6 +65,10 @@ export function withApiLogging(
...(isCloud && email && { email }), ...(isCloud && email && { email }),
...(Object.keys(input).length > 0 && { input }), ...(Object.keys(input).length > 0 && { input }),
status: statusCode, status: statusCode,
...(handlerError instanceof Error && {
error: handlerError.message,
stack: handlerError.stack,
}),
}; };
if (statusCode < 400) { if (statusCode < 400) {