mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-27 15:26:41 +00:00
Rename tiers and get working
This commit is contained in:
@@ -7,8 +7,8 @@ services:
|
|||||||
POSTGRES_DB: postgres # Default database name
|
POSTGRES_DB: postgres # Default database name
|
||||||
POSTGRES_USER: postgres # Default user
|
POSTGRES_USER: postgres # Default user
|
||||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||||
volumes:
|
# volumes:
|
||||||
- ./config/postgres:/var/lib/postgresql/data
|
# - ./config/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432" # Map host port 5432 to container port 5432
|
- "5432:5432" # Map host port 5432 to container port 5432
|
||||||
restart: no
|
restart: no
|
||||||
|
|||||||
14
drizzle.config.ts
Normal file
14
drizzle.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const schema = [path.join("server", "db", "pg", "schema")];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: "postgresql",
|
||||||
|
schema: schema,
|
||||||
|
out: path.join("server", "migrations"),
|
||||||
|
verbose: true,
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL as string
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -82,12 +82,14 @@ export const subscriptions = pgTable("subscriptions", {
|
|||||||
canceledAt: bigint("canceledAt", { mode: "number" }),
|
canceledAt: bigint("canceledAt", { mode: "number" }),
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }),
|
updatedAt: bigint("updatedAt", { mode: "number" }),
|
||||||
|
version: integer("version"),
|
||||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
||||||
type: varchar("type", { length: 50 }) // home_lab, starter, scale, or license
|
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = pgTable("subscriptionItems", {
|
export const subscriptionItems = pgTable("subscriptionItems", {
|
||||||
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
||||||
|
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", { length: 255 }),
|
||||||
subscriptionId: varchar("subscriptionId", { length: 255 })
|
subscriptionId: varchar("subscriptionId", { length: 255 })
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => subscriptions.subscriptionId, {
|
.references(() => subscriptions.subscriptionId, {
|
||||||
|
|||||||
@@ -70,8 +70,9 @@ export const subscriptions = sqliteTable("subscriptions", {
|
|||||||
canceledAt: integer("canceledAt"),
|
canceledAt: integer("canceledAt"),
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
updatedAt: integer("updatedAt"),
|
updatedAt: integer("updatedAt"),
|
||||||
|
version: integer("version"),
|
||||||
billingCycleAnchor: integer("billingCycleAnchor"),
|
billingCycleAnchor: integer("billingCycleAnchor"),
|
||||||
type: text("type") // home_lab, starter, scale, or license
|
type: text("type") // tier1, tier2, tier3, or license
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||||
|
|||||||
@@ -7,9 +7,29 @@ export enum FeatureId {
|
|||||||
EGRESS_DATA_MB = "egressDataMb",
|
EGRESS_DATA_MB = "egressDataMb",
|
||||||
DOMAINS = "domains",
|
DOMAINS = "domains",
|
||||||
REMOTE_EXIT_NODES = "remoteExitNodes",
|
REMOTE_EXIT_NODES = "remoteExitNodes",
|
||||||
HOME_LAB = "home_lab"
|
TIER1 = "tier1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
|
||||||
|
switch (featureId) {
|
||||||
|
case FeatureId.USERS:
|
||||||
|
return "Users";
|
||||||
|
case FeatureId.SITES:
|
||||||
|
return "Sites";
|
||||||
|
case FeatureId.EGRESS_DATA_MB:
|
||||||
|
return "Egress Data (MB)";
|
||||||
|
case FeatureId.DOMAINS:
|
||||||
|
return "Domains";
|
||||||
|
case FeatureId.REMOTE_EXIT_NODES:
|
||||||
|
return "Remote Exit Nodes";
|
||||||
|
case FeatureId.TIER1:
|
||||||
|
return "Home Lab";
|
||||||
|
default:
|
||||||
|
return featureId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is from the old system
|
||||||
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
|
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
|
||||||
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
||||||
};
|
};
|
||||||
@@ -40,11 +60,11 @@ export function getFeatureIdByMetricId(
|
|||||||
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
|
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
|
||||||
|
|
||||||
export const homeLabFeaturePriceSet: FeaturePriceSet = {
|
export const homeLabFeaturePriceSet: FeaturePriceSet = {
|
||||||
[FeatureId.HOME_LAB]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const homeLabFeaturePriceSetSandbox: FeaturePriceSet = {
|
export const homeLabFeaturePriceSetSandbox: FeaturePriceSet = {
|
||||||
[FeatureId.HOME_LAB]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getHomeLabFeaturePriceSet(): FeaturePriceSet {
|
export function getHomeLabFeaturePriceSet(): FeaturePriceSet {
|
||||||
@@ -58,11 +78,11 @@ export function getHomeLabFeaturePriceSet(): FeaturePriceSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const starterFeaturePriceSet: FeaturePriceSet = {
|
export const tier2FeaturePriceSet: FeaturePriceSet = {
|
||||||
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const starterFeaturePriceSetSandbox: FeaturePriceSet = {
|
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||||
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,17 +91,17 @@ export function getStarterFeaturePriceSet(): FeaturePriceSet {
|
|||||||
process.env.ENVIRONMENT == "prod" &&
|
process.env.ENVIRONMENT == "prod" &&
|
||||||
process.env.SANDBOX_MODE !== "true"
|
process.env.SANDBOX_MODE !== "true"
|
||||||
) {
|
) {
|
||||||
return starterFeaturePriceSet;
|
return tier2FeaturePriceSet;
|
||||||
} else {
|
} else {
|
||||||
return starterFeaturePriceSetSandbox;
|
return tier2FeaturePriceSetSandbox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scaleFeaturePriceSet: FeaturePriceSet = {
|
export const tier3FeaturePriceSet: FeaturePriceSet = {
|
||||||
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scaleFeaturePriceSetSandbox: FeaturePriceSet = {
|
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||||
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,9 +110,9 @@ export function getScaleFeaturePriceSet(): FeaturePriceSet {
|
|||||||
process.env.ENVIRONMENT == "prod" &&
|
process.env.ENVIRONMENT == "prod" &&
|
||||||
process.env.SANDBOX_MODE !== "true"
|
process.env.SANDBOX_MODE !== "true"
|
||||||
) {
|
) {
|
||||||
return scaleFeaturePriceSet;
|
return tier3FeaturePriceSet;
|
||||||
} else {
|
} else {
|
||||||
return scaleFeaturePriceSetSandbox;
|
return tier3FeaturePriceSetSandbox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +120,14 @@ export async function getLineItems(
|
|||||||
featurePriceSet: FeaturePriceSet,
|
featurePriceSet: FeaturePriceSet,
|
||||||
orgId: string,
|
orgId: string,
|
||||||
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||||
const users = await usageService.getUsageDaily(orgId, FeatureId.USERS);
|
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||||
|
|
||||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||||
let quantity: number | undefined;
|
let quantity: number | undefined;
|
||||||
|
|
||||||
if (featureId === FeatureId.USERS) {
|
if (featureId === FeatureId.USERS) {
|
||||||
quantity = users?.instantaneousValue || 1;
|
quantity = users?.instantaneousValue || 1;
|
||||||
} else if (featureId === FeatureId.HOME_LAB) {
|
} else if (featureId === FeatureId.TIER1) {
|
||||||
quantity = 1;
|
quantity = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const homeLabLimitSet: LimitSet = {
|
|||||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home lab limit" }
|
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home lab limit" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export const starterLimitSet: LimitSet = {
|
export const tier2LimitSet: LimitSet = {
|
||||||
[FeatureId.SITES]: {
|
[FeatureId.SITES]: {
|
||||||
value: 10,
|
value: 10,
|
||||||
description: "Starter limit"
|
description: "Starter limit"
|
||||||
@@ -60,7 +60,7 @@ export const starterLimitSet: LimitSet = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scaleLimitSet: LimitSet = {
|
export const tier3LimitSet: LimitSet = {
|
||||||
[FeatureId.SITES]: {
|
[FeatureId.SITES]: {
|
||||||
value: 10,
|
value: 10,
|
||||||
description: "Scale limit"
|
description: "Scale limit"
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export class UsageService {
|
|||||||
|
|
||||||
this.bucketName = process.env.S3_BUCKET || undefined;
|
this.bucketName = process.env.S3_BUCKET || undefined;
|
||||||
|
|
||||||
if ( // Only set up event uploading if usage reporting is enabled and bucket name is configured
|
if (
|
||||||
|
// Only set up event uploading if usage reporting is enabled and bucket name is configured
|
||||||
privateConfig.getRawPrivateConfig().flags.usage_reporting &&
|
privateConfig.getRawPrivateConfig().flags.usage_reporting &&
|
||||||
this.bucketName
|
this.bucketName
|
||||||
) {
|
) {
|
||||||
@@ -220,7 +221,7 @@ export class UsageService {
|
|||||||
return new Date(date * 1000).toISOString().split("T")[0];
|
return new Date(date * 1000).toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDaily(
|
async updateCount(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId,
|
featureId: FeatureId,
|
||||||
value?: number,
|
value?: number,
|
||||||
@@ -246,8 +247,6 @@ export class UsageService {
|
|||||||
value = this.truncateValue(value);
|
value = this.truncateValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = this.getTodayDateString();
|
|
||||||
|
|
||||||
let currentUsage: Usage | null = null;
|
let currentUsage: Usage | null = null;
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
@@ -261,57 +260,23 @@ export class UsageService {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (currentUsage) {
|
if (currentUsage) {
|
||||||
const lastUpdateDate = this.getDateString(
|
await trx
|
||||||
currentUsage.updatedAt
|
.update(usage)
|
||||||
);
|
.set({
|
||||||
const currentRunningTotal = currentUsage.latestValue;
|
instantaneousValue: value,
|
||||||
const lastDailyValue = currentUsage.instantaneousValue || 0;
|
updatedAt: Math.floor(Date.now() / 1000)
|
||||||
|
})
|
||||||
if (value == undefined || value === null) {
|
.where(eq(usage.usageId, usageId));
|
||||||
value = currentUsage.instantaneousValue || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastUpdateDate === today) {
|
|
||||||
// Same day update: replace the daily value
|
|
||||||
// Remove old daily value from running total, add new value
|
|
||||||
const newRunningTotal = this.truncateValue(
|
|
||||||
currentRunningTotal - lastDailyValue + value
|
|
||||||
);
|
|
||||||
|
|
||||||
await trx
|
|
||||||
.update(usage)
|
|
||||||
.set({
|
|
||||||
latestValue: newRunningTotal,
|
|
||||||
instantaneousValue: value,
|
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
|
||||||
})
|
|
||||||
.where(eq(usage.usageId, usageId));
|
|
||||||
} else {
|
|
||||||
// New day: add to running total
|
|
||||||
const newRunningTotal = this.truncateValue(
|
|
||||||
currentRunningTotal + value
|
|
||||||
);
|
|
||||||
|
|
||||||
await trx
|
|
||||||
.update(usage)
|
|
||||||
.set({
|
|
||||||
latestValue: newRunningTotal,
|
|
||||||
instantaneousValue: value,
|
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
|
||||||
})
|
|
||||||
.where(eq(usage.usageId, usageId));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// First record for this meter
|
// First record for this meter
|
||||||
const meterId = getFeatureMeterId(featureId);
|
const meterId = getFeatureMeterId(featureId);
|
||||||
const truncatedValue = this.truncateValue(value || 0);
|
|
||||||
await trx.insert(usage).values({
|
await trx.insert(usage).values({
|
||||||
usageId,
|
usageId,
|
||||||
featureId,
|
featureId,
|
||||||
orgId,
|
orgId,
|
||||||
meterId,
|
meterId,
|
||||||
instantaneousValue: truncatedValue,
|
instantaneousValue: value || 0,
|
||||||
latestValue: truncatedValue,
|
latestValue: value || 0,
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
updatedAt: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -322,7 +287,7 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to update daily usage for ${orgId}/${featureId}:`,
|
`Failed to update count usage for ${orgId}/${featureId}:`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -542,17 +507,6 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUsageDaily(
|
|
||||||
orgId: string,
|
|
||||||
featureId: FeatureId
|
|
||||||
): Promise<Usage | null> {
|
|
||||||
if (noop()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
|
||||||
return this.getUsage(orgId, featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async forceUpload(): Promise<void> {
|
public async forceUpload(): Promise<void> {
|
||||||
if (this.events.length > 0) {
|
if (this.events.length > 0) {
|
||||||
// Force upload regardless of time
|
// Force upload regardless of time
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export async function createUserAccountOrg(
|
|||||||
const customerId = await createCustomer(orgId, userEmail);
|
const customerId = await createCustomer(orgId, userEmail);
|
||||||
|
|
||||||
if (customerId) {
|
if (customerId) {
|
||||||
await usageService.updateDaily(orgId, FeatureId.USERS, 1, customerId); // Only 1 because we are crating the org
|
await usageService.updateCount(orgId, FeatureId.USERS, 1, customerId); // Only 1 because we are crating the org
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { eq, and, ne } from "drizzle-orm";
|
|||||||
|
|
||||||
export async function getOrgTierData(
|
export async function getOrgTierData(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<{ tier: "home_lab" | "starter" | "scale" | null; active: boolean }> {
|
): Promise<{ tier: "tier1" | "tier2" | "tier3" | null; active: boolean }> {
|
||||||
let tier: "home_lab" | "starter" | "scale" | null = null;
|
let tier: "tier1" | "tier2" | "tier3" | null = null;
|
||||||
let active = false;
|
let active = false;
|
||||||
|
|
||||||
if (build !== "saas") {
|
if (build !== "saas") {
|
||||||
@@ -50,9 +50,9 @@ export async function getOrgTierData(
|
|||||||
if (subscription) {
|
if (subscription) {
|
||||||
// Validate that subscription.type is one of the expected tier values
|
// Validate that subscription.type is one of the expected tier values
|
||||||
if (
|
if (
|
||||||
subscription.type === "home_lab" ||
|
subscription.type === "tier1" ||
|
||||||
subscription.type === "starter" ||
|
subscription.type === "tier2" ||
|
||||||
subscription.type === "scale"
|
subscription.type === "tier3"
|
||||||
) {
|
) {
|
||||||
tier = subscription.type;
|
tier = subscription.type;
|
||||||
active = true;
|
active = true;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
|
|||||||
|
|
||||||
if (build === "saas") {
|
if (build === "saas") {
|
||||||
const { tier, active } = await getOrgTierData(orgId);
|
const { tier, active } = await getOrgTierData(orgId);
|
||||||
return (tier == "home_lab" || tier == "starter" || tier == "scale") && active;
|
return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { getOrgTierData } from "#private/lib/billing";
|
|||||||
export async function isSubscribed(orgId: string): Promise<boolean> {
|
export async function isSubscribed(orgId: string): Promise<boolean> {
|
||||||
if (build === "saas") {
|
if (build === "saas") {
|
||||||
const { tier, active } = await getOrgTierData(orgId);
|
const { tier, active } = await getOrgTierData(orgId);
|
||||||
return (tier == "home_lab" || tier == "starter" || tier == "scale") && active;
|
return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export async function verifyValidSubscription(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { tier, active } = await getOrgTierData(orgId);
|
const { tier, active } = await getOrgTierData(orgId);
|
||||||
if ((tier == "home_lab" || tier == "starter" || tier == "scale") && active) {
|
if ((tier == "tier1" || tier == "tier2" || tier == "tier3") && active) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const changeTierSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const changeTierBodySchema = z.strictObject({
|
const changeTierBodySchema = z.strictObject({
|
||||||
tier: z.enum(["home_lab", "starter", "scale"])
|
tier: z.enum(["tier1", "tier2", "tier3"])
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function changeTier(
|
export async function changeTier(
|
||||||
@@ -93,9 +93,9 @@ export async function changeTier(
|
|||||||
eq(subscriptions.customerId, customer.customerId),
|
eq(subscriptions.customerId, customer.customerId),
|
||||||
eq(subscriptions.status, "active"),
|
eq(subscriptions.status, "active"),
|
||||||
or(
|
or(
|
||||||
eq(subscriptions.type, "home_lab"),
|
eq(subscriptions.type, "tier1"),
|
||||||
eq(subscriptions.type, "starter"),
|
eq(subscriptions.type, "tier2"),
|
||||||
eq(subscriptions.type, "scale")
|
eq(subscriptions.type, "tier3")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -112,11 +112,11 @@ export async function changeTier(
|
|||||||
|
|
||||||
// Get the target tier's price set
|
// Get the target tier's price set
|
||||||
let targetPriceSet: FeaturePriceSet;
|
let targetPriceSet: FeaturePriceSet;
|
||||||
if (tier === "home_lab") {
|
if (tier === "tier1") {
|
||||||
targetPriceSet = getHomeLabFeaturePriceSet();
|
targetPriceSet = getHomeLabFeaturePriceSet();
|
||||||
} else if (tier === "starter") {
|
} else if (tier === "tier2") {
|
||||||
targetPriceSet = getStarterFeaturePriceSet();
|
targetPriceSet = getStarterFeaturePriceSet();
|
||||||
} else if (tier === "scale") {
|
} else if (tier === "tier3") {
|
||||||
targetPriceSet = getScaleFeaturePriceSet();
|
targetPriceSet = getScaleFeaturePriceSet();
|
||||||
} else {
|
} else {
|
||||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid tier"));
|
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid tier"));
|
||||||
@@ -148,11 +148,11 @@ export async function changeTier(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Determine if we're switching between different products
|
// Determine if we're switching between different products
|
||||||
// home_lab uses HOME_LAB product, starter/scale use USERS product
|
// tier1 uses TIER1 product, tier2/tier3 use USERS product
|
||||||
const currentTier = subscription.type;
|
const currentTier = subscription.type;
|
||||||
const switchingProducts =
|
const switchingProducts =
|
||||||
(currentTier === "home_lab" && (tier === "starter" || tier === "scale")) ||
|
(currentTier === "tier1" && (tier === "tier2" || tier === "tier3")) ||
|
||||||
((currentTier === "starter" || currentTier === "scale") && tier === "home_lab");
|
((currentTier === "tier2" || currentTier === "tier3") && tier === "tier1");
|
||||||
|
|
||||||
let updatedSubscription;
|
let updatedSubscription;
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ export async function changeTier(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Same product, different price tier (starter <-> scale)
|
// Same product, different price tier (tier2 <-> tier3)
|
||||||
// We can simply update the price
|
// We can simply update the price
|
||||||
logger.info(
|
logger.info(
|
||||||
`Updating price from ${currentTier} to ${tier} for subscription ${subscription.subscriptionId}`
|
`Updating price from ${currentTier} to ${tier} for subscription ${subscription.subscriptionId}`
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const createCheckoutSessionSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createCheckoutSessionBodySchema = z.strictObject({
|
const createCheckoutSessionBodySchema = z.strictObject({
|
||||||
tier: z.enum(["home_lab", "starter", "scale"]),
|
tier: z.enum(["tier1", "tier2", "tier3"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function createCheckoutSession(
|
export async function createCheckoutSession(
|
||||||
@@ -83,11 +83,11 @@ export async function createCheckoutSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
|
let lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
|
||||||
if (tier === "home_lab") {
|
if (tier === "tier1") {
|
||||||
lineItems = await getLineItems(getHomeLabFeaturePriceSet(), orgId);
|
lineItems = await getLineItems(getHomeLabFeaturePriceSet(), orgId);
|
||||||
} else if (tier === "starter") {
|
} else if (tier === "tier2") {
|
||||||
lineItems = await getLineItems(getStarterFeaturePriceSet(), orgId);
|
lineItems = await getLineItems(getStarterFeaturePriceSet(), orgId);
|
||||||
} else if (tier === "scale") {
|
} else if (tier === "tier3") {
|
||||||
lineItems = await getLineItems(getScaleFeaturePriceSet(), orgId);
|
lineItems = await getLineItems(getScaleFeaturePriceSet(), orgId);
|
||||||
} else {
|
} else {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -78,16 +78,10 @@ export async function getOrgUsage(
|
|||||||
// Get usage for org
|
// Get usage for org
|
||||||
const usageData = [];
|
const usageData = [];
|
||||||
|
|
||||||
const sites = await usageService.getUsage(
|
const sites = await usageService.getUsage(orgId, FeatureId.SITES);
|
||||||
orgId,
|
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||||
FeatureId.SITES
|
const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS);
|
||||||
);
|
const remoteExitNodes = await usageService.getUsage(
|
||||||
const users = await usageService.getUsageDaily(orgId, FeatureId.USERS);
|
|
||||||
const domains = await usageService.getUsageDaily(
|
|
||||||
orgId,
|
|
||||||
FeatureId.DOMAINS
|
|
||||||
);
|
|
||||||
const remoteExitNodes = await usageService.getUsageDaily(
|
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.REMOTE_EXIT_NODES
|
FeatureId.REMOTE_EXIT_NODES
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from "@server/lib/billing/features";
|
} from "@server/lib/billing/features";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
export type SubscriptionType = "home_lab" | "starter" | "scale" | "license";
|
export type SubscriptionType = "tier1" | "tier2" | "tier3" | "license";
|
||||||
|
|
||||||
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): SubscriptionType | null {
|
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): SubscriptionType | null {
|
||||||
// Determine subscription type by checking subscription items
|
// Determine subscription type by checking subscription items
|
||||||
@@ -41,21 +41,21 @@ export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription
|
|||||||
// Check if price ID matches home lab tier
|
// Check if price ID matches home lab tier
|
||||||
const homeLabPrices = Object.values(getHomeLabFeaturePriceSet());
|
const homeLabPrices = Object.values(getHomeLabFeaturePriceSet());
|
||||||
if (homeLabPrices.includes(priceId)) {
|
if (homeLabPrices.includes(priceId)) {
|
||||||
return "home_lab";
|
return "tier1";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if price ID matches starter tier
|
// Check if price ID matches tier2 tier
|
||||||
const starterPrices = Object.values(getStarterFeaturePriceSet());
|
const tier2Prices = Object.values(getStarterFeaturePriceSet());
|
||||||
if (starterPrices.includes(priceId)) {
|
if (tier2Prices.includes(priceId)) {
|
||||||
return "starter";
|
return "tier2";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if price ID matches scale tier
|
// Check if price ID matches tier3 tier
|
||||||
const scalePrices = Object.values(getScaleFeaturePriceSet());
|
const tier3Prices = Object.values(getScaleFeaturePriceSet());
|
||||||
if (scalePrices.includes(priceId)) {
|
if (tier3Prices.includes(priceId)) {
|
||||||
return "scale";
|
return "tier3";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export async function handleSubscriptionCreated(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
stripeSubscriptionItemId: item.id,
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
planId: item.plan.id,
|
planId: item.plan.id,
|
||||||
priceId: item.price.id,
|
priceId: item.price.id,
|
||||||
@@ -132,7 +133,7 @@ export async function handleSubscriptionCreated(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "home_lab" || type === "starter" || type === "scale") {
|
if (type === "tier1" || type === "tier2" || type === "tier3") {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
|
`Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export async function handleSubscriptionDeleted(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = getSubType(fullSubscription);
|
const type = getSubType(fullSubscription);
|
||||||
if (type == "home_lab" || type == "starter" || type == "scale") {
|
if (type == "tier1" || type == "tier2" || type == "tier3") {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`
|
`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export async function handleSubscriptionUpdated(
|
|||||||
// Upsert subscription items
|
// Upsert subscription items
|
||||||
if (Array.isArray(fullSubscription.items?.data)) {
|
if (Array.isArray(fullSubscription.items?.data)) {
|
||||||
const itemsToUpsert = fullSubscription.items.data.map((item) => ({
|
const itemsToUpsert = fullSubscription.items.data.map((item) => ({
|
||||||
|
stripeSubscriptionItemId: item.id,
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
planId: item.plan.id,
|
planId: item.plan.id,
|
||||||
priceId: item.price.id,
|
priceId: item.price.id,
|
||||||
@@ -237,7 +238,7 @@ export async function handleSubscriptionUpdated(
|
|||||||
}
|
}
|
||||||
// --- end usage update ---
|
// --- end usage update ---
|
||||||
|
|
||||||
if (type === "home_lab" || type === "starter" || type === "scale") {
|
if (type === "tier1" || type === "tier2" || type === "tier3") {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
|
`Handling SAAS subscription lifecycle for org ${customer.orgId} with type ${type}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
import {
|
import {
|
||||||
freeLimitSet,
|
freeLimitSet,
|
||||||
homeLabLimitSet,
|
homeLabLimitSet,
|
||||||
starterLimitSet,
|
tier2LimitSet,
|
||||||
scaleLimitSet,
|
tier3LimitSet,
|
||||||
limitsService,
|
limitsService,
|
||||||
LimitSet
|
LimitSet
|
||||||
} from "@server/lib/billing";
|
} from "@server/lib/billing";
|
||||||
@@ -24,16 +24,16 @@ import { SubscriptionType } from "./hooks/getSubType";
|
|||||||
|
|
||||||
function getLimitSetForSubscriptionType(subType: SubscriptionType | null): LimitSet {
|
function getLimitSetForSubscriptionType(subType: SubscriptionType | null): LimitSet {
|
||||||
switch (subType) {
|
switch (subType) {
|
||||||
case "home_lab":
|
case "tier1":
|
||||||
return homeLabLimitSet;
|
return homeLabLimitSet;
|
||||||
case "starter":
|
case "tier2":
|
||||||
return starterLimitSet;
|
return tier2LimitSet;
|
||||||
case "scale":
|
case "tier3":
|
||||||
return scaleLimitSet;
|
return tier3LimitSet;
|
||||||
case "license":
|
case "license":
|
||||||
// License subscriptions use starter limits by default
|
// License subscriptions use tier2 limits by default
|
||||||
// This can be adjusted based on your business logic
|
// This can be adjusted based on your business logic
|
||||||
return starterLimitSet;
|
return tier2LimitSet;
|
||||||
default:
|
default:
|
||||||
return freeLimitSet;
|
return freeLimitSet;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export async function createRemoteExitNode(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numExitNodeOrgs) {
|
if (numExitNodeOrgs) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.REMOTE_EXIT_NODES,
|
FeatureId.REMOTE_EXIT_NODES,
|
||||||
numExitNodeOrgs.length
|
numExitNodeOrgs.length
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export async function deleteRemoteExitNode(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numExitNodeOrgs) {
|
if (numExitNodeOrgs) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.REMOTE_EXIT_NODES,
|
FeatureId.REMOTE_EXIT_NODES,
|
||||||
numExitNodeOrgs.length
|
numExitNodeOrgs.length
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export async function createOrgDomain(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numOrgDomains) {
|
if (numOrgDomains) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.DOMAINS,
|
FeatureId.DOMAINS,
|
||||||
numOrgDomains.length
|
numOrgDomains.length
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export async function deleteAccountDomain(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numOrgDomains) {
|
if (numOrgDomains) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.DOMAINS,
|
FeatureId.DOMAINS,
|
||||||
numOrgDomains.length
|
numOrgDomains.length
|
||||||
|
|||||||
@@ -587,7 +587,7 @@ export async function validateOidcCallback(
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const orgCount of orgUserCounts) {
|
for (const orgCount of orgUserCounts) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgCount.orgId,
|
orgCount.orgId,
|
||||||
FeatureId.USERS,
|
FeatureId.USERS,
|
||||||
orgCount.userCount
|
orgCount.userCount
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
FeatureId.EGRESS_DATA_MB
|
FeatureId.EGRESS_DATA_MB
|
||||||
);
|
);
|
||||||
|
|
||||||
// Do we need to check the users and domains daily limits here?
|
// Do we need to check the users and domains count limits here?
|
||||||
// const rejectUsers = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.USERS);
|
// const rejectUsers = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.USERS);
|
||||||
// const rejectDomains = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.DOMAINS);
|
// const rejectDomains = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.DOMAINS);
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export async function createOrg(
|
|||||||
// make sure we have the stripe customer
|
// make sure we have the stripe customer
|
||||||
const customerId = await createCustomer(orgId, req.user?.email);
|
const customerId = await createCustomer(orgId, req.user?.email);
|
||||||
if (customerId) {
|
if (customerId) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.USERS,
|
FeatureId.USERS,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ export async function createSite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numSites) {
|
if (numSites) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.SITES,
|
FeatureId.SITES,
|
||||||
numSites.length
|
numSites.length
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export async function deleteSite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numSites) {
|
if (numSites) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
site.orgId,
|
site.orgId,
|
||||||
FeatureId.SITES,
|
FeatureId.SITES,
|
||||||
numSites.length
|
numSites.length
|
||||||
|
|||||||
@@ -155,17 +155,19 @@ export async function acceptInvite(
|
|||||||
.delete(userInvites)
|
.delete(userInvites)
|
||||||
.where(eq(userInvites.inviteId, inviteId));
|
.where(eq(userInvites.inviteId, inviteId));
|
||||||
|
|
||||||
|
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
|
||||||
|
|
||||||
// Get the total number of users in the org now
|
// Get the total number of users in the org now
|
||||||
totalUsers = await db
|
totalUsers = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(eq(userOrgs.orgId, existingInvite.orgId));
|
.where(eq(userOrgs.orgId, existingInvite.orgId));
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
|
logger.debug(`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}. Total users in org: ${totalUsers.length}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (totalUsers) {
|
if (totalUsers) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
existingInvite.orgId,
|
existingInvite.orgId,
|
||||||
FeatureId.USERS,
|
FeatureId.USERS,
|
||||||
totalUsers.length
|
totalUsers.length
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export async function createOrgUser(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (orgUsers) {
|
if (orgUsers) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.USERS,
|
FeatureId.USERS,
|
||||||
orgUsers.length
|
orgUsers.length
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export async function removeUserOrg(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (userCount) {
|
if (userCount) {
|
||||||
await usageService.updateDaily(
|
await usageService.updateCount(
|
||||||
orgId,
|
orgId,
|
||||||
FeatureId.USERS,
|
FeatureId.USERS,
|
||||||
userCount.length
|
userCount.length
|
||||||
|
|||||||
162
server/setup/migrations.ts
Normal file
162
server/setup/migrations.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#! /usr/bin/env node
|
||||||
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
|
import { db } from "../db/pg";
|
||||||
|
import semver from "semver";
|
||||||
|
import { versionMigrations } from "../db/pg";
|
||||||
|
import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
||||||
|
import path from "path";
|
||||||
|
import m1 from "./scriptsPg/1.6.0";
|
||||||
|
import m2 from "./scriptsPg/1.7.0";
|
||||||
|
import m3 from "./scriptsPg/1.8.0";
|
||||||
|
import m4 from "./scriptsPg/1.9.0";
|
||||||
|
import m5 from "./scriptsPg/1.10.0";
|
||||||
|
import m6 from "./scriptsPg/1.10.2";
|
||||||
|
import m7 from "./scriptsPg/1.11.0";
|
||||||
|
import m8 from "./scriptsPg/1.11.1";
|
||||||
|
import m9 from "./scriptsPg/1.12.0";
|
||||||
|
import m10 from "./scriptsPg/1.13.0";
|
||||||
|
import m11 from "./scriptsPg/1.14.0";
|
||||||
|
import m12 from "./scriptsPg/1.15.0";
|
||||||
|
|
||||||
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
|
|
||||||
|
// Define the migration list with versions and their corresponding functions
|
||||||
|
const migrations = [
|
||||||
|
{ version: "1.6.0", run: m1 },
|
||||||
|
{ version: "1.7.0", run: m2 },
|
||||||
|
{ version: "1.8.0", run: m3 },
|
||||||
|
{ version: "1.9.0", run: m4 },
|
||||||
|
{ version: "1.10.0", run: m5 },
|
||||||
|
{ version: "1.10.2", run: m6 },
|
||||||
|
{ version: "1.11.0", run: m7 },
|
||||||
|
{ version: "1.11.1", run: m8 },
|
||||||
|
{ version: "1.12.0", run: m9 },
|
||||||
|
{ version: "1.13.0", run: m10 },
|
||||||
|
{ version: "1.14.0", run: m11 },
|
||||||
|
{ version: "1.15.0", run: m12 }
|
||||||
|
// Add new migrations here as they are created
|
||||||
|
] as {
|
||||||
|
version: string;
|
||||||
|
run: () => Promise<void>;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
// run the migrations
|
||||||
|
await runMigrations();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runMigrations() {
|
||||||
|
if (process.env.DISABLE_MIGRATIONS) {
|
||||||
|
console.log("Migrations are disabled. Skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const appVersion = APP_VERSION;
|
||||||
|
|
||||||
|
// determine if the migrations table exists
|
||||||
|
const exists = await db
|
||||||
|
.select()
|
||||||
|
.from(versionMigrations)
|
||||||
|
.limit(1)
|
||||||
|
.execute()
|
||||||
|
.then((res) => res.length > 0)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
console.log("Migrations table exists, running scripts...");
|
||||||
|
await executeScripts();
|
||||||
|
} else {
|
||||||
|
console.log("Migrations table does not exist, creating it...");
|
||||||
|
console.log("Running migrations...");
|
||||||
|
try {
|
||||||
|
await migrate(db, {
|
||||||
|
migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build
|
||||||
|
});
|
||||||
|
console.log("Migrations completed successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error running migrations:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(versionMigrations)
|
||||||
|
.values({
|
||||||
|
version: appVersion,
|
||||||
|
executedAt: Date.now()
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error running migrations:", e);
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 1000 * 60 * 60 * 24 * 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeScripts() {
|
||||||
|
try {
|
||||||
|
// Get the last executed version from the database
|
||||||
|
const lastExecuted = await db.select().from(versionMigrations);
|
||||||
|
|
||||||
|
// Filter and sort migrations
|
||||||
|
const pendingMigrations = lastExecuted
|
||||||
|
.map((m) => m)
|
||||||
|
.sort((a, b) => semver.compare(b.version, a.version));
|
||||||
|
const startVersion = pendingMigrations[0]?.version ?? "0.0.0";
|
||||||
|
console.log(`Starting migrations from version ${startVersion}`);
|
||||||
|
|
||||||
|
const migrationsToRun = migrations.filter((migration) =>
|
||||||
|
semver.gt(migration.version, startVersion)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Migrations to run:",
|
||||||
|
migrationsToRun.map((m) => m.version).join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run migrations in order
|
||||||
|
for (const migration of migrationsToRun) {
|
||||||
|
console.log(`Running migration ${migration.version}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await migration.run();
|
||||||
|
|
||||||
|
// Update version in database
|
||||||
|
await db
|
||||||
|
.insert(versionMigrations)
|
||||||
|
.values({
|
||||||
|
version: migration.version,
|
||||||
|
executedAt: Date.now()
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Successfully completed migration ${migration.version}`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
typeof (e as any).code === "string" &&
|
||||||
|
(e as any).code === "23505"
|
||||||
|
) {
|
||||||
|
console.error("Migration has already run! Skipping...");
|
||||||
|
continue; // or return, depending on context
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`Failed to run migration ${migration.version}:`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("All migrations completed successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Migration process failed:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ interface PlanOption {
|
|||||||
name: string;
|
name: string;
|
||||||
price: string;
|
price: string;
|
||||||
priceDetail?: string;
|
priceDetail?: string;
|
||||||
tierType: "home_lab" | "starter" | "scale" | null; // Maps to backend tier types
|
tierType: "tier1" | "tier2" | "tier3" | null; // Maps to backend tier types
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier limits for display in confirmation dialog
|
// Tier limits for display in confirmation dialog
|
||||||
@@ -69,20 +69,20 @@ interface TierLimits {
|
|||||||
remoteNodes: number;
|
remoteNodes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierLimits: Record<"home_lab" | "starter" | "scale", TierLimits> = {
|
const tierLimits: Record<"tier1" | "tier2" | "tier3", TierLimits> = {
|
||||||
home_lab: {
|
tier1: {
|
||||||
sites: 3,
|
sites: 3,
|
||||||
users: 3,
|
users: 3,
|
||||||
domains: 3,
|
domains: 3,
|
||||||
remoteNodes: 1
|
remoteNodes: 1
|
||||||
},
|
},
|
||||||
starter: {
|
tier2: {
|
||||||
sites: 10,
|
sites: 10,
|
||||||
users: 150,
|
users: 150,
|
||||||
domains: 250,
|
domains: 250,
|
||||||
remoteNodes: 5
|
remoteNodes: 5
|
||||||
},
|
},
|
||||||
scale: {
|
tier3: {
|
||||||
sites: 10,
|
sites: 10,
|
||||||
users: 150,
|
users: 150,
|
||||||
domains: 250,
|
domains: 250,
|
||||||
@@ -102,21 +102,21 @@ const planOptions: PlanOption[] = [
|
|||||||
name: "Homelab",
|
name: "Homelab",
|
||||||
price: "$15",
|
price: "$15",
|
||||||
priceDetail: "/ month",
|
priceDetail: "/ month",
|
||||||
tierType: "home_lab"
|
tierType: "tier1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "team",
|
id: "team",
|
||||||
name: "Team",
|
name: "Team",
|
||||||
price: "$5",
|
price: "$5",
|
||||||
priceDetail: "per user / month",
|
priceDetail: "per user / month",
|
||||||
tierType: "starter"
|
tierType: "tier2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "business",
|
id: "business",
|
||||||
name: "Business",
|
name: "Business",
|
||||||
price: "$10",
|
price: "$10",
|
||||||
priceDetail: "per user / month",
|
priceDetail: "per user / month",
|
||||||
tierType: "scale"
|
tierType: "tier3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "enterprise",
|
id: "enterprise",
|
||||||
@@ -155,7 +155,7 @@ export default function BillingPage() {
|
|||||||
const [hasSubscription, setHasSubscription] = useState(false);
|
const [hasSubscription, setHasSubscription] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [currentTier, setCurrentTier] = useState<
|
const [currentTier, setCurrentTier] = useState<
|
||||||
"home_lab" | "starter" | "scale" | null
|
"tier1" | "tier2" | "tier3" | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
// Usage IDs
|
// Usage IDs
|
||||||
@@ -167,7 +167,7 @@ export default function BillingPage() {
|
|||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [pendingTier, setPendingTier] = useState<{
|
const [pendingTier, setPendingTier] = useState<{
|
||||||
tier: "home_lab" | "starter" | "scale";
|
tier: "tier1" | "tier2" | "tier3";
|
||||||
action: "upgrade" | "downgrade";
|
action: "upgrade" | "downgrade";
|
||||||
planName: string;
|
planName: string;
|
||||||
price: string;
|
price: string;
|
||||||
@@ -186,18 +186,18 @@ export default function BillingPage() {
|
|||||||
// Find tier subscription
|
// Find tier subscription
|
||||||
const tierSub = subscriptions.find(
|
const tierSub = subscriptions.find(
|
||||||
({ subscription }) =>
|
({ subscription }) =>
|
||||||
subscription?.type === "home_lab" ||
|
subscription?.type === "tier1" ||
|
||||||
subscription?.type === "starter" ||
|
subscription?.type === "tier2" ||
|
||||||
subscription?.type === "scale"
|
subscription?.type === "tier3"
|
||||||
);
|
);
|
||||||
setTierSubscription(tierSub || null);
|
setTierSubscription(tierSub || null);
|
||||||
|
|
||||||
if (tierSub?.subscription) {
|
if (tierSub?.subscription) {
|
||||||
setCurrentTier(
|
setCurrentTier(
|
||||||
tierSub.subscription.type as
|
tierSub.subscription.type as
|
||||||
| "home_lab"
|
| "tier1"
|
||||||
| "starter"
|
| "tier2"
|
||||||
| "scale"
|
| "tier3"
|
||||||
);
|
);
|
||||||
setHasSubscription(
|
setHasSubscription(
|
||||||
tierSub.subscription.status === "active"
|
tierSub.subscription.status === "active"
|
||||||
@@ -243,7 +243,7 @@ export default function BillingPage() {
|
|||||||
}, [org.org.orgId]);
|
}, [org.org.orgId]);
|
||||||
|
|
||||||
const handleStartSubscription = async (
|
const handleStartSubscription = async (
|
||||||
tier: "home_lab" | "starter" | "scale"
|
tier: "tier1" | "tier2" | "tier3"
|
||||||
) => {
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -300,7 +300,7 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeTier = async (tier: "home_lab" | "starter" | "scale") => {
|
const handleChangeTier = async (tier: "tier1" | "tier2" | "tier3") => {
|
||||||
if (!hasSubscription) {
|
if (!hasSubscription) {
|
||||||
// If no subscription, start a new one
|
// If no subscription, start a new one
|
||||||
handleStartSubscription(tier);
|
handleStartSubscription(tier);
|
||||||
@@ -343,7 +343,7 @@ export default function BillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showTierConfirmation = (
|
const showTierConfirmation = (
|
||||||
tier: "home_lab" | "starter" | "scale",
|
tier: "tier1" | "tier2" | "tier3",
|
||||||
action: "upgrade" | "downgrade",
|
action: "upgrade" | "downgrade",
|
||||||
planName: string,
|
planName: string,
|
||||||
price: string
|
price: string
|
||||||
@@ -453,8 +453,8 @@ export default function BillingPage() {
|
|||||||
// Calculate current usage cost for display
|
// Calculate current usage cost for display
|
||||||
const getUserCount = () => getUsageValue(USERS);
|
const getUserCount = () => getUsageValue(USERS);
|
||||||
const getPricePerUser = () => {
|
const getPricePerUser = () => {
|
||||||
if (currentTier === "starter") return 5;
|
if (currentTier === "tier2") return 5;
|
||||||
if (currentTier === "scale") return 10;
|
if (currentTier === "tier3") return 10;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const isOrgSubscribed = cache(async (orgId: string) => {
|
|||||||
try {
|
try {
|
||||||
const subRes = await getCachedSubscription(orgId);
|
const subRes = await getCachedSubscription(orgId);
|
||||||
subscribed =
|
subscribed =
|
||||||
(subRes.data.data.tier == "home_lab" || subRes.data.data.tier == "starter" || subRes.data.data.tier == "scale") &&
|
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3") &&
|
||||||
subRes.data.data.active;
|
subRes.data.data.active;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ export function SubscriptionStatusProvider({
|
|||||||
// Iterate through all subscriptions
|
// Iterate through all subscriptions
|
||||||
for (const { subscription } of subscriptionStatus.subscriptions) {
|
for (const { subscription } of subscriptionStatus.subscriptions) {
|
||||||
if (
|
if (
|
||||||
subscription.type == "home_lab" ||
|
subscription.type == "tier1" ||
|
||||||
subscription.type == "starter" ||
|
subscription.type == "tier2" ||
|
||||||
subscription.type == "scale"
|
subscription.type == "tier3"
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
tier: subscription.type,
|
tier: subscription.type,
|
||||||
@@ -70,7 +70,7 @@ export function SubscriptionStatusProvider({
|
|||||||
}
|
}
|
||||||
const { tier, active } = getTier();
|
const { tier, active } = getTier();
|
||||||
return (
|
return (
|
||||||
(tier == "home_lab" || tier == "starter" || tier == "scale") &&
|
(tier == "tier1" || tier == "tier2" || tier == "tier3") &&
|
||||||
active
|
active
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user