From 39dee998ef9f64d0d46d50eefcb618bb2f09cd38 Mon Sep 17 00:00:00 2001 From: Adeosun Oluwaseyi Date: Thu, 15 Aug 2024 19:17:16 +0100 Subject: [PATCH] feat: Implement API Endpoints for Managing User Plans --- src/controllers/planController.ts | 82 +++++++++++++++ src/index.ts | 2 + src/middleware/authorisationSuperAdmin.ts | 15 +++ src/models/plan.ts | 20 ++++ src/models/subcription.ts | 34 +++++++ src/models/user.ts | 4 + src/routes/plans.ts | 50 +++++++++ src/services/super-admin-plans.ts | 117 ++++++++++++++++++++++ 8 files changed, 324 insertions(+) create mode 100644 src/controllers/planController.ts create mode 100644 src/middleware/authorisationSuperAdmin.ts create mode 100644 src/models/plan.ts create mode 100644 src/models/subcription.ts create mode 100644 src/routes/plans.ts create mode 100644 src/services/super-admin-plans.ts diff --git a/src/controllers/planController.ts b/src/controllers/planController.ts new file mode 100644 index 00000000..103fa71a --- /dev/null +++ b/src/controllers/planController.ts @@ -0,0 +1,82 @@ +import { Request, Response } from "express"; +import { PlanService } from "../services/super-admin-plans"; + +const planService = new PlanService(); + +export const getCurrentPlan = async (req: Request, res: Response) => { + const { userId } = req.params; + + try { + const planData = await planService.getCurrentPlan(userId); + return res.status(200).json(planData); + } catch (error) { + return res.status(404).json({ message: error.message }); + } +}; + +export const createPlan = async (req: Request, res: Response) => { + const { name, price, features, limitations } = req.body; + + try { + const newPlan = await planService.createPlan({ + name, + price, + features, + limitations, + }); + return res.status(201).json({ + message: "Plan created successfully", + plan: newPlan, + }); + } catch (error) { + const statusCode = + error.message === "Invalid input" || + error.message === "Plan already exists" + ? 400 + : 500; + return res.status(statusCode).json({ message: error.message }); + } +}; + +export const updatePlan = async (req: Request, res: Response) => { + const { id } = req.params; + const updateData = req.body; + + try { + const updatedPlan = await planService.updatePlan(id, updateData); + return res.status(200).json({ + message: "Plan updated successfully", + plan: updatedPlan, + }); + } catch (error) { + return res + .status(error.message === "Invalid price" ? 400 : 500) + .json({ message: error.message }); + } +}; + +export const comparePlans = async (req: Request, res: Response) => { + try { + const plans = await planService.comparePlans(); + return res.status(200).json(plans); + } catch (error) { + return res.status(500).json({ message: error.message }); + } +}; + +export const deletePlan = async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const result = await planService.deletePlan(id); + return res.status(200).json(result); + } catch (error) { + return res + .status( + error.message === "Cannot delete plan with active subscriptions" + ? 400 + : 500, + ) + .json({ message: error.message }); + } +}; diff --git a/src/index.ts b/src/index.ts index 7e013adc..4d06cf15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,7 @@ import { Limiter } from "./utils"; import log from "./utils/logger"; import ServerAdapter from "./views/bull-board"; import { roleRouter } from "./routes/roles"; +import { planRouter } from "./routes/plans"; dotenv.config(); const port = config.port; @@ -99,6 +100,7 @@ server.use("/api/v1", paymentPaystackRouter); server.use("/api/v1", billingPlanRouter); server.use("/api/v1", newsLetterSubscriptionRoute); server.use("/api/v1", squeezeRoute); +server.use("/api/v1", planRouter); server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); diff --git a/src/middleware/authorisationSuperAdmin.ts b/src/middleware/authorisationSuperAdmin.ts new file mode 100644 index 00000000..28fc08a5 --- /dev/null +++ b/src/middleware/authorisationSuperAdmin.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from "express"; +import { UserRole } from "../enums/userRoles"; // Adjust the import path as needed + +export const authorizeRole = (roles: UserRole[]) => { + return (req: Request, res: Response, next: NextFunction) => { + const userRole = req.user.role as UserRole; // Assuming `req.user.role` is set during authentication + + if (!roles.includes(userRole)) { + return res + .status(403) + .json({ message: "Access forbidden: insufficient rights" }); + } + next(); + }; +}; diff --git a/src/models/plan.ts b/src/models/plan.ts new file mode 100644 index 00000000..de3580bb --- /dev/null +++ b/src/models/plan.ts @@ -0,0 +1,20 @@ +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; +import ExtendedBaseEntity from "../models/extended-base-entity"; + +@Entity("plans") +export class Plan extends ExtendedBaseEntity { + @PrimaryGeneratedColumn() + id: string; + + @Column() + name: string; + + @Column("decimal") + price: number; + + @Column("simple-array") + features: string[]; + + @Column("text") + limitations: string; +} diff --git a/src/models/subcription.ts b/src/models/subcription.ts new file mode 100644 index 00000000..c8d9e815 --- /dev/null +++ b/src/models/subcription.ts @@ -0,0 +1,34 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; +import { User } from "./user"; +import { Plan } from "./plan"; + +@Entity() +export class Subscription { + @PrimaryGeneratedColumn() + id: string; + + @ManyToOne(() => User, (user) => user.subscriptions) + user: User; + + @ManyToOne(() => Plan) + plan: Plan; + + @CreateDateColumn() + startDate: Date; + + @Column({ type: "date" }) + renewalDate: Date; + + @Column({ default: "Active" }) + status: string; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/models/user.ts b/src/models/user.ts index ad88e983..66f943de 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -29,6 +29,7 @@ import ExtendedBaseEntity from "./extended-base-entity"; import { Like } from "./like"; import { OrganizationMember } from "./organization-member"; import { UserOrganization } from "./user-organisation"; +import { Subscription } from "./subcription"; @Entity() @Unique(["email"]) @@ -145,6 +146,9 @@ export class User extends ExtendedBaseEntity { @Column("simple-array", { nullable: true }) backup_codes: string[]; + @OneToMany(() => Subscription, (subscription) => subscription.user) + subscriptions: Subscription[]; + createPasswordResetToken(): string { const resetToken = crypto.randomBytes(32).toString("hex"); diff --git a/src/routes/plans.ts b/src/routes/plans.ts new file mode 100644 index 00000000..ae278445 --- /dev/null +++ b/src/routes/plans.ts @@ -0,0 +1,50 @@ +import { Router } from "express"; +import { + getCurrentPlan, + comparePlans, + createPlan, + updatePlan, + deletePlan, +} from "../controllers/planController"; +import { authorizeRole } from "../middleware/authorisationSuperAdmin"; +import { authMiddleware, validOrgAdmin } from "../middleware"; +import { UserRole } from "../enums/userRoles"; + +const planRouter = Router(); + +planRouter.get( + "admin/{userId}/current-plan", + authMiddleware, + authorizeRole([UserRole.SUPER_ADMIN]), + getCurrentPlan, +); + +planRouter.get( + "admin/plans", + authMiddleware, + authorizeRole([UserRole.SUPER_ADMIN]), + comparePlans, +); + +planRouter.post( + "admin/plans", + authMiddleware, + authorizeRole([UserRole.SUPER_ADMIN]), + createPlan, +); + +planRouter.delete( + "/admin/{userId}/current-plan", + authMiddleware, + authorizeRole([UserRole.SUPER_ADMIN]), + deletePlan, +); + +planRouter.put( + "/admin/{userId}/current-plan", + authMiddleware, + authorizeRole([UserRole.SUPER_ADMIN]), + updatePlan, +); + +export { planRouter }; diff --git a/src/services/super-admin-plans.ts b/src/services/super-admin-plans.ts new file mode 100644 index 00000000..5b9c3600 --- /dev/null +++ b/src/services/super-admin-plans.ts @@ -0,0 +1,117 @@ +import AppDataSource from "../data-source"; +import { Subscription } from "../models/subcription"; +import { Plan } from "../models/plan"; + +export class PlanService { + private subscriptionRepo = AppDataSource.getRepository(Subscription); + private planRepo = AppDataSource.getRepository(Plan); + + async getCurrentPlan(userId: string) { + try { + const subscription = await this.subscriptionRepo.findOne({ + where: { user: { id: userId }, status: "Active" }, + relations: ["plan"], + }); + + if (!subscription) { + throw new Error("User or subscription not found"); + } + + return { + planName: subscription.plan.name, + planPrice: subscription.plan.price, + features: subscription.plan.features, + startDate: subscription.startDate, + renewalDate: subscription.renewalDate, + status: subscription.status, + }; + } catch (error) { + throw new Error("Server error"); + } + } + + async createPlan(planData: { + name: string; + price: number; + features?: string; + limitations?: string; + }) { + const { name, price, features, limitations } = planData; + + if (!name || typeof price !== "number" || price <= 0) { + throw new Error("Invalid input"); + } + + const existingPlan = await this.planRepo.findOne({ where: { name } }); + + if (existingPlan) { + throw new Error("Plan already exists"); + } + const newPlan = this.planRepo.create({ + name, + price, + features: [features], + limitations, + }); + await this.planRepo.save(newPlan); + + return newPlan; + } + + async updatePlan(id: string, updateData: Partial) { + try { + const plan = await this.planRepo.findOne({ where: { id } }); + + if (!plan) { + throw new Error("Plan not found"); + } + + if ( + updateData.price !== undefined && + (typeof updateData.price !== "number" || updateData.price <= 0) + ) { + throw new Error("Invalid price"); + } + + Object.assign(plan, updateData); + + await this.planRepo.save(plan); + + return plan; + } catch (error) { + throw new Error("Server error"); + } + } + + async comparePlans() { + try { + return await this.planRepo.find(); + } catch (error) { + throw new Error("Server error"); + } + } + + async deletePlan(id: string) { + try { + const plan = await this.planRepo.findOne({ where: { id } }); + + if (!plan) { + throw new Error("Plan not found"); + } + + const hasDependencies = await this.subscriptionRepo.count({ + where: { plan }, + }); + + if (hasDependencies > 0) { + throw new Error("Cannot delete plan with active subscriptions"); + } + + await this.planRepo.remove(plan); + + return { message: "Plan deleted successfully" }; + } catch (error) { + throw new Error("Server error"); + } + } +}