first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
@@ -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;