mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-13 19:09:49 +00:00
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:
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user