Merge pull request #4158 from Dokploy/feat/prevent-billing-checks-to-enterprise-users

feat: add isEnterpriseCloud field and update billing logic
This commit is contained in:
Mauricio Siu
2026-04-05 00:44:53 -06:00
committed by GitHub
6 changed files with 8458 additions and 50 deletions
@@ -8,6 +8,7 @@ import {
Loader2,
MinusIcon,
PlusIcon,
ShieldCheck,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -141,6 +142,7 @@ export const ShowBilling = () => {
return isAnnual ? interval === "year" : interval === "month";
});
const isEnterpriseCloud = admin?.user.isEnterpriseCloud ?? false;
const maxServers = admin?.user.serversQuantity ?? 1;
const percentage = ((servers ?? 0) / maxServers) * 100;
const safePercentage = Math.min(percentage, 100);
@@ -182,7 +184,7 @@ export const ShowBilling = () => {
</nav>
<div className="flex flex-col gap-4 w-full mt-6">
{admin?.user.stripeSubscriptionId && (
{(admin?.user.stripeSubscriptionId || isEnterpriseCloud) && (
<div className="space-y-2 flex flex-col">
<h3 className="text-lg font-medium">Servers Plan</h3>
<p className="text-sm text-muted-foreground">
@@ -203,8 +205,36 @@ export const ShowBilling = () => {
)}
</div>
)}
{isEnterpriseCloud && (
<div className="flex items-start gap-3 rounded-xl border border-primary/30 bg-primary/5 p-4 max-w-2xl">
<ShieldCheck className="h-6 w-6 text-primary shrink-0 mt-0.5" />
<div className="flex flex-col gap-1">
<h3 className="text-base font-semibold text-foreground">
Enterprise Cloud Plan
</h3>
<p className="text-sm text-muted-foreground">
Your organization is on a managed Enterprise plan. Billing
is handled separately contact your account manager for
any changes.
</p>
{admin?.user.stripeCustomerId && (
<Button
variant="secondary"
className="w-fit mt-2"
onClick={async () => {
const session = await createCustomerPortalSession();
window.open(session.url);
}}
>
Manage Subscription
</Button>
)}
</div>
</div>
)}
{/* Upgrade: solo para usuarios en plan legacy con nuevos planes disponibles */}
{useNewPricing &&
{!isEnterpriseCloud &&
useNewPricing &&
data?.currentPlan === "legacy" &&
data?.subscriptions?.length > 0 && (
<div className="rounded-xl border border-border bg-primary/5 p-4 space-y-4 max-w-2xl">
@@ -394,7 +424,8 @@ export const ShowBilling = () => {
</div>
)}
{/* Cambiar plan o cantidad de servidores (usuarios en Hobby o Startup; el portal no permite esto) */}
{useNewPricing &&
{!isEnterpriseCloud &&
useNewPricing &&
(data?.currentPlan === "hobby" ||
data?.currentPlan === "startup") &&
data?.subscriptions?.length > 0 && (
@@ -779,17 +810,18 @@ export const ShowBilling = () => {
Manage Subscription
</Button>
)}
{(data?.subscriptions?.length ?? 0) === 0 && (
<Button
className="w-full"
onClick={() =>
handleCheckout("hobby", data!.hobbyProductId!)
}
disabled={hobbyServerQuantity < 1}
>
Get Started
</Button>
)}
{!isEnterpriseCloud &&
(data?.subscriptions?.length ?? 0) === 0 && (
<Button
className="w-full"
onClick={() =>
handleCheckout("hobby", data!.hobbyProductId!)
}
disabled={hobbyServerQuantity < 1}
>
Get Started
</Button>
)}
</div>
</div>
</section>
@@ -923,22 +955,24 @@ export const ShowBilling = () => {
Manage Subscription
</Button>
)}
{(data?.subscriptions?.length ?? 0) === 0 && (
<Button
className="w-full"
onClick={() =>
handleCheckout(
"startup",
data!.startupProductId!,
)
}
disabled={
startupServerQuantity < STARTUP_SERVERS_INCLUDED
}
>
Get Started
</Button>
)}
{!isEnterpriseCloud &&
(data?.subscriptions?.length ?? 0) === 0 && (
<Button
className="w-full"
onClick={() =>
handleCheckout(
"startup",
data!.startupProductId!,
)
}
disabled={
startupServerQuantity <
STARTUP_SERVERS_INCLUDED
}
>
Get Started
</Button>
)}
</div>
</div>
</section>
@@ -1143,17 +1177,18 @@ export const ShowBilling = () => {
Manage Subscription
</Button>
)}
{(data?.subscriptions?.length ?? 0) === 0 && (
<Button
className="w-full"
onClick={async () => {
handleCheckout("legacy", product.id);
}}
disabled={hobbyServerQuantity < 1}
>
Subscribe
</Button>
)}
{!isEnterpriseCloud &&
(data?.subscriptions?.length ?? 0) === 0 && (
<Button
className="w-full"
onClick={async () => {
handleCheckout("legacy", product.id);
}}
disabled={hobbyServerQuantity < 1}
>
Subscribe
</Button>
)}
</div>
</div>
</section>
@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "isEnterpriseCloud" boolean DEFAULT false NOT NULL;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1149,6 +1149,13 @@
"when": 1775367585821,
"tag": "0163_perfect_lethal_legion",
"breakpoints": true
},
{
"idx": 164,
"version": "7",
"when": 1775369858244,
"tag": "0164_slippery_sasquatch",
"breakpoints": true
}
]
}
+67 -9
View File
@@ -1,7 +1,7 @@
import { buffer } from "node:stream/consumers";
import { findUserById, type Server } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { asc, eq } from "drizzle-orm";
import { and, asc, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import { organization, server, user } from "@/server/db/schema";
@@ -92,13 +92,16 @@ export default async function handler(
stripeSubscriptionId: session.subscription as string,
serversQuantity,
})
.where(eq(user.id, adminId))
.where(and(eq(user.id, adminId), eq(user.isEnterpriseCloud, false)))
.returning();
const admin = await findUserById(adminId);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
if (admin.isEnterpriseCloud) {
break;
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
break;
@@ -112,7 +115,12 @@ export default async function handler(
stripeSubscriptionId: newSubscription.id,
stripeCustomerId: newSubscription.customer as string,
})
.where(eq(user.stripeCustomerId, newSubscription.customer as string))
.where(
and(
eq(user.stripeCustomerId, newSubscription.customer as string),
eq(user.isEnterpriseCloud, false),
),
)
.returning();
break;
@@ -127,7 +135,12 @@ export default async function handler(
stripeSubscriptionId: null,
serversQuantity: 0,
})
.where(eq(user.stripeCustomerId, newSubscription.customer as string));
.where(
and(
eq(user.stripeCustomerId, newSubscription.customer as string),
eq(user.isEnterpriseCloud, false),
),
);
const admin = await findUserByStripeCustomerId(
newSubscription.customer as string,
@@ -137,6 +150,10 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
if (admin.isEnterpriseCloud) {
break;
}
await disableServers(admin.id);
break;
}
@@ -151,6 +168,10 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
if (admin.isEnterpriseCloud) {
break;
}
if (newSubscription.status === "active") {
const serversQuantity = getSubscriptionServersQuantity(
newSubscription?.items?.data ?? [],
@@ -158,7 +179,12 @@ export default async function handler(
await db
.update(user)
.set({ serversQuantity })
.where(eq(user.stripeCustomerId, newSubscription.customer as string));
.where(
and(
eq(user.stripeCustomerId, newSubscription.customer as string),
eq(user.isEnterpriseCloud, false),
),
);
await updateServersBasedOnQuantity(admin.id, serversQuantity);
} else {
@@ -166,7 +192,12 @@ export default async function handler(
await db
.update(user)
.set({ serversQuantity: 0 })
.where(eq(user.stripeCustomerId, newSubscription.customer as string));
.where(
and(
eq(user.stripeCustomerId, newSubscription.customer as string),
eq(user.isEnterpriseCloud, false),
),
);
}
break;
@@ -191,7 +222,12 @@ export default async function handler(
await db
.update(user)
.set({ serversQuantity })
.where(eq(user.stripeCustomerId, subscription.customer as string));
.where(
and(
eq(user.stripeCustomerId, subscription.customer as string),
eq(user.isEnterpriseCloud, false),
),
);
const admin = await findUserByStripeCustomerId(
subscription.customer as string,
@@ -200,6 +236,9 @@ export default async function handler(
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
if (admin.isEnterpriseCloud) {
break;
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
break;
@@ -219,12 +258,22 @@ export default async function handler(
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
if (admin.isEnterpriseCloud) {
break;
}
await db
.update(user)
.set({
serversQuantity: 0,
})
.where(eq(user.stripeCustomerId, newInvoice.customer as string));
.where(
and(
eq(user.stripeCustomerId, newInvoice.customer as string),
eq(user.isEnterpriseCloud, false),
),
);
await disableServers(admin.id);
}
@@ -240,6 +289,10 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
if (admin.isEnterpriseCloud) {
break;
}
await disableServers(admin.id);
await db
.update(user)
@@ -248,7 +301,12 @@ export default async function handler(
stripeSubscriptionId: null,
serversQuantity: 0,
})
.where(eq(user.stripeCustomerId, customer.id));
.where(
and(
eq(user.stripeCustomerId, customer.id),
eq(user.isEnterpriseCloud, false),
),
);
break;
}
+2
View File
@@ -65,6 +65,7 @@ export const user = pgTable("user", {
stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
isEnterpriseCloud: boolean("isEnterpriseCloud").notNull().default(false),
trustedOrigins: text("trustedOrigins").array(),
bookmarkedTemplates: text("bookmarkedTemplates")
.array()
@@ -92,6 +93,7 @@ const createSchema = createInsertSchema(user, {
trustedOrigins: true,
bookmarkedTemplates: true,
isValidEnterpriseLicense: true,
isEnterpriseCloud: true,
});
export const apiCreateUserInvitation = createSchema.pick({}).extend({