feat: implement sysadmin subscription management and plan usage cycle recalculation tools
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { Document, Model, Schema, model } from "mongoose";
|
||||
import {
|
||||
CreatePlanUsageCycleParams,
|
||||
FindPlanUsageCycleParams,
|
||||
IPlanUsageCycle,
|
||||
IPlanUsageCycleAdapter,
|
||||
UpdatePlanUsageCycleParams,
|
||||
} from "./PlanUsageCycle.Interface";
|
||||
|
||||
export interface IPlanUsageCycleDocument extends Omit<IPlanUsageCycle, "id">, Document {}
|
||||
|
||||
export class PlanUsageCycleAdapterMongoose implements IPlanUsageCycleAdapter {
|
||||
schema: Schema;
|
||||
planUsageCycleList: Model<IPlanUsageCycleDocument>;
|
||||
|
||||
constructor() {
|
||||
this.schema = new Schema({
|
||||
userId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
|
||||
subscriptionId: { type: Schema.Types.ObjectId, required: false, ref: "PlanSuscription" },
|
||||
planId: { type: Schema.Types.ObjectId, required: true, ref: "Plan" },
|
||||
cycleStart: { type: Date, required: true },
|
||||
cycleEnd: { type: Date, required: true },
|
||||
appointmentsCount: { type: Number, required: true, default: 0 },
|
||||
creationDate: { type: Date, required: true, default: Date.now },
|
||||
updateDate: { type: Date, required: true, default: Date.now },
|
||||
});
|
||||
|
||||
this.schema.index({ userId: 1, subscriptionId: 1, planId: 1, cycleStart: 1 }, { unique: true });
|
||||
|
||||
this.planUsageCycleList = model<IPlanUsageCycleDocument>("PlanUsageCycle", this.schema);
|
||||
}
|
||||
|
||||
public async findOne(filters: FindPlanUsageCycleParams): Promise<IPlanUsageCycleDocument | null> {
|
||||
return await this.planUsageCycleList.findOne(filters);
|
||||
}
|
||||
|
||||
public async create(data: CreatePlanUsageCycleParams): Promise<IPlanUsageCycleDocument> {
|
||||
return await this.planUsageCycleList.create({
|
||||
...data,
|
||||
appointmentsCount: data.appointmentsCount || 0,
|
||||
updateDate: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getOrCreateCurrent(data: UpdatePlanUsageCycleParams): Promise<IPlanUsageCycleDocument> {
|
||||
const now = new Date();
|
||||
return await this.planUsageCycleList.findOneAndUpdate(
|
||||
{
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
},
|
||||
{
|
||||
$setOnInsert: {
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
cycleEnd: data.cycleEnd,
|
||||
appointmentsCount: 0,
|
||||
creationDate: now,
|
||||
},
|
||||
$set: { updateDate: now },
|
||||
},
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
public async incrementAppointments(
|
||||
data: UpdatePlanUsageCycleParams & { quantity: number }
|
||||
): Promise<IPlanUsageCycleDocument | null> {
|
||||
const now = new Date();
|
||||
return await this.planUsageCycleList.findOneAndUpdate(
|
||||
{
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
},
|
||||
{
|
||||
$setOnInsert: {
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
cycleEnd: data.cycleEnd,
|
||||
appointmentsCount: 0,
|
||||
creationDate: now,
|
||||
},
|
||||
$inc: { appointmentsCount: data.quantity },
|
||||
$set: { updateDate: now },
|
||||
},
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
public async incrementAppointmentsIfWithinLimit(
|
||||
data: UpdatePlanUsageCycleParams & { quantity: number; limit: number }
|
||||
): Promise<IPlanUsageCycleDocument | null> {
|
||||
const now = new Date();
|
||||
return await this.planUsageCycleList.findOneAndUpdate(
|
||||
{
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
appointmentsCount: { $lte: data.limit - data.quantity },
|
||||
},
|
||||
{
|
||||
$setOnInsert: {
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
cycleEnd: data.cycleEnd,
|
||||
appointmentsCount: 0,
|
||||
creationDate: now,
|
||||
},
|
||||
$inc: { appointmentsCount: data.quantity },
|
||||
$set: { updateDate: now },
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
}
|
||||
|
||||
public async setAppointmentsCount(
|
||||
data: UpdatePlanUsageCycleParams & { appointmentsCount: number }
|
||||
): Promise<IPlanUsageCycleDocument | null> {
|
||||
const now = new Date();
|
||||
return await this.planUsageCycleList.findOneAndUpdate(
|
||||
{
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
},
|
||||
{
|
||||
$setOnInsert: {
|
||||
userId: data.userId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
planId: data.planId,
|
||||
cycleStart: data.cycleStart,
|
||||
cycleEnd: data.cycleEnd,
|
||||
creationDate: now,
|
||||
},
|
||||
$set: {
|
||||
appointmentsCount: Math.max(data.appointmentsCount, 0),
|
||||
updateDate: now,
|
||||
},
|
||||
},
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { IPlanUsageCycleDocument } from "./PlanUsageCycle.Adapter.Mongoose";
|
||||
|
||||
export interface IPlanUsageCycle {
|
||||
id?: string;
|
||||
userId: string;
|
||||
subscriptionId?: string;
|
||||
planId: string;
|
||||
cycleStart: Date;
|
||||
cycleEnd: Date;
|
||||
appointmentsCount: number;
|
||||
creationDate: Date;
|
||||
updateDate: Date;
|
||||
}
|
||||
|
||||
export type FindPlanUsageCycleParams = {
|
||||
userId?: string;
|
||||
subscriptionId?: string;
|
||||
planId?: string;
|
||||
cycleStart?: Date;
|
||||
cycleEnd?: Date;
|
||||
};
|
||||
|
||||
export type CreatePlanUsageCycleParams = {
|
||||
userId: string;
|
||||
subscriptionId?: string;
|
||||
planId: string;
|
||||
cycleStart: Date;
|
||||
cycleEnd: Date;
|
||||
appointmentsCount?: number;
|
||||
};
|
||||
|
||||
export type CurrentPlanUsageCycleParams = {
|
||||
userId: string;
|
||||
now?: Date;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type SetPlanUsageCycleAppointmentsParams = CurrentPlanUsageCycleParams & {
|
||||
appointmentsCount: number;
|
||||
};
|
||||
|
||||
export type SysAdminRecalculatePlanUsageCycleParams = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type SysAdminRecalculatePlanUsageCycleResult = {
|
||||
userId: string;
|
||||
subscriptionId: string;
|
||||
planId: string;
|
||||
cycleStart: Date;
|
||||
cycleEnd: Date;
|
||||
appointmentsCount: number;
|
||||
};
|
||||
|
||||
export type UpdatePlanUsageCycleParams = {
|
||||
userId: string;
|
||||
subscriptionId: string;
|
||||
planId: string;
|
||||
cycleStart: Date;
|
||||
cycleEnd: Date;
|
||||
};
|
||||
|
||||
export type CalculateCurrentCycleParams = {
|
||||
subscriptionStartDate: Date;
|
||||
now?: Date;
|
||||
};
|
||||
|
||||
export type PlanUsageCycleRange = {
|
||||
cycleStart: Date;
|
||||
cycleEnd: Date;
|
||||
};
|
||||
|
||||
export interface IPlanUsageCycleAdapter {
|
||||
findOne(filters: FindPlanUsageCycleParams): Promise<IPlanUsageCycleDocument | null>;
|
||||
create(data: CreatePlanUsageCycleParams): Promise<IPlanUsageCycleDocument>;
|
||||
getOrCreateCurrent(data: UpdatePlanUsageCycleParams): Promise<IPlanUsageCycleDocument>;
|
||||
incrementAppointments(data: UpdatePlanUsageCycleParams & { quantity: number }): Promise<IPlanUsageCycleDocument | null>;
|
||||
incrementAppointmentsIfWithinLimit(
|
||||
data: UpdatePlanUsageCycleParams & { quantity: number; limit: number }
|
||||
): Promise<IPlanUsageCycleDocument | null>;
|
||||
setAppointmentsCount(
|
||||
data: UpdatePlanUsageCycleParams & { appointmentsCount: number }
|
||||
): Promise<IPlanUsageCycleDocument | null>;
|
||||
}
|
||||
|
||||
export interface IPlanUsageCycleManager {
|
||||
planUsageCycle: IPlanUsageCycleAdapter;
|
||||
getCurrentCycle(data: CurrentPlanUsageCycleParams): Promise<IPlanUsageCycleDocument>;
|
||||
addAppointment(data: CurrentPlanUsageCycleParams & { quantity?: number }): Promise<void>;
|
||||
reserveAppointment(data: CurrentPlanUsageCycleParams & { quantity?: number }): Promise<boolean>;
|
||||
releaseAppointment(data: CurrentPlanUsageCycleParams & { quantity?: number }): Promise<void>;
|
||||
getAppointmentsCount(data: CurrentPlanUsageCycleParams): Promise<number>;
|
||||
setAppointmentsCount(data: SetPlanUsageCycleAppointmentsParams): Promise<IPlanUsageCycleDocument | null>;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import dayjs from "dayjs";
|
||||
import { CalculateCurrentCycleParams, PlanUsageCycleRange } from "./PlanUsageCycle.Interface";
|
||||
|
||||
export function calculateCurrentPlanUsageCycle(data: CalculateCurrentCycleParams): PlanUsageCycleRange {
|
||||
const now = dayjs(data.now || new Date());
|
||||
const anchor = dayjs(data.subscriptionStartDate);
|
||||
|
||||
let cycleStart = anchor.clone();
|
||||
let cycleEnd = cycleStart.clone().add(1, "month");
|
||||
|
||||
while (!cycleEnd.isAfter(now)) {
|
||||
cycleStart = cycleEnd;
|
||||
cycleEnd = cycleStart.clone().add(1, "month");
|
||||
}
|
||||
|
||||
return {
|
||||
cycleStart: cycleStart.toDate(),
|
||||
cycleEnd: cycleEnd.toDate(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { isNull } from "../../helpers/IsNull";
|
||||
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
|
||||
import {
|
||||
CurrentPlanUsageCycleParams,
|
||||
IPlanUsageCycleManager,
|
||||
SetPlanUsageCycleAppointmentsParams,
|
||||
UpdatePlanUsageCycleParams,
|
||||
} from "./PlanUsageCycle.Interface";
|
||||
import {
|
||||
IPlanUsageCycleDocument,
|
||||
PlanUsageCycleAdapterMongoose,
|
||||
} from "./PlanUsageCycle.Adapter.Mongoose";
|
||||
import { calculateCurrentPlanUsageCycle } from "./PlanUsageCycle.helpers";
|
||||
|
||||
export { calculateCurrentPlanUsageCycle } from "./PlanUsageCycle.helpers";
|
||||
|
||||
class PlanUsageCycleManager implements IPlanUsageCycleManager {
|
||||
planUsageCycle: PlanUsageCycleAdapterMongoose;
|
||||
|
||||
constructor() {
|
||||
this.planUsageCycle = new PlanUsageCycleAdapterMongoose();
|
||||
}
|
||||
|
||||
private async getCurrentCycleData(data: CurrentPlanUsageCycleParams): Promise<UpdatePlanUsageCycleParams> {
|
||||
const subscription = await PlanSubscriptionsList.findOne({
|
||||
sessionUser: data.userId,
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("No se ha encontrado una suscripción activa");
|
||||
}
|
||||
|
||||
const { cycleStart, cycleEnd } = calculateCurrentPlanUsageCycle({
|
||||
subscriptionStartDate: subscription.startDate,
|
||||
now: data.now,
|
||||
});
|
||||
|
||||
return {
|
||||
userId: data.userId,
|
||||
subscriptionId: String(subscription.id),
|
||||
planId: String(subscription.planId),
|
||||
cycleStart,
|
||||
cycleEnd,
|
||||
};
|
||||
}
|
||||
|
||||
public async getCurrentCycle(data: CurrentPlanUsageCycleParams): Promise<IPlanUsageCycleDocument> {
|
||||
return await this.planUsageCycle.getOrCreateCurrent(await this.getCurrentCycleData(data));
|
||||
}
|
||||
|
||||
public async addAppointment(data: CurrentPlanUsageCycleParams & { quantity?: number }): Promise<void> {
|
||||
await this.planUsageCycle.incrementAppointments({
|
||||
...(await this.getCurrentCycleData(data)),
|
||||
quantity: isNull<number>(data.quantity, 1),
|
||||
});
|
||||
}
|
||||
|
||||
public async reserveAppointment(data: CurrentPlanUsageCycleParams & { quantity?: number }): Promise<boolean> {
|
||||
if (typeof data.limit !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const quantity = isNull<number>(data.quantity, 1);
|
||||
const usageCycleData = await this.getCurrentCycleData(data);
|
||||
const usageCycle = await this.planUsageCycle.getOrCreateCurrent(usageCycleData);
|
||||
|
||||
if (usageCycle.appointmentsCount + quantity > data.limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updatedCycle = await this.planUsageCycle.incrementAppointmentsIfWithinLimit({
|
||||
...usageCycleData,
|
||||
quantity,
|
||||
limit: data.limit,
|
||||
});
|
||||
|
||||
return !!updatedCycle;
|
||||
}
|
||||
|
||||
public async releaseAppointment(data: CurrentPlanUsageCycleParams & { quantity?: number }): Promise<void> {
|
||||
await this.addAppointment({
|
||||
...data,
|
||||
quantity: -Math.abs(isNull<number>(data.quantity, 1)),
|
||||
});
|
||||
}
|
||||
|
||||
public async getAppointmentsCount(data: CurrentPlanUsageCycleParams): Promise<number> {
|
||||
const usageCycle = await this.getCurrentCycle(data);
|
||||
return usageCycle.appointmentsCount;
|
||||
}
|
||||
|
||||
public async setAppointmentsCount(data: SetPlanUsageCycleAppointmentsParams): Promise<IPlanUsageCycleDocument | null> {
|
||||
return await this.planUsageCycle.setAppointmentsCount({
|
||||
...(await this.getCurrentCycleData(data)),
|
||||
appointmentsCount: data.appointmentsCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PlanUsageCycleList = new PlanUsageCycleManager();
|
||||
|
||||
export default PlanUsageCycleList;
|
||||
Reference in New Issue
Block a user