mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-03 17:26:38 +00:00
Basic billing page is working
This commit is contained in:
@@ -1520,6 +1520,19 @@
|
|||||||
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
||||||
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
||||||
"billingPricingCalculatorLink": "Pricing Calculator",
|
"billingPricingCalculatorLink": "Pricing Calculator",
|
||||||
|
"billingYourPlan": "Your Plan",
|
||||||
|
"billingViewOrModifyPlan": "View or modify your current plan",
|
||||||
|
"billingViewPlanDetails": "View Plan Details",
|
||||||
|
"billingUsageAndLimits": "Usage and Limits",
|
||||||
|
"billingViewUsageAndLimits": "View your plan's limits and current usage",
|
||||||
|
"billingCurrentUsage": "Current Usage",
|
||||||
|
"billingMaximumLimits": "Maximum Limits",
|
||||||
|
"billingRemoteNodes": "Remote Nodes",
|
||||||
|
"billingUnlimited": "Unlimited",
|
||||||
|
"billingPaidLicenseKeys": "Paid License Keys",
|
||||||
|
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
|
||||||
|
"billingCurrentKeys": "Current Keys",
|
||||||
|
"billingModifyCurrentPlan": "Modify Current Plan",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "I agree to the",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "terms of service",
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
||||||
"dev:check": "npx tsc --noEmit && npm run format:check",
|
"dev:check": "npx tsc --noEmit && npm run format:check",
|
||||||
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:generate && npm run db:sqlite:push",
|
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:generate && npm run db:sqlite:push",
|
||||||
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
|
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
|
||||||
|
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
|
||||||
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
|
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
|
||||||
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
|
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
|
||||||
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
|
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
import { usageService } from "./usageService";
|
||||||
|
|
||||||
export enum FeatureId {
|
export enum FeatureId {
|
||||||
USERS = "users",
|
USERS = "users",
|
||||||
@@ -95,10 +96,24 @@ export function getScaleFeaturePriceSet(): FeaturePriceSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLineItems(
|
export async function getLineItems(
|
||||||
featurePriceSet: FeaturePriceSet
|
featurePriceSet: FeaturePriceSet,
|
||||||
): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
orgId: string,
|
||||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
|
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||||
price: priceId
|
const users = await usageService.getUsageDaily(orgId, FeatureId.USERS);
|
||||||
}));
|
|
||||||
|
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||||
|
let quantity: number | undefined;
|
||||||
|
|
||||||
|
if (featureId === FeatureId.USERS) {
|
||||||
|
quantity = users?.instantaneousValue || 1;
|
||||||
|
} else if (featureId === FeatureId.HOME_LAB) {
|
||||||
|
quantity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
price: priceId,
|
||||||
|
quantity: quantity
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
3
server/lib/isSubscribed.ts
Normal file
3
server/lib/isSubscribed.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export async function isSubscribed(orgId: string): Promise<boolean> {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
import { eq, isNull, sql, not, and, desc } from "drizzle-orm";
|
import { eq, isNull, sql, not, and, desc } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { getUserDeviceName } from "@server/db/names";
|
import { getUserDeviceName } from "@server/db/names";
|
||||||
import { isLicensedOrSubscribed } from "@server/private/lib/isLicencedOrSubscribed";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
getHomeLabFeaturePriceSet,
|
getHomeLabFeaturePriceSet,
|
||||||
getScaleFeaturePriceSet,
|
getScaleFeaturePriceSet,
|
||||||
getStarterFeaturePriceSet,
|
getStarterFeaturePriceSet,
|
||||||
|
getLineItems,
|
||||||
FeatureId,
|
FeatureId,
|
||||||
type FeaturePriceSet
|
type FeaturePriceSet
|
||||||
} from "@server/lib/billing";
|
} from "@server/lib/billing";
|
||||||
@@ -149,7 +150,7 @@ 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
|
// home_lab uses HOME_LAB product, starter/scale use USERS product
|
||||||
const currentTier = subscription.type;
|
const currentTier = subscription.type;
|
||||||
const switchingProducts =
|
const switchingProducts =
|
||||||
(currentTier === "home_lab" && (tier === "starter" || tier === "scale")) ||
|
(currentTier === "home_lab" && (tier === "starter" || tier === "scale")) ||
|
||||||
((currentTier === "starter" || currentTier === "scale") && tier === "home_lab");
|
((currentTier === "starter" || currentTier === "scale") && tier === "home_lab");
|
||||||
|
|
||||||
@@ -175,10 +176,9 @@ export async function changeTier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add new items for the target tier
|
// Add new items for the target tier
|
||||||
for (const [featureId, priceId] of Object.entries(targetPriceSet)) {
|
const newLineItems = await getLineItems(targetPriceSet, orgId);
|
||||||
itemsToUpdate.push({
|
for (const lineItem of newLineItems) {
|
||||||
price: priceId
|
itemsToUpdate.push(lineItem);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedSubscription = await stripe!.subscriptions.update(
|
updatedSubscription = await stripe!.subscriptions.update(
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import config from "@server/lib/config";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import stripe from "#private/lib/stripe";
|
import stripe from "#private/lib/stripe";
|
||||||
import { getHomeLabFeaturePriceSet, getLineItems, getScaleFeaturePriceSet, getStarterFeaturePriceSet } from "@server/lib/billing";
|
import { getHomeLabFeaturePriceSet, getLineItems, getScaleFeaturePriceSet, getStarterFeaturePriceSet } from "@server/lib/billing";
|
||||||
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
const createCheckoutSessionSchema = z.strictObject({
|
const createCheckoutSessionSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -80,19 +82,21 @@ export async function createCheckoutSession(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lineItems;
|
let lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
|
||||||
if (tier === "home_lab") {
|
if (tier === "home_lab") {
|
||||||
lineItems = getLineItems(getHomeLabFeaturePriceSet());
|
lineItems = await getLineItems(getHomeLabFeaturePriceSet(), orgId);
|
||||||
} else if (tier === "starter") {
|
} else if (tier === "starter") {
|
||||||
lineItems = getLineItems(getStarterFeaturePriceSet());
|
lineItems = await getLineItems(getStarterFeaturePriceSet(), orgId);
|
||||||
} else if (tier === "scale") {
|
} else if (tier === "scale") {
|
||||||
lineItems = getLineItems(getScaleFeaturePriceSet());
|
lineItems = await getLineItems(getScaleFeaturePriceSet(), orgId);
|
||||||
} else {
|
} else {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid plan")
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid plan")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`Line items: ${JSON.stringify(lineItems)}`)
|
||||||
|
|
||||||
const session = await stripe!.checkout.sessions.create({
|
const session = await stripe!.checkout.sessions.create({
|
||||||
client_reference_id: orgId, // So we can look it up the org later on the webhook
|
client_reference_id: orgId, // So we can look it up the org later on the webhook
|
||||||
billing_address_collection: "required",
|
billing_address_collection: "required",
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ export * from "./createPortalSession";
|
|||||||
export * from "./getOrgSubscriptions";
|
export * from "./getOrgSubscriptions";
|
||||||
export * from "./getOrgUsage";
|
export * from "./getOrgUsage";
|
||||||
export * from "./internalGetOrgTier";
|
export * from "./internalGetOrgTier";
|
||||||
|
export * from "./changeTier";
|
||||||
|
|||||||
@@ -151,6 +151,14 @@ if (build === "saas") {
|
|||||||
billing.createCheckoutSession
|
billing.createCheckoutSession
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/billing/change-tier",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.billing),
|
||||||
|
logActionAudit(ActionsEnum.billing),
|
||||||
|
billing.changeTier
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/billing/create-portal-session",
|
"/org/:orgId/billing/create-portal-session",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, InferInsertModel } from "drizzle-orm";
|
import { eq, InferInsertModel } from "drizzle-orm";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import config from "@server/private/lib/config";
|
import config from "#private/lib/config";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import jsonwebtoken from "jsonwebtoken";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { isSubscribed } from "@server/private/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { cache } from "@server/lib/cache";
|
import { cache } from "@server/lib/cache";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { subscribe } from "node:diagnostics_channel";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
import { isSubscribed } from "@server/private/lib/isSubscribed";
|
|
||||||
|
|
||||||
const updateOrgParamsSchema = z.strictObject({
|
const updateOrgParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { usageService } from "@server/lib/billing/usageService";
|
|||||||
import { FeatureId } from "@server/lib/billing";
|
import { FeatureId } from "@server/lib/billing";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||||
import { isSubscribed } from "@server/private/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user