create customer
This commit is contained in:
1 parent
7b207f2ccb
commit
01c0e9ef4d
11 files changed
+159
-14
No files matched your search
@@ -1,7 +1,9 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import BillingService, {
|
||||
type IBillingService,
|
||||
} from "@/services/system/BillingService.js";
|
||||
import { type IBillingService } from "@/services/system/BillingService.js";
|
||||
import { Plans, type PlanKey } from "@/types/entitlements.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
import { config } from "@/config/index.js";
|
||||
import { Org } from "@/db/models/index.js";
|
||||
|
||||
export interface IBillingController {
|
||||
listPlans: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
||||
@@ -29,10 +31,24 @@ class BillingController implements IBillingController {
|
||||
|
||||
subscribePlan = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json({
|
||||
message: "OK",
|
||||
data: { redirectUrl: "https://www.google.com" },
|
||||
const user = req.user;
|
||||
if (!user) throw new ApiError("Unauthorized", 401);
|
||||
|
||||
const orgId = req.user?.orgId;
|
||||
if (!orgId) throw new ApiError("Organization not found", 404);
|
||||
|
||||
const planKey: PlanKey | undefined = req.body.planKey;
|
||||
const redirectUrl = await this.billingService.subscribePlan(
|
||||
user.email,
|
||||
orgId,
|
||||
planKey!
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Subscription initiation successful",
|
||||
data: { redirectUrl },
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ const verifyOrgPermission = (resourceActions: string[]) => {
|
||||
return next(new ApiError("User not authenticated", 401));
|
||||
}
|
||||
|
||||
console.log(req.user);
|
||||
|
||||
const orgPermissions = userContext.roles?.orgRole?.permissions || [];
|
||||
const allowed = hasPermission(orgPermissions, resourceActions);
|
||||
if (!allowed) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import BillingController from "@/controllers/BillingController.js";
|
||||
import { verifyToken } from "@/middleware/VerifyToken.js";
|
||||
import { validateBody } from "@/middleware/validation.js";
|
||||
import { subscribePlanSchema } from "@/validation/index.js";
|
||||
import { verifyOrgPermission } from "@/middleware/VerifyPermission.js";
|
||||
import { PERMISSIONS } from "@/types/permissions.js";
|
||||
import { addUserContext } from "@/middleware/AddUserContext.js";
|
||||
|
||||
class BillingRoutes {
|
||||
private router;
|
||||
@@ -13,7 +18,14 @@ class BillingRoutes {
|
||||
|
||||
initRoutes = () => {
|
||||
this.router.get("/plans", verifyToken, this.controller.listPlans);
|
||||
this.router.post("/subscribe", verifyToken, this.controller.subscribePlan);
|
||||
this.router.post(
|
||||
"/subscribe",
|
||||
verifyToken,
|
||||
addUserContext,
|
||||
verifyOrgPermission([PERMISSIONS.billing.all]),
|
||||
validateBody(subscribePlanSchema),
|
||||
this.controller.subscribePlan
|
||||
);
|
||||
};
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -1,20 +1,91 @@
|
||||
import { Plans, type Entitlements, type PlanKey } from "@/types/entitlements.js";
|
||||
import {
|
||||
Plans,
|
||||
type Entitlements,
|
||||
type PlanKey,
|
||||
} from "@/types/entitlements.js";
|
||||
import { config } from "@/config/index.js";
|
||||
import { Org } from "@/db/models/index.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export interface IBillingService {
|
||||
listPlans(): Promise<Entitlements[]>;
|
||||
subscribePlan(
|
||||
email: string,
|
||||
orgId: string,
|
||||
planKey: PlanKey
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
const SERVICE_NAME = "BillingService";
|
||||
|
||||
class BillingService implements IBillingService {
|
||||
public SERVICE_NAME: string;
|
||||
private stripeClient: Stripe;
|
||||
|
||||
constructor() {
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.stripeClient = new Stripe(config.STRIPE_SECRET);
|
||||
}
|
||||
async listPlans(): Promise<Entitlements[]> {
|
||||
const order: PlanKey[] = ["free", "pro", "business", "enterprise"];
|
||||
return order.map((key) => Plans[key]);
|
||||
}
|
||||
|
||||
subscribePlan = async (email: string, orgId: string, planKey: PlanKey) => {
|
||||
if (!planKey) {
|
||||
throw new ApiError("planKey is required", 400);
|
||||
}
|
||||
|
||||
if (planKey && !Plans[planKey]) {
|
||||
throw new ApiError("Invalid planKey", 400);
|
||||
}
|
||||
|
||||
// Ensure billing customer exists for this org
|
||||
const org = await Org.findById(orgId);
|
||||
if (!org) throw new ApiError("Organization not found", 404);
|
||||
if (!org.billingCustomerId) {
|
||||
const customer = await this.stripeClient.customers.create({
|
||||
email,
|
||||
metadata: { orgId: String(org._id) },
|
||||
});
|
||||
org.billingCustomerId = customer.id;
|
||||
await org.save();
|
||||
}
|
||||
|
||||
const prices = await this.stripeClient.prices.list({
|
||||
lookup_keys: [planKey],
|
||||
expand: ["data.product"],
|
||||
});
|
||||
|
||||
let price = prices.data[0]?.id;
|
||||
if (!price) {
|
||||
throw new ApiError("Price not found for the selected plan", 404);
|
||||
}
|
||||
|
||||
const successUrl = `${config.ORIGIN}/billing/success?plan=${planKey}`;
|
||||
const cancelUrl = `${config.ORIGIN}/billing/`;
|
||||
const session = await this.stripeClient.checkout.sessions.create({
|
||||
billing_address_collection: "auto",
|
||||
line_items: [
|
||||
{
|
||||
price,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "subscription",
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
customer: String(org.billingCustomerId),
|
||||
client_reference_id: String(org._id),
|
||||
subscription_data: { metadata: { orgId: String(org._id) } },
|
||||
metadata: { orgId: String(org._id) },
|
||||
});
|
||||
if (session.url === null) {
|
||||
throw new ApiError("Failed to create checkout session", 500);
|
||||
}
|
||||
return session.url;
|
||||
};
|
||||
}
|
||||
|
||||
export default BillingService;
|
||||
@@ -1,4 +1,12 @@
|
||||
export type PlanKey = "free" | "pro" | "business" | "enterprise" | "unlimited";
|
||||
export const PlanKeys = [
|
||||
"free",
|
||||
"pro",
|
||||
"business",
|
||||
"enterprise",
|
||||
"unlimited",
|
||||
] as const;
|
||||
|
||||
export type PlanKey = (typeof PlanKeys)[number];
|
||||
|
||||
export interface Entitlements {
|
||||
plan: PlanKey;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/db/models/index.js";
|
||||
import { z } from "zod";
|
||||
import mongoose from "mongoose";
|
||||
import { PlanKeys, type PlanKey } from "@/types/entitlements.js";
|
||||
|
||||
export const registerSchema = z.object({
|
||||
email: z.email({ message: "Invalid email address" }),
|
||||
@@ -127,6 +128,10 @@ export const monitorImportSchema = z.object({
|
||||
.min(1, { message: "Provide at least one monitor" }),
|
||||
});
|
||||
|
||||
export const subscribePlanSchema = z.object({
|
||||
planKey: z.enum(PlanKeys).optional(),
|
||||
});
|
||||
|
||||
export type MonitorImportPayload = z.infer<typeof monitorImportSchema>;
|
||||
|
||||
export const monitorPatchSchema = monitorSchema
|
||||
|
||||
Reference in new issue
Block a user