feat: implement plan upgrade logic with prorated pricing and MercadoPago integration
This commit is contained in:
@@ -31,10 +31,13 @@ import { NotificationsManager } from "../Notifications/Notifications";
|
||||
import WapServerList from "../WapServer/WapServer";
|
||||
import CompaniesList from "../Companies/Companies";
|
||||
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
|
||||
import { calculateProratedUpgradeAmount } from "./PlanSubscriptions.pricing";
|
||||
|
||||
dayjs.locale("es");
|
||||
dayjs.extend(isSameOrAfter);
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
planSuscriptions: PlanSuscriptionsAdapterMongoose;
|
||||
|
||||
@@ -46,6 +49,20 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
return subscription.mpStatus === MP_SUBS_STATUS.AUTHORIZED || subscription.mpStatus === MP_SUBS_STATUS.CANCELLED;
|
||||
}
|
||||
|
||||
private buildMercadoPagoBackUrl(returnStatus: "success" | "failure" | "pending"): string {
|
||||
const backUrl = process.env.MP_BACK_URL || "";
|
||||
if (!backUrl) return backUrl;
|
||||
|
||||
try {
|
||||
const url = new URL(backUrl);
|
||||
url.searchParams.set("mp_return", returnStatus);
|
||||
return url.toString();
|
||||
} catch {
|
||||
const separator = backUrl.includes("?") ? "&" : "?";
|
||||
return `${backUrl}${separator}mp_return=${returnStatus}`;
|
||||
}
|
||||
}
|
||||
|
||||
public async getInitPoint(
|
||||
data: GetSubscriptionInitPointParams
|
||||
): Promise<GetSuscriptionInitPointResponse> {
|
||||
@@ -190,6 +207,23 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
months: data.months || 1,
|
||||
});
|
||||
} else {
|
||||
const isActivePaidSubscription =
|
||||
actualPlan.price > 0 &&
|
||||
(subscription.mpStatus === MP_SUBS_STATUS.AUTHORIZED ||
|
||||
subscription.mpStatus === MP_SUBS_STATUS.CANCELLED) &&
|
||||
dayjs(subscription.endDate).isAfter(new Date());
|
||||
|
||||
if (isActivePaidSubscription && actualPlan.id !== data.planId) {
|
||||
if (plan.price <= actualPlan.price) {
|
||||
throw new Error("No se puede cambiar a un plan igual o inferior desde esta ruta de actualización.");
|
||||
}
|
||||
|
||||
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."
|
||||
@@ -201,6 +235,7 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
sessionUser: data.sessionUser,
|
||||
months: data.months || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +284,42 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
const currentPlan = currentSubscription
|
||||
? await PlansList.plans.findOne({ _id: String(currentSubscription.planId) })
|
||||
: null;
|
||||
|
||||
const isUpgradePayment =
|
||||
currentSubscription &&
|
||||
currentPlan &&
|
||||
String(currentSubscription.planId) !== String(data.planId) &&
|
||||
currentPlan.price > 0 &&
|
||||
plan.price > currentPlan.price &&
|
||||
(currentSubscription.mpStatus === MP_SUBS_STATUS.AUTHORIZED ||
|
||||
currentSubscription.mpStatus === MP_SUBS_STATUS.CANCELLED) &&
|
||||
dayjs(currentSubscription.endDate).isAfter(preferenceDate);
|
||||
|
||||
const remainingDays = isUpgradePayment
|
||||
? Math.max(0, Math.ceil((new Date(currentSubscription.endDate).getTime() - preferenceDate.toDate().getTime()) / DAY_MS))
|
||||
: 0;
|
||||
const proratedAmount = isUpgradePayment
|
||||
? calculateProratedUpgradeAmount(currentPlan.price, plan.price, remainingDays)
|
||||
: 0;
|
||||
|
||||
if (isUpgradePayment && remainingDays <= 0) {
|
||||
throw new Error("No se puede actualizar el plan porque la suscripción actual no tiene días restantes.");
|
||||
}
|
||||
|
||||
if (
|
||||
currentSubscription?.pendingPaymentPreferenceId &&
|
||||
currentSubscription.pendingPaymentInitPoint
|
||||
) {
|
||||
if (String(currentSubscription.pendingPaymentPlanId || currentSubscription.planId) === String(data.planId)) {
|
||||
return currentSubscription;
|
||||
}
|
||||
|
||||
throw new Error("Ya tenés un pago pendiente para tu plan. Verificalo o esperá a que se actualice antes de intentar otro cambio.");
|
||||
}
|
||||
|
||||
if (
|
||||
currentSubscription &&
|
||||
String(currentSubscription.planId) === String(data.planId) &&
|
||||
@@ -265,16 +336,18 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
{
|
||||
items: [
|
||||
{
|
||||
title: "TurnosXpress.com.ar - Plan " + plan.name + " (" + data.months + " meses)",
|
||||
title: isUpgradePayment
|
||||
? "TurnosXpress.com.ar - Upgrade al plan " + plan.name
|
||||
: "TurnosXpress.com.ar - Plan " + plan.name + " (" + data.months + " meses)",
|
||||
quantity: 1,
|
||||
currency_id: "ARS",
|
||||
unit_price: finalPrice
|
||||
unit_price: isUpgradePayment ? proratedAmount : finalPrice
|
||||
}
|
||||
],
|
||||
back_urls: {
|
||||
success: process.env.MP_BACK_URL,
|
||||
failure: process.env.MP_BACK_URL,
|
||||
pending: process.env.MP_BACK_URL
|
||||
success: this.buildMercadoPagoBackUrl("success"),
|
||||
failure: this.buildMercadoPagoBackUrl("failure"),
|
||||
pending: this.buildMercadoPagoBackUrl("pending")
|
||||
},
|
||||
auto_return: "approved",
|
||||
payer: {
|
||||
@@ -293,6 +366,35 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
|
||||
const subscriptionData = response.data;
|
||||
|
||||
if (isUpgradePayment && currentSubscription && currentPlan) {
|
||||
currentSubscription.pendingPaymentInitPoint = subscriptionData.init_point;
|
||||
currentSubscription.pendingPaymentPreferenceId = subscriptionData.id;
|
||||
currentSubscription.pendingPaymentBillingMonths = undefined;
|
||||
currentSubscription.pendingPaymentType = "upgrade";
|
||||
currentSubscription.pendingPaymentPlanId = data.planId;
|
||||
currentSubscription.pendingPaymentCurrentPlanPrice = currentPlan.price;
|
||||
currentSubscription.pendingPaymentRequestedPlanPrice = plan.price;
|
||||
currentSubscription.pendingPaymentProratedAmount = proratedAmount;
|
||||
currentSubscription.pendingPaymentRemainingDays = remainingDays;
|
||||
currentSubscription.pendingPaymentPeriodEndDate = currentSubscription.endDate;
|
||||
currentSubscription.lastPaymentStatus = undefined;
|
||||
currentSubscription.lastPaymentPreferenceId = undefined;
|
||||
currentSubscription.lastPaymentAt = undefined;
|
||||
currentSubscription.lastPaymentType = undefined;
|
||||
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
||||
await currentSubscription.save();
|
||||
|
||||
await NotificationsManager.sendSystemNotification({
|
||||
userId: String(data.sessionUser),
|
||||
subject: "Se ha creado una actualización de suscripción",
|
||||
message: `Se ha creado una actualización al plan ${plan.name}.`,
|
||||
type: NotificationType.BILLING,
|
||||
code: String(currentSubscription._id)
|
||||
});
|
||||
|
||||
return currentSubscription;
|
||||
}
|
||||
|
||||
if (
|
||||
currentSubscription &&
|
||||
String(currentSubscription.planId) === String(data.planId) &&
|
||||
@@ -302,6 +404,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
currentSubscription.pendingPaymentInitPoint = subscriptionData.init_point;
|
||||
currentSubscription.pendingPaymentPreferenceId = subscriptionData.id;
|
||||
currentSubscription.pendingPaymentBillingMonths = data.months;
|
||||
currentSubscription.pendingPaymentType = "extension";
|
||||
currentSubscription.lastPaymentStatus = undefined;
|
||||
currentSubscription.lastPaymentPreferenceId = undefined;
|
||||
currentSubscription.lastPaymentAt = undefined;
|
||||
currentSubscription.lastPaymentType = undefined;
|
||||
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
||||
await currentSubscription.save();
|
||||
|
||||
@@ -335,6 +442,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
pendingPaymentInitPoint: "",
|
||||
pendingPaymentPreferenceId: "",
|
||||
pendingPaymentBillingMonths: undefined,
|
||||
pendingPaymentType: undefined,
|
||||
lastPaymentStatus: undefined,
|
||||
lastPaymentPreferenceId: undefined,
|
||||
lastPaymentAt: undefined,
|
||||
lastPaymentType: undefined,
|
||||
});
|
||||
|
||||
await NotificationsManager.sendSystemNotification({
|
||||
@@ -385,7 +497,7 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
const dateEnd = dayjs(subscription.endDate);
|
||||
|
||||
if (dateNow.isSameOrAfter(dateEnd)) {
|
||||
this.toFreePlan({
|
||||
await this.toFreePlan({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
} else {
|
||||
@@ -393,6 +505,16 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
subscription.mpInitPoint = "";
|
||||
subscription.mpPayerEmail = "";
|
||||
subscription.mpPayerId = -1;
|
||||
subscription.pendingPaymentInitPoint = "";
|
||||
subscription.pendingPaymentPreferenceId = "";
|
||||
subscription.pendingPaymentBillingMonths = undefined;
|
||||
subscription.pendingPaymentType = undefined;
|
||||
subscription.pendingPaymentPlanId = undefined;
|
||||
subscription.pendingPaymentCurrentPlanPrice = undefined;
|
||||
subscription.pendingPaymentRequestedPlanPrice = undefined;
|
||||
subscription.pendingPaymentProratedAmount = undefined;
|
||||
subscription.pendingPaymentRemainingDays = undefined;
|
||||
subscription.pendingPaymentPeriodEndDate = undefined;
|
||||
await subscription.save();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -462,6 +584,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
subscription.pendingPaymentInitPoint = "";
|
||||
subscription.pendingPaymentPreferenceId = "";
|
||||
subscription.pendingPaymentBillingMonths = undefined;
|
||||
subscription.pendingPaymentType = undefined;
|
||||
subscription.pendingPaymentPlanId = undefined;
|
||||
subscription.pendingPaymentCurrentPlanPrice = undefined;
|
||||
subscription.pendingPaymentRequestedPlanPrice = undefined;
|
||||
subscription.pendingPaymentProratedAmount = undefined;
|
||||
subscription.pendingPaymentRemainingDays = undefined;
|
||||
subscription.pendingPaymentPeriodEndDate = undefined;
|
||||
subscription.startDate = new Date();
|
||||
subscription.endDate = dayjs(new Date()).add(10, "years").toDate();
|
||||
|
||||
@@ -488,6 +617,10 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
autoRenew: subscription.autoRenew,
|
||||
mpStatus: subscription.mpStatus,
|
||||
mpDateCreated: subscription.mpDateCreated,
|
||||
lastPaymentStatus: subscription.lastPaymentStatus,
|
||||
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
||||
lastPaymentAt: subscription.lastPaymentAt,
|
||||
lastPaymentType: subscription.lastPaymentType,
|
||||
};
|
||||
|
||||
io.to(`user:${String(subscription.userId)}`).emit("suscription_status_updated", susInfo);
|
||||
@@ -539,7 +672,9 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
_id: subscription.planId,
|
||||
});
|
||||
|
||||
if (!subscription.isActive || subscription.mpStatus === MP_SUBS_STATUS.PENDING) {
|
||||
const isPendingPaidSubscription = subscription.mpStatus === MP_SUBS_STATUS.PENDING && plan.price > 0;
|
||||
|
||||
if (!isPendingPaidSubscription && (!subscription.isActive || subscription.mpStatus === MP_SUBS_STATUS.PENDING)) {
|
||||
const freePlan = await PlansList.findOne({ price: 0 });
|
||||
if (freePlan) {
|
||||
plan = freePlan;
|
||||
@@ -559,6 +694,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
||||
autoRenew: subscription.autoRenew,
|
||||
mpStatus: subscription.mpStatus,
|
||||
mpDateCreated: subscription.mpDateCreated,
|
||||
pendingPaymentInitPoint: subscription.pendingPaymentInitPoint,
|
||||
pendingPaymentPreferenceId: subscription.pendingPaymentPreferenceId,
|
||||
pendingPaymentType: subscription.pendingPaymentType,
|
||||
lastPaymentStatus: subscription.lastPaymentStatus,
|
||||
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
||||
lastPaymentAt: subscription.lastPaymentAt,
|
||||
lastPaymentType: subscription.lastPaymentType,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user