mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 10:46:38 +00:00
Add test button to launch stripe
This commit is contained in:
37
server/lib/billing/licenses.ts
Normal file
37
server/lib/billing/licenses.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
113
server/private/routers/billing/createCheckoutSessionLicense.ts
Normal file
113
server/private/routers/billing/createCheckoutSessionLicense.ts
Normal file
@@ -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<any> {
|
||||||
|
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<string>(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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ const createCheckoutSessionSchema = z.strictObject({
|
|||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function createCheckoutSession(
|
export async function createCheckoutSessionSAAS(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
@@ -87,7 +87,7 @@ export async function createCheckoutSession(
|
|||||||
data: session.url,
|
data: session.url,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Organization created successfully",
|
message: "Checkout session created successfully",
|
||||||
status: HttpCode.CREATED
|
status: HttpCode.CREATED
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./createCheckoutSession";
|
export * from "./createCheckoutSessionSAAS";
|
||||||
export * from "./createPortalSession";
|
export * from "./createPortalSession";
|
||||||
export * from "./getOrgSubscription";
|
export * from "./getOrgSubscription";
|
||||||
export * from "./getOrgUsage";
|
export * from "./getOrgUsage";
|
||||||
export * from "./internalGetOrgTier";
|
export * from "./internalGetOrgTier";
|
||||||
|
export * from "./createCheckoutSessionLicense";
|
||||||
|
|||||||
@@ -159,11 +159,19 @@ if (build === "saas") {
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/billing/create-checkout-session",
|
"/org/:orgId/billing/create-checkout-session-saas",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifyUserHasAction(ActionsEnum.billing),
|
verifyUserHasAction(ActionsEnum.billing),
|
||||||
logActionAudit(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(
|
authenticated.post(
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default function GeneralPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.post<AxiosResponse<string>>(
|
const response = await api.post<AxiosResponse<string>>(
|
||||||
`/org/${org.org.orgId}/billing/create-checkout-session`,
|
`/org/${org.org.orgId}/billing/create-checkout-session-saas`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
console.log("Checkout session response:", response.data);
|
console.log("Checkout session response:", response.data);
|
||||||
|
|||||||
@@ -345,6 +345,37 @@ export default function GenerateLicenseKeyForm({
|
|||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTestCheckout = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.post<AxiosResponse<string>>(
|
||||||
|
`/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 (
|
return (
|
||||||
<Credenza open={open} onOpenChange={handleClose}>
|
<Credenza open={open} onOpenChange={handleClose}>
|
||||||
<CredenzaContent className="max-w-4xl">
|
<CredenzaContent className="max-w-4xl">
|
||||||
@@ -1066,16 +1097,26 @@ export default function GenerateLicenseKeyForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!generatedKey && useCaseType === "business" && (
|
{!generatedKey && useCaseType === "business" && (
|
||||||
<Button
|
<>
|
||||||
type="submit"
|
<Button
|
||||||
form="generate-license-business-form"
|
variant="secondary"
|
||||||
disabled={loading}
|
onClick={handleTestCheckout}
|
||||||
loading={loading}
|
disabled={loading}
|
||||||
>
|
loading={loading}
|
||||||
{t(
|
>
|
||||||
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
TEST: Go to Checkout
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="generate-license-business-form"
|
||||||
|
disabled={loading}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user