mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02: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,
|
Loader2,
|
||||||
MinusIcon,
|
MinusIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
ShieldCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -141,6 +142,7 @@ export const ShowBilling = () => {
|
|||||||
return isAnnual ? interval === "year" : interval === "month";
|
return isAnnual ? interval === "year" : interval === "month";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isEnterpriseCloud = admin?.user.isEnterpriseCloud ?? false;
|
||||||
const maxServers = admin?.user.serversQuantity ?? 1;
|
const maxServers = admin?.user.serversQuantity ?? 1;
|
||||||
const percentage = ((servers ?? 0) / maxServers) * 100;
|
const percentage = ((servers ?? 0) / maxServers) * 100;
|
||||||
const safePercentage = Math.min(percentage, 100);
|
const safePercentage = Math.min(percentage, 100);
|
||||||
@@ -182,7 +184,7 @@ export const ShowBilling = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full mt-6">
|
<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">
|
<div className="space-y-2 flex flex-col">
|
||||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -203,8 +205,36 @@ export const ShowBilling = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Upgrade: solo para usuarios en plan legacy con nuevos planes disponibles */}
|
||||||
{useNewPricing &&
|
{!isEnterpriseCloud &&
|
||||||
|
useNewPricing &&
|
||||||
data?.currentPlan === "legacy" &&
|
data?.currentPlan === "legacy" &&
|
||||||
data?.subscriptions?.length > 0 && (
|
data?.subscriptions?.length > 0 && (
|
||||||
<div className="rounded-xl border border-border bg-primary/5 p-4 space-y-4 max-w-2xl">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Cambiar plan o cantidad de servidores (usuarios en Hobby o Startup; el portal no permite esto) */}
|
{/* Cambiar plan o cantidad de servidores (usuarios en Hobby o Startup; el portal no permite esto) */}
|
||||||
{useNewPricing &&
|
{!isEnterpriseCloud &&
|
||||||
|
useNewPricing &&
|
||||||
(data?.currentPlan === "hobby" ||
|
(data?.currentPlan === "hobby" ||
|
||||||
data?.currentPlan === "startup") &&
|
data?.currentPlan === "startup") &&
|
||||||
data?.subscriptions?.length > 0 && (
|
data?.subscriptions?.length > 0 && (
|
||||||
@@ -779,17 +810,18 @@ export const ShowBilling = () => {
|
|||||||
Manage Subscription
|
Manage Subscription
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{(data?.subscriptions?.length ?? 0) === 0 && (
|
{!isEnterpriseCloud &&
|
||||||
<Button
|
(data?.subscriptions?.length ?? 0) === 0 && (
|
||||||
className="w-full"
|
<Button
|
||||||
onClick={() =>
|
className="w-full"
|
||||||
handleCheckout("hobby", data!.hobbyProductId!)
|
onClick={() =>
|
||||||
}
|
handleCheckout("hobby", data!.hobbyProductId!)
|
||||||
disabled={hobbyServerQuantity < 1}
|
}
|
||||||
>
|
disabled={hobbyServerQuantity < 1}
|
||||||
Get Started
|
>
|
||||||
</Button>
|
Get Started
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -923,22 +955,24 @@ export const ShowBilling = () => {
|
|||||||
Manage Subscription
|
Manage Subscription
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{(data?.subscriptions?.length ?? 0) === 0 && (
|
{!isEnterpriseCloud &&
|
||||||
<Button
|
(data?.subscriptions?.length ?? 0) === 0 && (
|
||||||
className="w-full"
|
<Button
|
||||||
onClick={() =>
|
className="w-full"
|
||||||
handleCheckout(
|
onClick={() =>
|
||||||
"startup",
|
handleCheckout(
|
||||||
data!.startupProductId!,
|
"startup",
|
||||||
)
|
data!.startupProductId!,
|
||||||
}
|
)
|
||||||
disabled={
|
}
|
||||||
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
disabled={
|
||||||
}
|
startupServerQuantity <
|
||||||
>
|
STARTUP_SERVERS_INCLUDED
|
||||||
Get Started
|
}
|
||||||
</Button>
|
>
|
||||||
)}
|
Get Started
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1143,17 +1177,18 @@ export const ShowBilling = () => {
|
|||||||
Manage Subscription
|
Manage Subscription
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{(data?.subscriptions?.length ?? 0) === 0 && (
|
{!isEnterpriseCloud &&
|
||||||
<Button
|
(data?.subscriptions?.length ?? 0) === 0 && (
|
||||||
className="w-full"
|
<Button
|
||||||
onClick={async () => {
|
className="w-full"
|
||||||
handleCheckout("legacy", product.id);
|
onClick={async () => {
|
||||||
}}
|
handleCheckout("legacy", product.id);
|
||||||
disabled={hobbyServerQuantity < 1}
|
}}
|
||||||
>
|
disabled={hobbyServerQuantity < 1}
|
||||||
Subscribe
|
>
|
||||||
</Button>
|
Subscribe
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0164_slippery_sasquatch.sql
Normal file
1
apps/dokploy/drizzle/0164_slippery_sasquatch.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "isEnterpriseCloud" boolean DEFAULT false NOT NULL;
|
||||||
8305
apps/dokploy/drizzle/meta/0164_snapshot.json
Normal file
8305
apps/dokploy/drizzle/meta/0164_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1149,6 +1149,13 @@
|
|||||||
"when": 1775367585821,
|
"when": 1775367585821,
|
||||||
"tag": "0163_perfect_lethal_legion",
|
"tag": "0163_perfect_lethal_legion",
|
||||||
"breakpoints": true
|
"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 { buffer } from "node:stream/consumers";
|
||||||
import { findUserById, type Server } from "@dokploy/server";
|
import { findUserById, type Server } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
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 type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { organization, server, user } from "@/server/db/schema";
|
import { organization, server, user } from "@/server/db/schema";
|
||||||
@@ -92,13 +92,16 @@ export default async function handler(
|
|||||||
stripeSubscriptionId: session.subscription as string,
|
stripeSubscriptionId: session.subscription as string,
|
||||||
serversQuantity,
|
serversQuantity,
|
||||||
})
|
})
|
||||||
.where(eq(user.id, adminId))
|
.where(and(eq(user.id, adminId), eq(user.isEnterpriseCloud, false)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const admin = await findUserById(adminId);
|
const admin = await findUserById(adminId);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
if (admin.isEnterpriseCloud) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
break;
|
break;
|
||||||
@@ -112,7 +115,12 @@ export default async function handler(
|
|||||||
stripeSubscriptionId: newSubscription.id,
|
stripeSubscriptionId: newSubscription.id,
|
||||||
stripeCustomerId: newSubscription.customer as string,
|
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();
|
.returning();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -127,7 +135,12 @@ export default async function handler(
|
|||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
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(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
@@ -137,6 +150,10 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (admin.isEnterpriseCloud) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -151,6 +168,10 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (admin.isEnterpriseCloud) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (newSubscription.status === "active") {
|
if (newSubscription.status === "active") {
|
||||||
const serversQuantity = getSubscriptionServersQuantity(
|
const serversQuantity = getSubscriptionServersQuantity(
|
||||||
newSubscription?.items?.data ?? [],
|
newSubscription?.items?.data ?? [],
|
||||||
@@ -158,7 +179,12 @@ export default async function handler(
|
|||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({ serversQuantity })
|
.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);
|
await updateServersBasedOnQuantity(admin.id, serversQuantity);
|
||||||
} else {
|
} else {
|
||||||
@@ -166,7 +192,12 @@ export default async function handler(
|
|||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({ serversQuantity: 0 })
|
.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;
|
break;
|
||||||
@@ -191,7 +222,12 @@ export default async function handler(
|
|||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({ serversQuantity })
|
.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(
|
const admin = await findUserByStripeCustomerId(
|
||||||
subscription.customer as string,
|
subscription.customer as string,
|
||||||
@@ -200,6 +236,9 @@ export default async function handler(
|
|||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
if (admin.isEnterpriseCloud) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
break;
|
break;
|
||||||
@@ -219,12 +258,22 @@ export default async function handler(
|
|||||||
if (!admin) {
|
if (!admin) {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (admin.isEnterpriseCloud) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: 0,
|
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);
|
await disableServers(admin.id);
|
||||||
}
|
}
|
||||||
@@ -240,6 +289,10 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (admin.isEnterpriseCloud) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
@@ -248,7 +301,12 @@ export default async function handler(
|
|||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(eq(user.stripeCustomerId, customer.id));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(user.stripeCustomerId, customer.id),
|
||||||
|
eq(user.isEnterpriseCloud, false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const user = pgTable("user", {
|
|||||||
stripeCustomerId: text("stripeCustomerId"),
|
stripeCustomerId: text("stripeCustomerId"),
|
||||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||||
|
isEnterpriseCloud: boolean("isEnterpriseCloud").notNull().default(false),
|
||||||
trustedOrigins: text("trustedOrigins").array(),
|
trustedOrigins: text("trustedOrigins").array(),
|
||||||
bookmarkedTemplates: text("bookmarkedTemplates")
|
bookmarkedTemplates: text("bookmarkedTemplates")
|
||||||
.array()
|
.array()
|
||||||
@@ -92,6 +93,7 @@ const createSchema = createInsertSchema(user, {
|
|||||||
trustedOrigins: true,
|
trustedOrigins: true,
|
||||||
bookmarkedTemplates: true,
|
bookmarkedTemplates: true,
|
||||||
isValidEnterpriseLicense: true,
|
isValidEnterpriseLicense: true,
|
||||||
|
isEnterpriseCloud: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||||
|
|||||||
Reference in New Issue
Block a user