From 158d7b23d89b937fa106923341d87e0c177c1a82 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 14:13:25 -0800 Subject: [PATCH] Add test button to launch stripe --- server/lib/billing/licenses.ts | 37 ++++++ .../billing/createCheckoutSessionLicense.ts | 113 ++++++++++++++++++ ...ession.ts => createCheckoutSessionSAAS.ts} | 4 +- server/private/routers/billing/index.ts | 3 +- server/private/routers/external.ts | 12 +- .../settings/(private)/billing/page.tsx | 2 +- src/components/GenerateLicenseKeyForm.tsx | 61 ++++++++-- 7 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 server/lib/billing/licenses.ts create mode 100644 server/private/routers/billing/createCheckoutSessionLicense.ts rename server/private/routers/billing/{createCheckoutSession.ts => createCheckoutSessionSAAS.ts} (96%) diff --git a/server/lib/billing/licenses.ts b/server/lib/billing/licenses.ts new file mode 100644 index 00000000..a481527e --- /dev/null +++ b/server/lib/billing/licenses.ts @@ -0,0 +1,37 @@ +export enum LicenseId { + SMALL_LICENSE = "small_license", + BIG_LICENSE = "big_license" +} + +export type LicensePriceSet = { + [key in LicenseId]: string; +}; + +export const licensePriceSet: LicensePriceSet = { + // Free license matches the freeLimitSet + [LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN", + [LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl" +}; + +export const licensePriceSetSandbox: LicensePriceSet = { + // Free license matches the freeLimitSet + // when matching license the keys closer to 0 index are matched first so list the licenses in descending order of value + [LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN", + [LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl" +}; + +export function getLicensePriceSet( + environment?: string, + sandbox_mode?: boolean +): LicensePriceSet { + if ( + (process.env.ENVIRONMENT == "prod" && + process.env.SANDBOX_MODE !== "true") || + (environment === "prod" && sandbox_mode !== true) + ) { + // THIS GETS LOADED CLIENT SIDE AND SERVER SIDE + return licensePriceSet; + } else { + return licensePriceSetSandbox; + } +} diff --git a/server/private/routers/billing/createCheckoutSessionLicense.ts b/server/private/routers/billing/createCheckoutSessionLicense.ts new file mode 100644 index 00000000..045f1797 --- /dev/null +++ b/server/private/routers/billing/createCheckoutSessionLicense.ts @@ -0,0 +1,113 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { customers, db } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import config from "@server/lib/config"; +import { fromError } from "zod-validation-error"; +import stripe from "#private/lib/stripe"; +import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; + +const createCheckoutSessionParamsSchema = z.strictObject({ + orgId: z.string(), +}); + +const createCheckoutSessionBodySchema = z.strictObject({ + tier: z.enum([LicenseId.BIG_LICENSE, LicenseId.SMALL_LICENSE]), +}); + +export async function createCheckoutSessionoLicense( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = createCheckoutSessionParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const parsedBody = createCheckoutSessionBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { tier } = parsedBody.data; + + // check if we already have a customer for this org + const [customer] = await db + .select() + .from(customers) + .where(eq(customers.orgId, orgId)) + .limit(1); + + // If we don't have a customer, create one + if (!customer) { + // error + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "No customer found for this organization" + ) + ); + } + + const tierPrice = getLicensePriceSet()[tier] + + const session = await stripe!.checkout.sessions.create({ + client_reference_id: orgId, // So we can look it up the org later on the webhook + billing_address_collection: "required", + line_items: [ + { + price: tierPrice, // Use the standard tier + quantity: 1 + }, + ], // Start with the standard feature set that matches the free limits + customer: customer.customerId, + mode: "subscription", + success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?success=true&session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true` + }); + + return response(res, { + data: session.url, + success: true, + error: false, + message: "Checkout session created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSessionSAAS.ts similarity index 96% rename from server/private/routers/billing/createCheckoutSession.ts rename to server/private/routers/billing/createCheckoutSessionSAAS.ts index a2d8080f..0f9b783e 100644 --- a/server/private/routers/billing/createCheckoutSession.ts +++ b/server/private/routers/billing/createCheckoutSessionSAAS.ts @@ -29,7 +29,7 @@ const createCheckoutSessionSchema = z.strictObject({ orgId: z.string() }); -export async function createCheckoutSession( +export async function createCheckoutSessionSAAS( req: Request, res: Response, next: NextFunction @@ -87,7 +87,7 @@ export async function createCheckoutSession( data: session.url, success: true, error: false, - message: "Organization created successfully", + message: "Checkout session created successfully", status: HttpCode.CREATED }); } catch (error) { diff --git a/server/private/routers/billing/index.ts b/server/private/routers/billing/index.ts index 59fce8d6..aef867af 100644 --- a/server/private/routers/billing/index.ts +++ b/server/private/routers/billing/index.ts @@ -11,8 +11,9 @@ * This file is not licensed under the AGPLv3. */ -export * from "./createCheckoutSession"; +export * from "./createCheckoutSessionSAAS"; export * from "./createPortalSession"; export * from "./getOrgSubscription"; export * from "./getOrgUsage"; export * from "./internalGetOrgTier"; +export * from "./createCheckoutSessionLicense"; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index cf6e58bc..9ad0609f 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -159,11 +159,19 @@ if (build === "saas") { ); authenticated.post( - "/org/:orgId/billing/create-checkout-session", + "/org/:orgId/billing/create-checkout-session-saas", verifyOrgAccess, verifyUserHasAction(ActionsEnum.billing), logActionAudit(ActionsEnum.billing), - billing.createCheckoutSession + billing.createCheckoutSessionSAAS + ); + + authenticated.post( + "/org/:orgId/billing/create-checkout-session-license", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + logActionAudit(ActionsEnum.billing), + billing.createCheckoutSessionoLicense ); authenticated.post( diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index 1ed5c094..e63eebcc 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -121,7 +121,7 @@ export default function GeneralPage() { setIsLoading(true); try { const response = await api.post>( - `/org/${org.org.orgId}/billing/create-checkout-session`, + `/org/${org.org.orgId}/billing/create-checkout-session-saas`, {} ); console.log("Checkout session response:", response.data); diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx index 6a380082..6a5aaf54 100644 --- a/src/components/GenerateLicenseKeyForm.tsx +++ b/src/components/GenerateLicenseKeyForm.tsx @@ -345,6 +345,37 @@ export default function GenerateLicenseKeyForm({ resetForm(); }; + const handleTestCheckout = async () => { + setLoading(true); + try { + const response = await api.post>( + `/org/${orgId}/billing/create-checkout-session-license`, + { + tier: "big_license" + } + ); + console.log("Checkout session response:", response.data); + const checkoutUrl = response.data.data; + if (checkoutUrl) { + window.location.href = checkoutUrl; + } else { + toast({ + title: "Failed to get checkout URL", + description: "Please try again later", + variant: "destructive" + }); + setLoading(false); + } + } catch (error) { + toast({ + title: "Checkout error", + description: formatAxiosError(error), + variant: "destructive" + }); + setLoading(false); + } + }; + return ( @@ -1066,16 +1097,26 @@ export default function GenerateLicenseKeyForm({ )} {!generatedKey && useCaseType === "business" && ( - + <> + + + )}