feat: add manual subscription finalization functionality and support for expired plan notifications
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
FindPlanSuscripcionsParams,
|
||||
IPlanSuscriptionsAdapter,
|
||||
@@ -53,6 +52,11 @@ export class PlanSuscriptionsAdapterMongoose implements IPlanSuscriptionsAdapter
|
||||
lastPaymentPreferenceId: { type: String, required: false },
|
||||
lastPaymentAt: { type: Date, required: false },
|
||||
lastPaymentType: { type: String, required: false, enum: ["new", "extension", "upgrade"] },
|
||||
downgradedFromPlanId: { type: Schema.Types.ObjectId, required: false, ref: "Plan" },
|
||||
downgradedFromPlanName: { type: String, required: false },
|
||||
downgradedFromPlanCode: { type: String, required: false },
|
||||
downgradedAt: { type: Date, required: false },
|
||||
downgradeReason: { type: String, required: false, enum: ["expired"] },
|
||||
});
|
||||
|
||||
this.planSuscriptionList = model<IPlanSuscriptionDocument>("PlanSuscription", this.schema);
|
||||
@@ -89,12 +93,12 @@ export class PlanSuscriptionsAdapterMongoose implements IPlanSuscriptionsAdapter
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date());
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
const dateNow = new Date();
|
||||
const dateEnd = new Date(subscription.endDate);
|
||||
|
||||
try {
|
||||
if (subscription.mpPreferenceId) {
|
||||
if (dateNow.isAfter(dateEnd)) {
|
||||
if (dateNow > dateEnd) {
|
||||
if (subscription.mpStatus == MP_SUBS_STATUS.CANCELLED) {
|
||||
subscription.mpPreferenceId = "";
|
||||
subscription.isActive = false;
|
||||
|
||||
@@ -36,6 +36,11 @@ export type CreatePlanSuscriptionParams = {
|
||||
lastPaymentPreferenceId?: string;
|
||||
lastPaymentAt?: Date;
|
||||
lastPaymentType?: LastPlanPaymentType;
|
||||
downgradedFromPlanId?: string;
|
||||
downgradedFromPlanName?: string;
|
||||
downgradedFromPlanCode?: string;
|
||||
downgradedAt?: Date;
|
||||
downgradeReason?: DowngradeReason;
|
||||
};
|
||||
|
||||
export type CancellPlanSuscriptionParams = {
|
||||
@@ -65,6 +70,7 @@ export type MPPreApprovalResponse = {
|
||||
|
||||
export type ToFreePlanParams = {
|
||||
sessionUser: string;
|
||||
downgradeReason?: DowngradeReason;
|
||||
};
|
||||
|
||||
export type ToMpParams = {
|
||||
@@ -101,6 +107,7 @@ export type VerifyPendingPlanPaymentResponse = {
|
||||
|
||||
export type LastPlanPaymentStatus = "rejected" | "failed" | "cancelled";
|
||||
export type LastPlanPaymentType = "new" | "extension" | "upgrade";
|
||||
export type DowngradeReason = "expired";
|
||||
|
||||
export interface GetSuscriptionInitPointResponse {
|
||||
init_point: string;
|
||||
@@ -137,6 +144,11 @@ export interface IPlanSuscription {
|
||||
lastPaymentPreferenceId?: string;
|
||||
lastPaymentAt?: Date;
|
||||
lastPaymentType?: LastPlanPaymentType;
|
||||
downgradedFromPlanId?: string;
|
||||
downgradedFromPlanName?: string;
|
||||
downgradedFromPlanCode?: string;
|
||||
downgradedAt?: Date;
|
||||
downgradeReason?: DowngradeReason;
|
||||
}
|
||||
|
||||
export interface ISubscriptionInfo {
|
||||
@@ -155,6 +167,11 @@ export interface ISubscriptionInfo {
|
||||
lastPaymentPreferenceId?: string;
|
||||
lastPaymentAt?: Date;
|
||||
lastPaymentType?: LastPlanPaymentType;
|
||||
downgradedFromPlanId?: string;
|
||||
downgradedFromPlanName?: string;
|
||||
downgradedFromPlanCode?: string;
|
||||
downgradedAt?: Date;
|
||||
downgradeReason?: DowngradeReason;
|
||||
}
|
||||
|
||||
export interface IPlanSuscriptionsAdapter {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import axios from "axios";
|
||||
import PlansList from "../../Models/Plans/Plans";
|
||||
import dayjs from "dayjs";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||
import "dayjs/locale/es";
|
||||
import { isNull } from "../../helpers/IsNull";
|
||||
import { PlanFeatures } from "../Plans/Plans.interface";
|
||||
@@ -34,7 +33,6 @@ import { NotificationType } from "../../Models/SystemNotifications/SystemNotific
|
||||
import { calculateProratedUpgradeAmount } from "./PlanSubscriptions.pricing";
|
||||
|
||||
dayjs.locale("es");
|
||||
dayjs.extend(isSameOrAfter);
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -195,6 +193,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
subscription.autoRenew = false;
|
||||
subscription.mpStatus = "";
|
||||
subscription.mpInitPoint = dashboardUrl || "";
|
||||
subscription.downgradedFromPlanId = undefined;
|
||||
subscription.downgradedFromPlanName = undefined;
|
||||
subscription.downgradedFromPlanCode = undefined;
|
||||
subscription.downgradedAt = undefined;
|
||||
subscription.downgradeReason = undefined;
|
||||
await subscription.save();
|
||||
}
|
||||
} else {
|
||||
@@ -381,6 +384,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
currentSubscription.lastPaymentPreferenceId = undefined;
|
||||
currentSubscription.lastPaymentAt = undefined;
|
||||
currentSubscription.lastPaymentType = undefined;
|
||||
currentSubscription.downgradedFromPlanId = undefined;
|
||||
currentSubscription.downgradedFromPlanName = undefined;
|
||||
currentSubscription.downgradedFromPlanCode = undefined;
|
||||
currentSubscription.downgradedAt = undefined;
|
||||
currentSubscription.downgradeReason = undefined;
|
||||
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
||||
await currentSubscription.save();
|
||||
|
||||
@@ -409,6 +417,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
currentSubscription.lastPaymentPreferenceId = undefined;
|
||||
currentSubscription.lastPaymentAt = undefined;
|
||||
currentSubscription.lastPaymentType = undefined;
|
||||
currentSubscription.downgradedFromPlanId = undefined;
|
||||
currentSubscription.downgradedFromPlanName = undefined;
|
||||
currentSubscription.downgradedFromPlanCode = undefined;
|
||||
currentSubscription.downgradedAt = undefined;
|
||||
currentSubscription.downgradeReason = undefined;
|
||||
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
||||
await currentSubscription.save();
|
||||
|
||||
@@ -447,6 +460,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
lastPaymentPreferenceId: undefined,
|
||||
lastPaymentAt: undefined,
|
||||
lastPaymentType: undefined,
|
||||
downgradedFromPlanId: undefined,
|
||||
downgradedFromPlanName: undefined,
|
||||
downgradedFromPlanCode: undefined,
|
||||
downgradedAt: undefined,
|
||||
downgradeReason: undefined,
|
||||
});
|
||||
|
||||
await NotificationsManager.sendSystemNotification({
|
||||
@@ -493,12 +511,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
// We just cancel it locally if not paid, or change to free plan if paid.
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date()).endOf("day");
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
const dateNow = new Date();
|
||||
const dateEnd = new Date(subscription.endDate);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd)) {
|
||||
if (dateNow > dateEnd) {
|
||||
await this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
downgradeReason: "expired",
|
||||
});
|
||||
} else {
|
||||
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
|
||||
@@ -573,6 +592,10 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
|
||||
//console.log("guardando el plan gratuito...");
|
||||
//try {
|
||||
const downgradedFromPlan = data.downgradeReason === "expired"
|
||||
? await PlansList.plans.findOne({ _id: String(subscription.planId) })
|
||||
: null;
|
||||
|
||||
console.log("cambiando al free plan:", freePlan.id);
|
||||
subscription.planId = freePlan.id;
|
||||
subscription.mpStatus = "";
|
||||
@@ -593,6 +616,19 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
subscription.pendingPaymentPeriodEndDate = undefined;
|
||||
subscription.startDate = new Date();
|
||||
subscription.endDate = dayjs(new Date()).add(10, "years").toDate();
|
||||
if (data.downgradeReason === "expired" && downgradedFromPlan) {
|
||||
subscription.downgradedFromPlanId = String(downgradedFromPlan.id || downgradedFromPlan._id);
|
||||
subscription.downgradedFromPlanName = downgradedFromPlan.name;
|
||||
subscription.downgradedFromPlanCode = downgradedFromPlan.code;
|
||||
subscription.downgradedAt = new Date();
|
||||
subscription.downgradeReason = "expired";
|
||||
} else {
|
||||
subscription.downgradedFromPlanId = undefined;
|
||||
subscription.downgradedFromPlanName = undefined;
|
||||
subscription.downgradedFromPlanCode = undefined;
|
||||
subscription.downgradedAt = undefined;
|
||||
subscription.downgradeReason = undefined;
|
||||
}
|
||||
|
||||
await subscription.save();
|
||||
// console.log("plan gratuito guardado:", freePlan.id);
|
||||
@@ -621,6 +657,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
||||
lastPaymentAt: subscription.lastPaymentAt,
|
||||
lastPaymentType: subscription.lastPaymentType,
|
||||
downgradedFromPlanId: subscription.downgradedFromPlanId,
|
||||
downgradedFromPlanName: subscription.downgradedFromPlanName,
|
||||
downgradedFromPlanCode: subscription.downgradedFromPlanCode,
|
||||
downgradedAt: subscription.downgradedAt,
|
||||
downgradeReason: subscription.downgradeReason,
|
||||
};
|
||||
|
||||
io.to(`user:${String(subscription.userId)}`).emit("suscription_status_updated", susInfo);
|
||||
@@ -637,12 +678,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date()).endOf("day");
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
const dateNow = new Date();
|
||||
const dateEnd = new Date(subscription.endDate);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd) && this.shouldDowngradeExpiredSubscription(subscription)) {
|
||||
if (dateNow > dateEnd && this.shouldDowngradeExpiredSubscription(subscription)) {
|
||||
await this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
downgradeReason: "expired",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -658,12 +700,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date()).endOf("day");
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
const dateNow = new Date();
|
||||
const dateEnd = new Date(subscription.endDate);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd) && this.shouldDowngradeExpiredSubscription(subscription)) {
|
||||
if (dateNow > dateEnd && this.shouldDowngradeExpiredSubscription(subscription)) {
|
||||
await this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
downgradeReason: "expired",
|
||||
});
|
||||
return this.getSubscriptionByUser(data);
|
||||
}
|
||||
@@ -701,6 +744,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
||||
lastPaymentAt: subscription.lastPaymentAt,
|
||||
lastPaymentType: subscription.lastPaymentType,
|
||||
downgradedFromPlanId: subscription.downgradedFromPlanId,
|
||||
downgradedFromPlanName: subscription.downgradedFromPlanName,
|
||||
downgradedFromPlanCode: subscription.downgradedFromPlanCode,
|
||||
downgradedAt: subscription.downgradedAt,
|
||||
downgradeReason: subscription.downgradeReason,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,18 @@ export type SysAdminExtendUserSubscriptionResult = {
|
||||
transactionId?: string;
|
||||
};
|
||||
|
||||
export type SysAdminFinalizeUserSubscriptionParams = {
|
||||
userId: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type SysAdminFinalizeUserSubscriptionResult = {
|
||||
userId: string;
|
||||
subscriptionId: string;
|
||||
previousEndDate: Date;
|
||||
newEndDate: Date;
|
||||
};
|
||||
|
||||
export type SysAdminUserSubscriptionPayment = {
|
||||
id: string;
|
||||
subscriptionId: string;
|
||||
|
||||
@@ -328,6 +328,11 @@ export class MercadoPagoWebhookService {
|
||||
subscription.lastPaymentPreferenceId = undefined;
|
||||
subscription.lastPaymentAt = undefined;
|
||||
subscription.lastPaymentType = undefined;
|
||||
subscription.downgradedFromPlanId = undefined;
|
||||
subscription.downgradedFromPlanName = undefined;
|
||||
subscription.downgradedFromPlanCode = undefined;
|
||||
subscription.downgradedAt = undefined;
|
||||
subscription.downgradeReason = undefined;
|
||||
await subscription.save();
|
||||
|
||||
await PlanPaymentsList.createIfMissingByTransactionId({
|
||||
|
||||
@@ -4,6 +4,8 @@ import { sysAdminAuthMiddleware } from "../../../middleware/sysadminAuth";
|
||||
import {
|
||||
SysAdminExtendUserSubscriptionParams,
|
||||
SysAdminExtendUserSubscriptionResult,
|
||||
SysAdminFinalizeUserSubscriptionParams,
|
||||
SysAdminFinalizeUserSubscriptionResult,
|
||||
SysAdminUserSubscriptionDetailsParams,
|
||||
SysAdminUserSubscriptionDetailsResult,
|
||||
} from "../../../Models/Users/Users.Interface";
|
||||
@@ -29,6 +31,23 @@ export class SysAdminSubscriptionsController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post("finalize-user")
|
||||
public async finalizeUserSubscription(
|
||||
@Body() requestBody: SysAdminFinalizeUserSubscriptionParams
|
||||
): Promise<SysAdminFinalizeUserSubscriptionResult | ApiValidationError> {
|
||||
try {
|
||||
const result = await new SysAdminSubscriptionsService().finalizeUserSubscription(requestBody);
|
||||
this.setStatus(200);
|
||||
return result;
|
||||
} catch (e) {
|
||||
const errorOccurred: Error = e as Error;
|
||||
this.setStatus(500);
|
||||
return new ApiValidationError(500, errorOccurred.message);
|
||||
}
|
||||
}
|
||||
|
||||
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post("user-details")
|
||||
|
||||
@@ -6,6 +6,8 @@ import PlansList from "../../../Models/Plans/Plans";
|
||||
import {
|
||||
SysAdminExtendUserSubscriptionParams,
|
||||
SysAdminExtendUserSubscriptionResult,
|
||||
SysAdminFinalizeUserSubscriptionParams,
|
||||
SysAdminFinalizeUserSubscriptionResult,
|
||||
SysAdminUserSubscriptionDetailsParams,
|
||||
SysAdminUserSubscriptionDetailsResult,
|
||||
} from "../../../Models/Users/Users.Interface";
|
||||
@@ -86,6 +88,42 @@ export class SysAdminSubscriptionsService {
|
||||
};
|
||||
}
|
||||
|
||||
public async finalizeUserSubscription(
|
||||
params: SysAdminFinalizeUserSubscriptionParams
|
||||
): Promise<SysAdminFinalizeUserSubscriptionResult> {
|
||||
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||
|
||||
if (!params.userId || !isValidObjectId(params.userId)) {
|
||||
throw new Error("User id is required");
|
||||
}
|
||||
|
||||
const subscription = await PlanSubscriptionsList.findOne({ sessionUser: params.userId });
|
||||
if (!subscription) {
|
||||
throw new Error("No subscription found for user");
|
||||
}
|
||||
|
||||
const previousEndDate = subscription.endDate ? new Date(subscription.endDate) : new Date();
|
||||
const newEndDate = dayjs().subtract(1, "day").startOf("day").toDate();
|
||||
const plan = await PlansList.plans.findOne({ _id: String(subscription.planId) });
|
||||
|
||||
subscription.endDate = newEndDate;
|
||||
await subscription.save();
|
||||
|
||||
if (plan && plan.price > 0) {
|
||||
await PlanSubscriptionsList.toFreePlan({
|
||||
sessionUser: params.userId,
|
||||
downgradeReason: "expired",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
userId: params.userId,
|
||||
subscriptionId: String(subscription.id),
|
||||
previousEndDate,
|
||||
newEndDate,
|
||||
};
|
||||
}
|
||||
|
||||
public async getUserSubscriptionDetails(
|
||||
params: SysAdminUserSubscriptionDetailsParams
|
||||
): Promise<SysAdminUserSubscriptionDetailsResult> {
|
||||
|
||||
Reference in New Issue
Block a user