Skip to content

Commit

Permalink
feat: Implement API Endpoints for Managing User Plans
Browse files Browse the repository at this point in the history
  • Loading branch information
deolla committed Aug 15, 2024
1 parent 16761cf commit 39dee99
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/controllers/planController.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));

Expand Down
15 changes: 15 additions & 0 deletions src/middleware/authorisationSuperAdmin.ts
Original file line number Diff line number Diff line change
@@ -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();
};
};
20 changes: 20 additions & 0 deletions src/models/plan.ts
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 34 additions & 0 deletions src/models/subcription.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -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");

Expand Down
50 changes: 50 additions & 0 deletions src/routes/plans.ts
Original file line number Diff line number Diff line change
@@ -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 };
117 changes: 117 additions & 0 deletions src/services/super-admin-plans.ts
Original file line number Diff line number Diff line change
@@ -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<Plan>) {
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");
}
}
}

0 comments on commit 39dee99

Please sign in to comment.