feat: update subscription when increasing/decreasing workspace members
This commit is contained in:
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||
import * as userRepo from "@kan/db/repository/user.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { updateSubscriptionSeats } from "@kan/stripe";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { assertUserInWorkspace } from "../utils/auth";
|
||||
@@ -60,6 +61,42 @@ export const memberRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_KAN_ENV === "cloud") {
|
||||
const subscriptions = await ctx.auth.api.listActiveSubscriptions({
|
||||
workspacePublicId: workspace.publicId,
|
||||
});
|
||||
|
||||
// get the active subscription
|
||||
const activeSubscription = subscriptions.find(
|
||||
(sub) =>
|
||||
sub.status === "active" ||
|
||||
(sub.status === "trialing" && sub.plan === "team"),
|
||||
);
|
||||
|
||||
if (!activeSubscription) {
|
||||
throw new TRPCError({
|
||||
message: `Workspace with public ID ${workspace.publicId} does not have an active subscription`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the Stripe subscription to add a seat with immediate proration
|
||||
if (activeSubscription.stripeSubscriptionId) {
|
||||
try {
|
||||
await updateSubscriptionSeats(
|
||||
activeSubscription.stripeSubscriptionId,
|
||||
1,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update Stripe subscription seats:", error);
|
||||
throw new TRPCError({
|
||||
message: `Failed to update subscription for the new member.`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existingUser = await userRepo.getByEmail(ctx.db, input.email);
|
||||
|
||||
const invite = await memberRepo.create(ctx.db, {
|
||||
@@ -100,19 +137,6 @@ export const memberRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_KAN_ENV === "cloud") {
|
||||
const subscriptions = await ctx.auth.api.listActiveSubscriptions({
|
||||
userId,
|
||||
});
|
||||
|
||||
// get the active subscription
|
||||
const activeSubscription = subscriptions.find(
|
||||
(sub) => sub.status === "active" || sub.status === "trialing",
|
||||
);
|
||||
|
||||
// @todo: update the subscription with the new seat
|
||||
}
|
||||
|
||||
return invite;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
@@ -178,6 +202,35 @@ export const memberRouter = createTRPCRouter({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
// Handle subscription seat decrement for cloud environment
|
||||
if (process.env.NEXT_PUBLIC_KAN_ENV === "cloud") {
|
||||
const subscriptions = await ctx.auth.api.listActiveSubscriptions({
|
||||
workspacePublicId: workspace.publicId,
|
||||
});
|
||||
|
||||
// get the active subscription
|
||||
const activeSubscription = subscriptions.find(
|
||||
(sub) =>
|
||||
sub.status === "active" ||
|
||||
(sub.status === "trialing" && sub.plan === "team"),
|
||||
);
|
||||
|
||||
// Only decrease seats if there's an active subscription and stripeSubscriptionId
|
||||
if (activeSubscription?.stripeSubscriptionId) {
|
||||
try {
|
||||
await updateSubscriptionSeats(
|
||||
activeSubscription.stripeSubscriptionId,
|
||||
-1,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to decrease Stripe subscription seats:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -28,16 +28,14 @@ const createAuthWithHeaders = (
|
||||
api: {
|
||||
getSession: () => auth.api.getSession({ headers }),
|
||||
signInMagicLink: (input: { email: string; callbackURL: string }) =>
|
||||
// @ts-expect-error - types need fixing
|
||||
auth.api.signInMagicLink({
|
||||
headers,
|
||||
body: { email: input.email, callbackURL: input.callbackURL },
|
||||
}),
|
||||
listActiveSubscriptions: (input: { userId: string }) =>
|
||||
// @ts-expect-error - types need fixing
|
||||
listActiveSubscriptions: (input: { workspacePublicId: string }) =>
|
||||
auth.api.listActiveSubscriptions({
|
||||
headers,
|
||||
query: { referenceId: input.userId },
|
||||
query: { referenceId: input.workspacePublicId },
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,4 +16,59 @@ const createStripeClient = () => {
|
||||
return stripe;
|
||||
};
|
||||
|
||||
export const updateSubscriptionSeats = async (
|
||||
stripeSubscriptionId: string,
|
||||
seatIncrement = 1,
|
||||
): Promise<Stripe.Subscription> => {
|
||||
const stripe = createStripeClient();
|
||||
|
||||
// First, retrieve the current subscription to get the subscription items
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
stripeSubscriptionId,
|
||||
{
|
||||
expand: ["items"],
|
||||
},
|
||||
);
|
||||
|
||||
if (!subscription.items.data.length) {
|
||||
throw new Error(
|
||||
`No subscription items found for subscription ${stripeSubscriptionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the first subscription item
|
||||
const subscriptionItem = subscription.items.data[0];
|
||||
if (!subscriptionItem) {
|
||||
throw new Error(
|
||||
`No subscription item found for subscription ${stripeSubscriptionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const currentQuantity = subscriptionItem.quantity ?? 1;
|
||||
const newQuantity = currentQuantity + seatIncrement;
|
||||
|
||||
// Ensure we don't go below 1 seat
|
||||
if (newQuantity < 1) {
|
||||
throw new Error(
|
||||
`Cannot reduce seats below 1. Current: ${currentQuantity}, Requested change: ${seatIncrement}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Update the subscription with the new quantity and immediate invoicing
|
||||
const updatedSubscription = await stripe.subscriptions.update(
|
||||
stripeSubscriptionId,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: subscriptionItem.id,
|
||||
quantity: newQuantity,
|
||||
},
|
||||
],
|
||||
proration_behavior: "always_invoice", // Accumulate charges for next billing cycle
|
||||
},
|
||||
);
|
||||
|
||||
return updatedSubscription;
|
||||
};
|
||||
|
||||
export { createStripeClient };
|
||||
|
||||
Reference in New Issue
Block a user