first commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
FindPlanSuscripcionsParams,
|
||||
IPlanSuscriptionsAdapter,
|
||||
IPlanSuscription,
|
||||
CreatePlanSuscriptionParams,
|
||||
CancellPlanSuscriptionParams,
|
||||
} from "./PlanSubscriptions.interface";
|
||||
import { Document, Model, Schema, model } from "mongoose";
|
||||
|
||||
export interface IPlanSuscriptionDocument extends Omit<IPlanSuscription, "id">, Document {}
|
||||
|
||||
export enum MP_SUBS_STATUS {
|
||||
PENDING = "pending",
|
||||
AUTHORIZED = "authorized",
|
||||
PAUSED = "paused",
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
|
||||
export class PlanSuscriptionsAdapterMongoose implements IPlanSuscriptionsAdapter {
|
||||
schema: Schema;
|
||||
planSuscriptionList: Model<IPlanSuscriptionDocument>;
|
||||
|
||||
constructor() {
|
||||
this.schema = new Schema({
|
||||
userId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
|
||||
planId: { type: Schema.Types.ObjectId, required: true, ref: "Plan" },
|
||||
mpPlanId: { type: String, required: false },
|
||||
startDate: { type: Date, required: true },
|
||||
endDate: { type: Date, required: true },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
autoRenew: { type: Boolean, required: true, default: true },
|
||||
paymentMethod: { type: String, required: false },
|
||||
mpPayerId: { type: Number, required: false },
|
||||
mpPayerEmail: { type: String, required: false },
|
||||
mpStatus: { type: String, required: false },
|
||||
mpDateCreated: { type: Date, required: false },
|
||||
mpInitPoint: { type: String, required: false },
|
||||
mpPreferenceId: { type: String, required: false },
|
||||
});
|
||||
|
||||
this.planSuscriptionList = model<IPlanSuscriptionDocument>("PlanSuscription", this.schema);
|
||||
}
|
||||
|
||||
public async find(filters: FindPlanSuscripcionsParams): Promise<IPlanSuscription[]> {
|
||||
return this.planSuscriptionList.find(filters).exec();
|
||||
}
|
||||
|
||||
public async delete(data: CancellPlanSuscriptionParams): Promise<void> {
|
||||
await this.planSuscriptionList.deleteOne({ userId: data.sessionUser }).exec();
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
filters: FindPlanSuscripcionsParams
|
||||
): Promise<IPlanSuscriptionDocument | null> {
|
||||
let subscription = await this.planSuscriptionList
|
||||
.findOne({ userId: filters.sessionUser })
|
||||
.exec();
|
||||
|
||||
if (!subscription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date());
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
|
||||
try {
|
||||
if (subscription.mpPreferenceId) {
|
||||
if (dateNow.isAfter(dateEnd)) {
|
||||
if (subscription.mpStatus == MP_SUBS_STATUS.CANCELLED) {
|
||||
subscription.mpPreferenceId = "";
|
||||
subscription.isActive = false;
|
||||
subscription.autoRenew = false;
|
||||
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
|
||||
subscription.mpInitPoint = "";
|
||||
await subscription.save();
|
||||
return subscription;
|
||||
}
|
||||
// Without subscriptions, we wait for the webhook to update the status.
|
||||
// If it's expired and not updated by webhook, we just cancel it.
|
||||
if (subscription.mpStatus === MP_SUBS_STATUS.PENDING) {
|
||||
subscription.mpPreferenceId = "";
|
||||
subscription.isActive = false;
|
||||
subscription.autoRenew = false;
|
||||
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
|
||||
subscription.mpInitPoint = "";
|
||||
await subscription.save();
|
||||
return subscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Ha ocurrido un error al revisar el estado de la suscripcion:", e);
|
||||
}
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
// updateMpStatus was removed as it relied on MercadoPago Subscriptions API.
|
||||
// Payments status updates are now handled by the WebhookController.
|
||||
|
||||
public async create(data: CreatePlanSuscriptionParams): Promise<IPlanSuscriptionDocument> {
|
||||
return await this.planSuscriptionList.create(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { IPlan, PlanFeatures } from "../../Models/Plans/Plans.interface";
|
||||
import { IPlanSuscriptionDocument } from "./PlanSubscriptions.Adapter.Mongoose";
|
||||
|
||||
export type FindPlanSuscripcionsParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type CreatePlanSuscriptionParams = {
|
||||
userId: string;
|
||||
planId: string;
|
||||
mpPlanId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
autoRenew: boolean;
|
||||
mpPayerId?: number;
|
||||
mpPayerEmail?: string;
|
||||
mpStatus?: string;
|
||||
mpDateCreated?: Date;
|
||||
mpInitPoint?: string;
|
||||
mpPreferenceId?: string;
|
||||
};
|
||||
|
||||
export type CancellPlanSuscriptionParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type CreateMPPreApprovalParams = {
|
||||
planId: string;
|
||||
sessionUser: string;
|
||||
months: number;
|
||||
};
|
||||
|
||||
export type CheckFeatureParams = {
|
||||
userId: string;
|
||||
feature: PlanFeatures;
|
||||
};
|
||||
|
||||
export enum MP_PRE_APPROVAL_STATUS {
|
||||
READY_TO_START = "ready_to_start",
|
||||
NO_PAYER_EMAIL = "no_payer_email",
|
||||
}
|
||||
|
||||
export type MPPreApprovalResponse = {
|
||||
init_point: string;
|
||||
status: MP_PRE_APPROVAL_STATUS;
|
||||
};
|
||||
|
||||
export type ToFreePlanParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type ToMpParams = {
|
||||
planId: string;
|
||||
sessionUser: string;
|
||||
months: number;
|
||||
};
|
||||
|
||||
export type CheckMpStatusParams = {
|
||||
mpPreferenceId: string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type DeletePlanSuscriptionByUserParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type GetSubscriptionInitPointParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export interface GetSuscriptionInitPointResponse {
|
||||
init_point: string;
|
||||
}
|
||||
|
||||
export interface IPlanSuscription {
|
||||
id?: string;
|
||||
userId: string; // Referencia al ID del usuario
|
||||
planId: string; // Referencia al ID del plan suscrito
|
||||
mpPlanId: string; //Referencia al ultimo plan contratado por mercadopago.
|
||||
startDate: Date; // Fecha de inicio de la suscripción
|
||||
endDate: Date; // Fecha de finalización de la suscripción
|
||||
isActive: boolean; // Estado de la suscripción (activa o no)
|
||||
autoRenew: boolean; // Indica si la suscripción se renueva automáticamente
|
||||
paymentMethod: string; // Método de pago usado para la suscripción
|
||||
mpPayerId: number; // ID del pagador en MercadoPago.
|
||||
mpPayerEmail: string; // Email del pagador en MercadoPago.
|
||||
mpStatus: string; // Estado de la suscripción en MercadoPago.
|
||||
mpDateCreated: Date; // Fecha de creación de la suscripción en MercadoPago.
|
||||
mpInitPoint: string; // URL de inicio de la suscripción en MercadoPago.
|
||||
mpPreferenceId: string; // ID de la preferencia en MercadoPago.
|
||||
}
|
||||
|
||||
export interface ISubscriptionInfo {
|
||||
id: string;
|
||||
plan: IPlan;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
isActive: boolean;
|
||||
autoRenew: boolean;
|
||||
mpStatus: string;
|
||||
mpDateCreated: Date;
|
||||
}
|
||||
|
||||
export interface IPlanSuscriptionsAdapter {
|
||||
find(filters: FindPlanSuscripcionsParams): Promise<IPlanSuscription[]>;
|
||||
findOne(data: FindPlanSuscripcionsParams): Promise<IPlanSuscriptionDocument | null>;
|
||||
create(data: CreatePlanSuscriptionParams): Promise<IPlanSuscriptionDocument>;
|
||||
delete(data: CancellPlanSuscriptionParams): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IPlanSuscriptionsManager {
|
||||
planSuscriptions: IPlanSuscriptionsAdapter;
|
||||
create(data: CreateMPPreApprovalParams): Promise<MPPreApprovalResponse>;
|
||||
cancell(data: CancellPlanSuscriptionParams): Promise<void>;
|
||||
findOne(data: FindPlanSuscripcionsParams): Promise<IPlanSuscriptionDocument | null>;
|
||||
getSubscriptionByUser(data: FindPlanSuscripcionsParams): Promise<ISubscriptionInfo | null>;
|
||||
checkFeature(data: CheckFeatureParams): Promise<boolean>;
|
||||
isSubscriptionActive(data: FindPlanSuscripcionsParams): Promise<boolean>;
|
||||
toFreePlan(data: ToFreePlanParams): Promise<void>;
|
||||
toMP(data: ToMpParams): Promise<IPlanSuscriptionDocument>;
|
||||
deletePlanSuscriptionByUser(data: DeletePlanSuscriptionByUserParams): Promise<void>;
|
||||
getInitPoint(data: GetSubscriptionInitPointParams): Promise<GetSuscriptionInitPointResponse>;
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
import { io } from "../../index";
|
||||
import {
|
||||
IPlanSuscriptionDocument,
|
||||
MP_SUBS_STATUS,
|
||||
PlanSuscriptionsAdapterMongoose,
|
||||
} from "./PlanSubscriptions.Adapter.Mongoose";
|
||||
import UsersManager from "../Users/Users";
|
||||
import {
|
||||
CancellPlanSuscriptionParams,
|
||||
CheckFeatureParams,
|
||||
CreateMPPreApprovalParams,
|
||||
DeletePlanSuscriptionByUserParams,
|
||||
FindPlanSuscripcionsParams,
|
||||
GetSubscriptionInitPointParams,
|
||||
GetSuscriptionInitPointResponse,
|
||||
IPlanSuscriptionsManager,
|
||||
ISubscriptionInfo,
|
||||
MP_PRE_APPROVAL_STATUS,
|
||||
MPPreApprovalResponse,
|
||||
ToFreePlanParams,
|
||||
ToMpParams,
|
||||
} from "./PlanSubscriptions.interface";
|
||||
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";
|
||||
import { NotificationsManager } from "../Notifications/Notifications";
|
||||
import WapServerList from "../WapServer/WapServer";
|
||||
import CompaniesList from "../Companies/Companies";
|
||||
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
|
||||
|
||||
dayjs.locale("es");
|
||||
dayjs.extend(isSameOrAfter);
|
||||
|
||||
class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
planSuscriptions: PlanSuscriptionsAdapterMongoose;
|
||||
|
||||
constructor() {
|
||||
this.planSuscriptions = new PlanSuscriptionsAdapterMongoose();
|
||||
}
|
||||
|
||||
public async getInitPoint(
|
||||
data: GetSubscriptionInitPointParams
|
||||
): Promise<GetSuscriptionInitPointResponse> {
|
||||
const sessionUser = await UsersManager.users.findOne({
|
||||
_id: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!sessionUser) {
|
||||
throw new Error(
|
||||
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = await this.planSuscriptions.findOne({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("No se ha encontrado una suscripción activa");
|
||||
}
|
||||
|
||||
const plan = await PlansList.plans.findOne({
|
||||
_id: String(subscription.planId),
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
throw new Error("No se ha encontrado el plan");
|
||||
}
|
||||
|
||||
if (plan.price == 0) {
|
||||
throw new Error("El plan gratuito no requiere el pago de una suscripción");
|
||||
}
|
||||
|
||||
if (!subscription.mpPreferenceId) {
|
||||
throw new Error("No se ha encontrado una suscripción activa");
|
||||
}
|
||||
|
||||
if (subscription.mpStatus !== MP_SUBS_STATUS.PENDING) {
|
||||
throw new Error("No se ha encontrado una suscripción pendiente de pago");
|
||||
}
|
||||
|
||||
if (!subscription.mpInitPoint) {
|
||||
throw new Error("No se ha encontrado una suscripción pendiente de pago");
|
||||
}
|
||||
|
||||
return {
|
||||
init_point: subscription.mpInitPoint,
|
||||
};
|
||||
}
|
||||
|
||||
public async deletePlanSuscriptionByUser(
|
||||
data: DeletePlanSuscriptionByUserParams
|
||||
): Promise<void> {
|
||||
await this.planSuscriptions.delete({ sessionUser: data.sessionUser });
|
||||
}
|
||||
|
||||
public async create(data: CreateMPPreApprovalParams): Promise<MPPreApprovalResponse> {
|
||||
const sessionUser = await UsersManager.users.findOne({
|
||||
_id: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!sessionUser) {
|
||||
throw new Error(
|
||||
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
|
||||
);
|
||||
}
|
||||
|
||||
const plan = await PlansList.plans.findOne({ _id: data.planId });
|
||||
|
||||
if (!plan) {
|
||||
throw new Error("El plan no existe");
|
||||
}
|
||||
|
||||
if (!plan.active) {
|
||||
throw new Error("El plan no esta activo");
|
||||
}
|
||||
|
||||
let subscription = await this.planSuscriptions.findOne({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
let isNewSubscription = false;
|
||||
|
||||
if (!subscription) {
|
||||
const subscriptionStartDate = dayjs(new Date());
|
||||
|
||||
if (plan.price === 0) {
|
||||
isNewSubscription = true;
|
||||
subscription = await this.planSuscriptions.create({
|
||||
userId: data.sessionUser,
|
||||
planId: data.planId,
|
||||
mpPlanId: data.planId,
|
||||
startDate: subscriptionStartDate.toDate(),
|
||||
endDate: subscriptionStartDate.clone().add(data.months || 1, "months").toDate(),
|
||||
autoRenew: true,
|
||||
mpInitPoint: "/landing/dashboard",
|
||||
});
|
||||
} else if (plan.price > 0) {
|
||||
|
||||
|
||||
isNewSubscription = true;
|
||||
subscription = await this.toMP({
|
||||
planId: data.planId,
|
||||
sessionUser: data.sessionUser,
|
||||
months: data.months || 1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const actualPlan = await PlansList.plans.findOne({
|
||||
_id: subscription.planId,
|
||||
});
|
||||
if (!actualPlan) {
|
||||
throw new Error("El plan no existe");
|
||||
}
|
||||
isNewSubscription = true;
|
||||
|
||||
const dashboardUrl = "/landing/dashboard";
|
||||
|
||||
if (plan.price === 0) {
|
||||
if (actualPlan.id !== data.planId) {
|
||||
await this.toFreePlan({ sessionUser: data.sessionUser });
|
||||
subscription = await this.planSuscriptions.findOne({ sessionUser: data.sessionUser });
|
||||
}
|
||||
|
||||
if (subscription) {
|
||||
const subscriptionStartDate = dayjs(new Date());
|
||||
subscription.endDate = subscriptionStartDate.clone().add(data.months || 1, "months").toDate();
|
||||
subscription.mpInitPoint = dashboardUrl || "";
|
||||
await subscription.save();
|
||||
}
|
||||
} else {
|
||||
// Actualizamos el plan si actualmente tiene el plan gratuito.
|
||||
if (actualPlan.id !== data.planId && actualPlan.price === 0) {
|
||||
//realizar la suscripcion...
|
||||
subscription = await this.toMP({
|
||||
planId: data.planId,
|
||||
sessionUser: data.sessionUser,
|
||||
months: data.months || 1,
|
||||
});
|
||||
} else {
|
||||
if (subscription.mpStatus != MP_SUBS_STATUS.CANCELLED && actualPlan.id !== data.planId) {
|
||||
throw new Error(
|
||||
"No se puede cambiar el plan, ya que el usuario ya tiene una suscripción activa. Para cambiarlo primero debes cancelar tu plan actual."
|
||||
);
|
||||
}
|
||||
|
||||
subscription = await this.toMP({
|
||||
planId: data.planId,
|
||||
sessionUser: data.sessionUser,
|
||||
months: data.months || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("No fue posible crear la suscripción");
|
||||
}
|
||||
|
||||
return {
|
||||
init_point: isNewSubscription
|
||||
? subscription.mpInitPoint
|
||||
: isNull<string>(process.env.MP_BACK_URL, ""),
|
||||
status: MP_PRE_APPROVAL_STATUS.READY_TO_START,
|
||||
};
|
||||
}
|
||||
|
||||
public async toMP(data: ToMpParams): Promise<IPlanSuscriptionDocument> {
|
||||
const sessionUser = await UsersManager.users.findOne({
|
||||
_id: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!sessionUser) {
|
||||
throw new Error(
|
||||
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
|
||||
);
|
||||
}
|
||||
|
||||
const plan = await PlansList.plans.findOne({ _id: data.planId });
|
||||
|
||||
if (!plan) {
|
||||
throw new Error("El plan no existe");
|
||||
}
|
||||
|
||||
try {
|
||||
const subscriptionStartDate = dayjs(new Date());
|
||||
|
||||
let discount = 0;
|
||||
if (data.months === 3) discount = plan.discount3Months || 0;
|
||||
else if (data.months === 6) discount = plan.discount6Months || 0;
|
||||
else if (data.months === 12) discount = plan.discount12Months || 0;
|
||||
|
||||
const totalPrice = plan.price * data.months;
|
||||
const finalPrice = totalPrice - (totalPrice * (discount / 100));
|
||||
|
||||
const response = await axios.post(
|
||||
"https://api.mercadopago.com/checkout/preferences",
|
||||
{
|
||||
items: [
|
||||
{
|
||||
title: "TurnosXpress.com.ar - Plan " + plan.name + " (" + data.months + " meses)",
|
||||
quantity: 1,
|
||||
currency_id: "ARS",
|
||||
unit_price: finalPrice
|
||||
}
|
||||
],
|
||||
back_urls: {
|
||||
success: process.env.MP_BACK_URL,
|
||||
failure: process.env.MP_BACK_URL,
|
||||
pending: process.env.MP_BACK_URL
|
||||
},
|
||||
auto_return: "approved",
|
||||
payer: {
|
||||
email: sessionUser.mpPayerEmail || ""
|
||||
},
|
||||
external_reference: String(sessionUser._id),
|
||||
notification_url: process.env.MP_WEBHOOK_URL
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.MP_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const subscriptionData = response.data;
|
||||
|
||||
this.planSuscriptions.delete({ sessionUser: data.sessionUser });
|
||||
|
||||
const subscription = await this.planSuscriptions.create({
|
||||
userId: data.sessionUser,
|
||||
planId: data.planId,
|
||||
mpPlanId: data.planId,
|
||||
startDate: subscriptionStartDate.toDate(),
|
||||
endDate: subscriptionStartDate.clone().add(data.months, "months").toDate(),
|
||||
autoRenew: false,
|
||||
mpPayerEmail: sessionUser.mpPayerEmail,
|
||||
mpStatus: MP_SUBS_STATUS.PENDING,
|
||||
mpDateCreated: new Date(),
|
||||
mpInitPoint: subscriptionData.init_point,
|
||||
mpPreferenceId: subscriptionData.id,
|
||||
});
|
||||
|
||||
await NotificationsManager.sendSystemNotification({
|
||||
userId: String(data.sessionUser),
|
||||
subject: "Se ha creado una nueva suscripción",
|
||||
message: `Se ha creado una nueva suscripción para el plan ${plan.name}.`,
|
||||
type: NotificationType.BILLING,
|
||||
code: String(subscription._id)
|
||||
});
|
||||
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
console.log("ha ocurrido un error:", error);
|
||||
throw new Error("No fue posible crear la suscripción en el proveedor de pagos");
|
||||
}
|
||||
}
|
||||
|
||||
public async cancell(data: CancellPlanSuscriptionParams): Promise<void> {
|
||||
const sessionUser = await UsersManager.users.findOne({
|
||||
_id: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!sessionUser) {
|
||||
throw new Error(
|
||||
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = await this.planSuscriptions.findOne({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("No se ha encontrado una suscripción activa");
|
||||
}
|
||||
|
||||
if (subscription.mpStatus === MP_SUBS_STATUS.CANCELLED) {
|
||||
throw new Error("La suscripción ya se encuentraba cancelada");
|
||||
}
|
||||
|
||||
try {
|
||||
if (subscription.mpPreferenceId) {
|
||||
// Preferences don't need to be explicitly cancelled in MercadoPago
|
||||
// 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);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd)) {
|
||||
this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
} else {
|
||||
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
|
||||
subscription.mpInitPoint = "";
|
||||
subscription.mpPayerEmail = "";
|
||||
subscription.mpPayerId = -1;
|
||||
await subscription.save();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("ha ocurrido un error:", error);
|
||||
throw new Error("No fue posible crear la suscripción en el proveedor de pagos");
|
||||
}
|
||||
}
|
||||
|
||||
public async toFreePlan(data: ToFreePlanParams): Promise<void> {
|
||||
console.log("intentando aplicar el free plan...");
|
||||
|
||||
const sessionUser = await UsersManager.users.findOne({
|
||||
_id: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!sessionUser) {
|
||||
throw new Error(
|
||||
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = await this.planSuscriptions.findOne({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("No se ha encontrado una suscripción activa");
|
||||
}
|
||||
|
||||
const freePlan = await PlansList.plans.findOne({ price: 0 });
|
||||
if (!freePlan || !freePlan._id) {
|
||||
throw new Error("No se ha encontrado el plan gratuito");
|
||||
}
|
||||
|
||||
if (String(subscription.planId) == String(freePlan._id)) {
|
||||
//console.log("El usuario ya tiene el plan gratuito");
|
||||
return;
|
||||
}
|
||||
|
||||
const company = await CompaniesList.companies.findOne({
|
||||
ownerId: data.sessionUser,
|
||||
});
|
||||
|
||||
if (company) {
|
||||
//Intentamos eliminar el bot de el usuario.
|
||||
try {
|
||||
//console.log("intentando borrar el bot.");
|
||||
await WapServerList.deleteBot({
|
||||
companyId: String(company._id),
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("ha ocurrido un error al intentar eliminar el bot:", error);
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("guardando el plan gratuito...");
|
||||
//try {
|
||||
console.log("cambiando al free plan:", freePlan.id);
|
||||
subscription.planId = freePlan.id;
|
||||
subscription.mpStatus = "";
|
||||
subscription.isActive = true;
|
||||
subscription.mpInitPoint = "";
|
||||
subscription.mpPreferenceId = "";
|
||||
|
||||
await subscription.save();
|
||||
// console.log("plan gratuito guardado:", freePlan.id);
|
||||
//} catch (error) {
|
||||
// console.log("no se pudo guardar el plan gratuito.");
|
||||
//}
|
||||
|
||||
await NotificationsManager.sendSystemNotification({
|
||||
userId: String(data.sessionUser),
|
||||
subject: "Bienvenido al plan gratuito",
|
||||
message: `El plan gratuito ha sido asignado a su usuario. Cuando lo desee puede actualizar su plan para acceder más funciones.`,
|
||||
type: NotificationType.BILLING,
|
||||
code: String(subscription._id)
|
||||
});
|
||||
|
||||
const susInfo: ISubscriptionInfo = {
|
||||
id: subscription.id,
|
||||
plan: freePlan,
|
||||
startDate: subscription.startDate,
|
||||
endDate: subscription.endDate,
|
||||
isActive: subscription.isActive,
|
||||
autoRenew: subscription.autoRenew,
|
||||
mpStatus: subscription.mpStatus,
|
||||
mpDateCreated: subscription.mpDateCreated,
|
||||
};
|
||||
|
||||
io.to(`user:${String(subscription.userId)}`).emit("suscription_status_updated", susInfo);
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
data: FindPlanSuscripcionsParams
|
||||
): Promise<IPlanSuscriptionDocument | null> {
|
||||
const subscription = await this.planSuscriptions.findOne(data);
|
||||
|
||||
//console.log("findOne", subscription);
|
||||
|
||||
if (!subscription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date()).endOf("day");
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd)) {
|
||||
if (subscription.mpStatus === MP_SUBS_STATUS.CANCELLED) {
|
||||
this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await this.planSuscriptions.findOne(data);
|
||||
}
|
||||
|
||||
public async getSubscriptionByUser(
|
||||
data: FindPlanSuscripcionsParams
|
||||
): Promise<ISubscriptionInfo | null> {
|
||||
const subscription = await this.planSuscriptions.findOne(data);
|
||||
|
||||
if (!subscription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateNow = dayjs(new Date()).endOf("day");
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd)) {
|
||||
//Esta condicion era la culpable de que todo el tiempo se queria aplicar el free plan.
|
||||
//Ya que la fecha de finalizacion queda guardada en la base de datos a pesar de que ya no se use.
|
||||
if (
|
||||
subscription.mpStatus !== MP_SUBS_STATUS.AUTHORIZED &&
|
||||
subscription.mpStatus !== ""
|
||||
) {
|
||||
this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await PlansList.plans.findOne({
|
||||
_id: subscription.planId,
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
throw new Error("No se ha encontrado el plan");
|
||||
}
|
||||
|
||||
return {
|
||||
id: subscription.id,
|
||||
plan: plan,
|
||||
startDate: subscription.startDate,
|
||||
endDate: subscription.endDate,
|
||||
isActive: subscription.isActive,
|
||||
autoRenew: subscription.autoRenew,
|
||||
mpStatus: subscription.mpStatus,
|
||||
mpDateCreated: subscription.mpDateCreated,
|
||||
};
|
||||
}
|
||||
|
||||
public async checkFeature(data: CheckFeatureParams): Promise<boolean> {
|
||||
const subscriptionInfo = await this.getSubscriptionByUser({
|
||||
sessionUser: data.userId,
|
||||
});
|
||||
if (!subscriptionInfo) {
|
||||
throw new Error("No se encontro una suscripcion activa");
|
||||
}
|
||||
const plan = subscriptionInfo.plan;
|
||||
|
||||
if (!plan) {
|
||||
throw new Error("No se encontro el plan de la suscripcion");
|
||||
}
|
||||
|
||||
switch (data.feature) {
|
||||
case PlanFeatures.WAP_NOTIFICATIONS:
|
||||
if (plan.wapNotifications) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case PlanFeatures.MAIL_NOTIFICATIONS:
|
||||
if (plan.mailNotifications) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case PlanFeatures.SMS_NOTIFICATIONS:
|
||||
if (plan.smsNotifications) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case PlanFeatures.HAS_BOT:
|
||||
if (plan.bot) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case PlanFeatures.PAYMENTS:
|
||||
if (plan.payments) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async isSubscriptionActive(data: FindPlanSuscripcionsParams): Promise<boolean> {
|
||||
const subscriptionInfo = await this.getSubscriptionByUser(data);
|
||||
if (!subscriptionInfo) {
|
||||
throw new Error("No se encontro una suscripcion activa");
|
||||
}
|
||||
return subscriptionInfo.isActive;
|
||||
}
|
||||
}
|
||||
|
||||
const PlanSubscriptionsList = new PlanSuscriptionsManager();
|
||||
|
||||
export default PlanSubscriptionsList;
|
||||
@@ -0,0 +1,43 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
describe("PlanSubscriptions Logic", () => {
|
||||
it("should calculate correct endDate for 1, 3, 6, 12 months", () => {
|
||||
const start = dayjs("2024-01-01T10:00:00.000Z");
|
||||
|
||||
const end1 = start.clone().add(1, "months");
|
||||
expect(end1.format("YYYY-MM-DD")).toBe("2024-02-01");
|
||||
|
||||
const end3 = start.clone().add(3, "months");
|
||||
expect(end3.format("YYYY-MM-DD")).toBe("2024-04-01");
|
||||
|
||||
const end6 = start.clone().add(6, "months");
|
||||
expect(end6.format("YYYY-MM-DD")).toBe("2024-07-01");
|
||||
|
||||
const end12 = start.clone().add(12, "months");
|
||||
expect(end12.format("YYYY-MM-DD")).toBe("2025-01-01");
|
||||
});
|
||||
|
||||
it("should calculate total price correctly with discounts", () => {
|
||||
const planPrice = 1000;
|
||||
|
||||
// 1 month, 0 discount
|
||||
const total1 = planPrice * 1;
|
||||
const final1 = total1 - (total1 * (0 / 100));
|
||||
expect(final1).toBe(1000);
|
||||
|
||||
// 3 months, 10% discount
|
||||
const total3 = planPrice * 3; // 3000
|
||||
const final3 = total3 - (total3 * (10 / 100)); // 3000 - 300 = 2700
|
||||
expect(final3).toBe(2700);
|
||||
|
||||
// 6 months, 20% discount
|
||||
const total6 = planPrice * 6; // 6000
|
||||
const final6 = total6 - (total6 * (20 / 100)); // 6000 - 1200 = 4800
|
||||
expect(final6).toBe(4800);
|
||||
|
||||
// 12 months, 30% discount
|
||||
const total12 = planPrice * 12; // 12000
|
||||
const final12 = total12 - (total12 * (30 / 100)); // 12000 - 3600 = 8400
|
||||
expect(final12).toBe(8400);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user