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 {
|
import {
|
||||||
FindPlanSuscripcionsParams,
|
FindPlanSuscripcionsParams,
|
||||||
IPlanSuscriptionsAdapter,
|
IPlanSuscriptionsAdapter,
|
||||||
@@ -53,6 +52,11 @@ export class PlanSuscriptionsAdapterMongoose implements IPlanSuscriptionsAdapter
|
|||||||
lastPaymentPreferenceId: { type: String, required: false },
|
lastPaymentPreferenceId: { type: String, required: false },
|
||||||
lastPaymentAt: { type: Date, required: false },
|
lastPaymentAt: { type: Date, required: false },
|
||||||
lastPaymentType: { type: String, required: false, enum: ["new", "extension", "upgrade"] },
|
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);
|
this.planSuscriptionList = model<IPlanSuscriptionDocument>("PlanSuscription", this.schema);
|
||||||
@@ -89,12 +93,12 @@ export class PlanSuscriptionsAdapterMongoose implements IPlanSuscriptionsAdapter
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateNow = dayjs(new Date());
|
const dateNow = new Date();
|
||||||
const dateEnd = dayjs(subscription.endDate);
|
const dateEnd = new Date(subscription.endDate);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (subscription.mpPreferenceId) {
|
if (subscription.mpPreferenceId) {
|
||||||
if (dateNow.isAfter(dateEnd)) {
|
if (dateNow > dateEnd) {
|
||||||
if (subscription.mpStatus == MP_SUBS_STATUS.CANCELLED) {
|
if (subscription.mpStatus == MP_SUBS_STATUS.CANCELLED) {
|
||||||
subscription.mpPreferenceId = "";
|
subscription.mpPreferenceId = "";
|
||||||
subscription.isActive = false;
|
subscription.isActive = false;
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ export type CreatePlanSuscriptionParams = {
|
|||||||
lastPaymentPreferenceId?: string;
|
lastPaymentPreferenceId?: string;
|
||||||
lastPaymentAt?: Date;
|
lastPaymentAt?: Date;
|
||||||
lastPaymentType?: LastPlanPaymentType;
|
lastPaymentType?: LastPlanPaymentType;
|
||||||
|
downgradedFromPlanId?: string;
|
||||||
|
downgradedFromPlanName?: string;
|
||||||
|
downgradedFromPlanCode?: string;
|
||||||
|
downgradedAt?: Date;
|
||||||
|
downgradeReason?: DowngradeReason;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CancellPlanSuscriptionParams = {
|
export type CancellPlanSuscriptionParams = {
|
||||||
@@ -65,6 +70,7 @@ export type MPPreApprovalResponse = {
|
|||||||
|
|
||||||
export type ToFreePlanParams = {
|
export type ToFreePlanParams = {
|
||||||
sessionUser: string;
|
sessionUser: string;
|
||||||
|
downgradeReason?: DowngradeReason;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToMpParams = {
|
export type ToMpParams = {
|
||||||
@@ -101,6 +107,7 @@ export type VerifyPendingPlanPaymentResponse = {
|
|||||||
|
|
||||||
export type LastPlanPaymentStatus = "rejected" | "failed" | "cancelled";
|
export type LastPlanPaymentStatus = "rejected" | "failed" | "cancelled";
|
||||||
export type LastPlanPaymentType = "new" | "extension" | "upgrade";
|
export type LastPlanPaymentType = "new" | "extension" | "upgrade";
|
||||||
|
export type DowngradeReason = "expired";
|
||||||
|
|
||||||
export interface GetSuscriptionInitPointResponse {
|
export interface GetSuscriptionInitPointResponse {
|
||||||
init_point: string;
|
init_point: string;
|
||||||
@@ -137,6 +144,11 @@ export interface IPlanSuscription {
|
|||||||
lastPaymentPreferenceId?: string;
|
lastPaymentPreferenceId?: string;
|
||||||
lastPaymentAt?: Date;
|
lastPaymentAt?: Date;
|
||||||
lastPaymentType?: LastPlanPaymentType;
|
lastPaymentType?: LastPlanPaymentType;
|
||||||
|
downgradedFromPlanId?: string;
|
||||||
|
downgradedFromPlanName?: string;
|
||||||
|
downgradedFromPlanCode?: string;
|
||||||
|
downgradedAt?: Date;
|
||||||
|
downgradeReason?: DowngradeReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISubscriptionInfo {
|
export interface ISubscriptionInfo {
|
||||||
@@ -155,6 +167,11 @@ export interface ISubscriptionInfo {
|
|||||||
lastPaymentPreferenceId?: string;
|
lastPaymentPreferenceId?: string;
|
||||||
lastPaymentAt?: Date;
|
lastPaymentAt?: Date;
|
||||||
lastPaymentType?: LastPlanPaymentType;
|
lastPaymentType?: LastPlanPaymentType;
|
||||||
|
downgradedFromPlanId?: string;
|
||||||
|
downgradedFromPlanName?: string;
|
||||||
|
downgradedFromPlanCode?: string;
|
||||||
|
downgradedAt?: Date;
|
||||||
|
downgradeReason?: DowngradeReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlanSuscriptionsAdapter {
|
export interface IPlanSuscriptionsAdapter {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import PlansList from "../../Models/Plans/Plans";
|
import PlansList from "../../Models/Plans/Plans";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
|
||||||
import "dayjs/locale/es";
|
import "dayjs/locale/es";
|
||||||
import { isNull } from "../../helpers/IsNull";
|
import { isNull } from "../../helpers/IsNull";
|
||||||
import { PlanFeatures } from "../Plans/Plans.interface";
|
import { PlanFeatures } from "../Plans/Plans.interface";
|
||||||
@@ -34,7 +33,6 @@ import { NotificationType } from "../../Models/SystemNotifications/SystemNotific
|
|||||||
import { calculateProratedUpgradeAmount } from "./PlanSubscriptions.pricing";
|
import { calculateProratedUpgradeAmount } from "./PlanSubscriptions.pricing";
|
||||||
|
|
||||||
dayjs.locale("es");
|
dayjs.locale("es");
|
||||||
dayjs.extend(isSameOrAfter);
|
|
||||||
|
|
||||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@@ -195,6 +193,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
subscription.autoRenew = false;
|
subscription.autoRenew = false;
|
||||||
subscription.mpStatus = "";
|
subscription.mpStatus = "";
|
||||||
subscription.mpInitPoint = dashboardUrl || "";
|
subscription.mpInitPoint = dashboardUrl || "";
|
||||||
|
subscription.downgradedFromPlanId = undefined;
|
||||||
|
subscription.downgradedFromPlanName = undefined;
|
||||||
|
subscription.downgradedFromPlanCode = undefined;
|
||||||
|
subscription.downgradedAt = undefined;
|
||||||
|
subscription.downgradeReason = undefined;
|
||||||
await subscription.save();
|
await subscription.save();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -381,6 +384,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
currentSubscription.lastPaymentPreferenceId = undefined;
|
currentSubscription.lastPaymentPreferenceId = undefined;
|
||||||
currentSubscription.lastPaymentAt = undefined;
|
currentSubscription.lastPaymentAt = undefined;
|
||||||
currentSubscription.lastPaymentType = undefined;
|
currentSubscription.lastPaymentType = undefined;
|
||||||
|
currentSubscription.downgradedFromPlanId = undefined;
|
||||||
|
currentSubscription.downgradedFromPlanName = undefined;
|
||||||
|
currentSubscription.downgradedFromPlanCode = undefined;
|
||||||
|
currentSubscription.downgradedAt = undefined;
|
||||||
|
currentSubscription.downgradeReason = undefined;
|
||||||
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
||||||
await currentSubscription.save();
|
await currentSubscription.save();
|
||||||
|
|
||||||
@@ -409,6 +417,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
currentSubscription.lastPaymentPreferenceId = undefined;
|
currentSubscription.lastPaymentPreferenceId = undefined;
|
||||||
currentSubscription.lastPaymentAt = undefined;
|
currentSubscription.lastPaymentAt = undefined;
|
||||||
currentSubscription.lastPaymentType = undefined;
|
currentSubscription.lastPaymentType = undefined;
|
||||||
|
currentSubscription.downgradedFromPlanId = undefined;
|
||||||
|
currentSubscription.downgradedFromPlanName = undefined;
|
||||||
|
currentSubscription.downgradedFromPlanCode = undefined;
|
||||||
|
currentSubscription.downgradedAt = undefined;
|
||||||
|
currentSubscription.downgradeReason = undefined;
|
||||||
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
currentSubscription.mpInitPoint = subscriptionData.init_point;
|
||||||
await currentSubscription.save();
|
await currentSubscription.save();
|
||||||
|
|
||||||
@@ -447,6 +460,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
lastPaymentPreferenceId: undefined,
|
lastPaymentPreferenceId: undefined,
|
||||||
lastPaymentAt: undefined,
|
lastPaymentAt: undefined,
|
||||||
lastPaymentType: undefined,
|
lastPaymentType: undefined,
|
||||||
|
downgradedFromPlanId: undefined,
|
||||||
|
downgradedFromPlanName: undefined,
|
||||||
|
downgradedFromPlanCode: undefined,
|
||||||
|
downgradedAt: undefined,
|
||||||
|
downgradeReason: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await NotificationsManager.sendSystemNotification({
|
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.
|
// We just cancel it locally if not paid, or change to free plan if paid.
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateNow = dayjs(new Date()).endOf("day");
|
const dateNow = new Date();
|
||||||
const dateEnd = dayjs(subscription.endDate);
|
const dateEnd = new Date(subscription.endDate);
|
||||||
|
|
||||||
if (dateNow.isSameOrAfter(dateEnd)) {
|
if (dateNow > dateEnd) {
|
||||||
await this.toFreePlan({
|
await this.toFreePlan({
|
||||||
sessionUser: data.sessionUser,
|
sessionUser: data.sessionUser,
|
||||||
|
downgradeReason: "expired",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
|
subscription.mpStatus = MP_SUBS_STATUS.CANCELLED;
|
||||||
@@ -573,6 +592,10 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
|
|
||||||
//console.log("guardando el plan gratuito...");
|
//console.log("guardando el plan gratuito...");
|
||||||
//try {
|
//try {
|
||||||
|
const downgradedFromPlan = data.downgradeReason === "expired"
|
||||||
|
? await PlansList.plans.findOne({ _id: String(subscription.planId) })
|
||||||
|
: null;
|
||||||
|
|
||||||
console.log("cambiando al free plan:", freePlan.id);
|
console.log("cambiando al free plan:", freePlan.id);
|
||||||
subscription.planId = freePlan.id;
|
subscription.planId = freePlan.id;
|
||||||
subscription.mpStatus = "";
|
subscription.mpStatus = "";
|
||||||
@@ -593,6 +616,19 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
subscription.pendingPaymentPeriodEndDate = undefined;
|
subscription.pendingPaymentPeriodEndDate = undefined;
|
||||||
subscription.startDate = new Date();
|
subscription.startDate = new Date();
|
||||||
subscription.endDate = dayjs(new Date()).add(10, "years").toDate();
|
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();
|
await subscription.save();
|
||||||
// console.log("plan gratuito guardado:", freePlan.id);
|
// console.log("plan gratuito guardado:", freePlan.id);
|
||||||
@@ -621,6 +657,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
||||||
lastPaymentAt: subscription.lastPaymentAt,
|
lastPaymentAt: subscription.lastPaymentAt,
|
||||||
lastPaymentType: subscription.lastPaymentType,
|
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);
|
io.to(`user:${String(subscription.userId)}`).emit("suscription_status_updated", susInfo);
|
||||||
@@ -637,12 +678,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateNow = dayjs(new Date()).endOf("day");
|
const dateNow = new Date();
|
||||||
const dateEnd = dayjs(subscription.endDate);
|
const dateEnd = new Date(subscription.endDate);
|
||||||
|
|
||||||
if (dateNow.isSameOrAfter(dateEnd) && this.shouldDowngradeExpiredSubscription(subscription)) {
|
if (dateNow > dateEnd && this.shouldDowngradeExpiredSubscription(subscription)) {
|
||||||
await this.toFreePlan({
|
await this.toFreePlan({
|
||||||
sessionUser: data.sessionUser,
|
sessionUser: data.sessionUser,
|
||||||
|
downgradeReason: "expired",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,12 +700,13 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateNow = dayjs(new Date()).endOf("day");
|
const dateNow = new Date();
|
||||||
const dateEnd = dayjs(subscription.endDate);
|
const dateEnd = new Date(subscription.endDate);
|
||||||
|
|
||||||
if (dateNow.isSameOrAfter(dateEnd) && this.shouldDowngradeExpiredSubscription(subscription)) {
|
if (dateNow > dateEnd && this.shouldDowngradeExpiredSubscription(subscription)) {
|
||||||
await this.toFreePlan({
|
await this.toFreePlan({
|
||||||
sessionUser: data.sessionUser,
|
sessionUser: data.sessionUser,
|
||||||
|
downgradeReason: "expired",
|
||||||
});
|
});
|
||||||
return this.getSubscriptionByUser(data);
|
return this.getSubscriptionByUser(data);
|
||||||
}
|
}
|
||||||
@@ -701,6 +744,11 @@ class PlanSuscriptionsManager implements IPlanSuscriptionsManager {
|
|||||||
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
lastPaymentPreferenceId: subscription.lastPaymentPreferenceId,
|
||||||
lastPaymentAt: subscription.lastPaymentAt,
|
lastPaymentAt: subscription.lastPaymentAt,
|
||||||
lastPaymentType: subscription.lastPaymentType,
|
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;
|
transactionId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SysAdminFinalizeUserSubscriptionParams = {
|
||||||
|
userId: string;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SysAdminFinalizeUserSubscriptionResult = {
|
||||||
|
userId: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
previousEndDate: Date;
|
||||||
|
newEndDate: Date;
|
||||||
|
};
|
||||||
|
|
||||||
export type SysAdminUserSubscriptionPayment = {
|
export type SysAdminUserSubscriptionPayment = {
|
||||||
id: string;
|
id: string;
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
|
|||||||
@@ -328,6 +328,11 @@ export class MercadoPagoWebhookService {
|
|||||||
subscription.lastPaymentPreferenceId = undefined;
|
subscription.lastPaymentPreferenceId = undefined;
|
||||||
subscription.lastPaymentAt = undefined;
|
subscription.lastPaymentAt = undefined;
|
||||||
subscription.lastPaymentType = undefined;
|
subscription.lastPaymentType = undefined;
|
||||||
|
subscription.downgradedFromPlanId = undefined;
|
||||||
|
subscription.downgradedFromPlanName = undefined;
|
||||||
|
subscription.downgradedFromPlanCode = undefined;
|
||||||
|
subscription.downgradedAt = undefined;
|
||||||
|
subscription.downgradeReason = undefined;
|
||||||
await subscription.save();
|
await subscription.save();
|
||||||
|
|
||||||
await PlanPaymentsList.createIfMissingByTransactionId({
|
await PlanPaymentsList.createIfMissingByTransactionId({
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { sysAdminAuthMiddleware } from "../../../middleware/sysadminAuth";
|
|||||||
import {
|
import {
|
||||||
SysAdminExtendUserSubscriptionParams,
|
SysAdminExtendUserSubscriptionParams,
|
||||||
SysAdminExtendUserSubscriptionResult,
|
SysAdminExtendUserSubscriptionResult,
|
||||||
|
SysAdminFinalizeUserSubscriptionParams,
|
||||||
|
SysAdminFinalizeUserSubscriptionResult,
|
||||||
SysAdminUserSubscriptionDetailsParams,
|
SysAdminUserSubscriptionDetailsParams,
|
||||||
SysAdminUserSubscriptionDetailsResult,
|
SysAdminUserSubscriptionDetailsResult,
|
||||||
} from "../../../Models/Users/Users.Interface";
|
} 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")
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
@SuccessResponse(200, "Done")
|
@SuccessResponse(200, "Done")
|
||||||
@Post("user-details")
|
@Post("user-details")
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import PlansList from "../../../Models/Plans/Plans";
|
|||||||
import {
|
import {
|
||||||
SysAdminExtendUserSubscriptionParams,
|
SysAdminExtendUserSubscriptionParams,
|
||||||
SysAdminExtendUserSubscriptionResult,
|
SysAdminExtendUserSubscriptionResult,
|
||||||
|
SysAdminFinalizeUserSubscriptionParams,
|
||||||
|
SysAdminFinalizeUserSubscriptionResult,
|
||||||
SysAdminUserSubscriptionDetailsParams,
|
SysAdminUserSubscriptionDetailsParams,
|
||||||
SysAdminUserSubscriptionDetailsResult,
|
SysAdminUserSubscriptionDetailsResult,
|
||||||
} from "../../../Models/Users/Users.Interface";
|
} 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(
|
public async getUserSubscriptionDetails(
|
||||||
params: SysAdminUserSubscriptionDetailsParams
|
params: SysAdminUserSubscriptionDetailsParams
|
||||||
): Promise<SysAdminUserSubscriptionDetailsResult> {
|
): Promise<SysAdminUserSubscriptionDetailsResult> {
|
||||||
|
|||||||
@@ -109,6 +109,23 @@ export const extendUserSubscription = async (data: ExtendUserSubscriptionPayload
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FinalizeUserSubscriptionPayload = {
|
||||||
|
userId: string;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FinalizeUserSubscriptionResult = {
|
||||||
|
userId: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
previousEndDate: string;
|
||||||
|
newEndDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const finalizeUserSubscription = async (data: FinalizeUserSubscriptionPayload): Promise<FinalizeUserSubscriptionResult> => {
|
||||||
|
const res = await sysadminApi.post('/users/finalize-subscription', data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
export type Plan = {
|
export type Plan = {
|
||||||
id?: string;
|
id?: string;
|
||||||
_id?: string;
|
_id?: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { CreditCard, X } from 'lucide-react';
|
import { CreditCard, X } from 'lucide-react';
|
||||||
import { extendUserSubscription, getUserSubscriptionDetails, type UserSubscriptionDetails } from '../../api/sysadmin';
|
import { extendUserSubscription, finalizeUserSubscription, getUserSubscriptionDetails, type UserSubscriptionDetails } from '../../api/sysadmin';
|
||||||
import { formatDateTime } from '../../utils/formatters';
|
import { formatDateTime } from '../../utils/formatters';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -17,6 +17,7 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
|||||||
const [extensionLoading, setExtensionLoading] = useState(false);
|
const [extensionLoading, setExtensionLoading] = useState(false);
|
||||||
const [extensionMessage, setExtensionMessage] = useState('');
|
const [extensionMessage, setExtensionMessage] = useState('');
|
||||||
const [extensionError, setExtensionError] = useState('');
|
const [extensionError, setExtensionError] = useState('');
|
||||||
|
const [finalizeLoading, setFinalizeLoading] = useState(false);
|
||||||
|
|
||||||
const formatAmount = (value?: number) => {
|
const formatAmount = (value?: number) => {
|
||||||
if (typeof value !== 'number') return '-';
|
if (typeof value !== 'number') return '-';
|
||||||
@@ -61,6 +62,29 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFinalize = async () => {
|
||||||
|
if (!details?.currentSubscription) return;
|
||||||
|
|
||||||
|
const confirmed = window.confirm('Esto establecerá el vencimiento de la suscripción a ayer para forzar el comportamiento de plan vencido. No elimina datos ni cambia el plan directamente. ¿Continuar?');
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
setFinalizeLoading(true);
|
||||||
|
setExtensionError('');
|
||||||
|
setExtensionMessage('');
|
||||||
|
try {
|
||||||
|
const result = await finalizeUserSubscription({
|
||||||
|
userId: user._id,
|
||||||
|
reason: 'Finalización manual desde sysadmin-cli para probar expiración de plan',
|
||||||
|
});
|
||||||
|
setExtensionMessage(`Período finalizado. El vencimiento quedó en ${formatDateTime(result.newEndDate)} para forzar la expiración en la próxima lectura.`);
|
||||||
|
await loadDetails();
|
||||||
|
} catch (e: any) {
|
||||||
|
setExtensionError(e.response?.data?.message || e.message || 'Error finalizando período');
|
||||||
|
} finally {
|
||||||
|
setFinalizeLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const subscription = details?.currentSubscription;
|
const subscription = details?.currentSubscription;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -118,10 +142,10 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
|||||||
Extiende la vigencia sin pago y registra un pago de bonificación por $0 asociado a esta suscripción.
|
Extiende la vigencia sin pago y registra un pago de bonificación por $0 asociado a esta suscripción.
|
||||||
</p>
|
</p>
|
||||||
<div className="extension-actions">
|
<div className="extension-actions">
|
||||||
<button className="btn btn-secondary" disabled={extensionLoading} onClick={() => handleExtend({ mode: 'add_months', months: 1 })}>
|
<button className="btn btn-secondary" disabled={extensionLoading || finalizeLoading} onClick={() => handleExtend({ mode: 'add_months', months: 1 })}>
|
||||||
Extender 1 mes
|
Extender 1 mes
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-secondary" disabled={extensionLoading} onClick={() => handleExtend({ mode: 'add_months', months: 2 })}>
|
<button className="btn btn-secondary" disabled={extensionLoading || finalizeLoading} onClick={() => handleExtend({ mode: 'add_months', months: 2 })}>
|
||||||
Extender 2 meses
|
Extender 2 meses
|
||||||
</button>
|
</button>
|
||||||
<div className="extension-date-action">
|
<div className="extension-date-action">
|
||||||
@@ -129,18 +153,27 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
|||||||
type="date"
|
type="date"
|
||||||
value={extensionDate}
|
value={extensionDate}
|
||||||
onChange={(event) => setExtensionDate(event.target.value)}
|
onChange={(event) => setExtensionDate(event.target.value)}
|
||||||
disabled={extensionLoading}
|
disabled={extensionLoading || finalizeLoading}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
disabled={extensionLoading || !extensionDate}
|
disabled={extensionLoading || finalizeLoading || !extensionDate}
|
||||||
onClick={() => handleExtend({ mode: 'set_end_date', endDate: extensionDate })}
|
onClick={() => handleExtend({ mode: 'set_end_date', endDate: extensionDate })}
|
||||||
>
|
>
|
||||||
Establecer vencimiento
|
Establecer vencimiento
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{extensionLoading && <p style={{ color: 'var(--text-muted)', marginTop: '0.75rem' }}>Aplicando ajuste...</p>}
|
<div className="finalize-action">
|
||||||
|
<div>
|
||||||
|
<h4>Finalizar período</h4>
|
||||||
|
<p>Establece el vencimiento en ayer para forzar el comportamiento de suscripción vencida en la próxima lectura. No elimina datos ni cambia el plan directamente.</p>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-danger" disabled={extensionLoading || finalizeLoading} onClick={handleFinalize}>
|
||||||
|
Finalizar período
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(extensionLoading || finalizeLoading) && <p style={{ color: 'var(--text-muted)', marginTop: '0.75rem' }}>Aplicando ajuste...</p>}
|
||||||
{extensionMessage && <p style={{ color: 'var(--success)', marginTop: '0.75rem' }}>{extensionMessage}</p>}
|
{extensionMessage && <p style={{ color: 'var(--success)', marginTop: '0.75rem' }}>{extensionMessage}</p>}
|
||||||
{extensionError && <p style={{ color: 'var(--danger)', marginTop: '0.75rem' }}>{extensionError}</p>}
|
{extensionError && <p style={{ color: 'var(--danger)', marginTop: '0.75rem' }}>{extensionError}</p>}
|
||||||
</section>
|
</section>
|
||||||
@@ -226,6 +259,30 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
padding: 0.65rem 0.75rem;
|
padding: 0.65rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
.finalize-action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
.finalize-action h4 {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
.finalize-action p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.finalize-action {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
`}} />
|
`}} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SysAdminOrganizationsStatusParams,
|
SysAdminOrganizationsStatusParams,
|
||||||
SysAdminRecalculatePlanUsageCycleParams,
|
SysAdminRecalculatePlanUsageCycleParams,
|
||||||
SysAdminExtendUserSubscriptionParams,
|
SysAdminExtendUserSubscriptionParams,
|
||||||
|
SysAdminFinalizeUserSubscriptionParams,
|
||||||
SysAdminUserSubscriptionDetailsParams,
|
SysAdminUserSubscriptionDetailsParams,
|
||||||
} from "../models/Users.Model";
|
} from "../models/Users.Model";
|
||||||
|
|
||||||
@@ -101,4 +102,15 @@ export class UsersController {
|
|||||||
res.status(500).json(error?.response?.data || { success: false, message: "Internal Server Error" });
|
res.status(500).json(error?.response?.data || { success: false, message: "Internal Server Error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public finalizeSubscription = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const data: SysAdminFinalizeUserSubscriptionParams = req.body;
|
||||||
|
const result = await usersService.finalizeSubscription(data);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error finalizing subscription:", error?.response?.data || error);
|
||||||
|
res.status(500).json(error?.response?.data || { success: false, message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ export type SysAdminExtendUserSubscriptionResult = {
|
|||||||
transactionId?: string;
|
transactionId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SysAdminFinalizeUserSubscriptionParams = {
|
||||||
|
userId: string;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SysAdminFinalizeUserSubscriptionResult = {
|
||||||
|
userId: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
previousEndDate: string;
|
||||||
|
newEndDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SysAdminUserSubscriptionDetailsResult = {
|
export type SysAdminUserSubscriptionDetailsResult = {
|
||||||
userId: string;
|
userId: string;
|
||||||
currentSubscription: {
|
currentSubscription: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ router.post("/organizations-status", usersController.organizationsStatus);
|
|||||||
router.post("/recalculate-plan-usage-cycle", usersController.recalculatePlanUsageCycle);
|
router.post("/recalculate-plan-usage-cycle", usersController.recalculatePlanUsageCycle);
|
||||||
router.post("/subscription-details", usersController.subscriptionDetails);
|
router.post("/subscription-details", usersController.subscriptionDetails);
|
||||||
router.post("/extend-subscription", usersController.extendSubscription);
|
router.post("/extend-subscription", usersController.extendSubscription);
|
||||||
|
router.post("/finalize-subscription", usersController.finalizeSubscription);
|
||||||
router.post("/delete", usersController.deleteUser);
|
router.post("/delete", usersController.deleteUser);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
SysAdminRecalculatePlanUsageCycleResult,
|
SysAdminRecalculatePlanUsageCycleResult,
|
||||||
SysAdminExtendUserSubscriptionParams,
|
SysAdminExtendUserSubscriptionParams,
|
||||||
SysAdminExtendUserSubscriptionResult,
|
SysAdminExtendUserSubscriptionResult,
|
||||||
|
SysAdminFinalizeUserSubscriptionParams,
|
||||||
|
SysAdminFinalizeUserSubscriptionResult,
|
||||||
SysAdminUserSubscriptionDetailsParams,
|
SysAdminUserSubscriptionDetailsParams,
|
||||||
SysAdminUserSubscriptionDetailsResult,
|
SysAdminUserSubscriptionDetailsResult,
|
||||||
} from "../models/Users.Model";
|
} from "../models/Users.Model";
|
||||||
@@ -120,4 +122,18 @@ export class UsersService {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async finalizeSubscription(
|
||||||
|
data: SysAdminFinalizeUserSubscriptionParams
|
||||||
|
): Promise<SysAdminFinalizeUserSubscriptionResult> {
|
||||||
|
const nonce = await getSysAdminNonce();
|
||||||
|
const postData = { ...data, ...{ payload: getPayload(nonce) } };
|
||||||
|
|
||||||
|
const response = await axios.post(`${getApiHost()}/sysadmin/subscriptions/finalize-user`, postData, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ NEXT_PUBLIC_DATAGRID_PAGE_SIZE = 10
|
|||||||
NEXT_PUBLIC_SLOGAN = Tu tiempo en orden.
|
NEXT_PUBLIC_SLOGAN = Tu tiempo en orden.
|
||||||
|
|
||||||
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
||||||
|
NEXT_PUBLIC_INFO_EMAIL = info@turnosxpress.com.ar
|
||||||
@@ -12,3 +12,4 @@ NEXT_PUBLIC_DATAGRID_PAGE_SIZE = 10
|
|||||||
NEXT_PUBLIC_SLOGAN = Tu tiempo en orden.
|
NEXT_PUBLIC_SLOGAN = Tu tiempo en orden.
|
||||||
|
|
||||||
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
||||||
|
NEXT_PUBLIC_INFO_EMAIL = info@turnosxpress.com.ar
|
||||||
@@ -63,6 +63,7 @@ export type VerifyPendingPlanPaymentResponse = {
|
|||||||
|
|
||||||
export type LastPlanPaymentStatus = "rejected" | "failed" | "cancelled";
|
export type LastPlanPaymentStatus = "rejected" | "failed" | "cancelled";
|
||||||
export type LastPlanPaymentType = "new" | "extension" | "upgrade";
|
export type LastPlanPaymentType = "new" | "extension" | "upgrade";
|
||||||
|
export type DowngradeReason = "expired";
|
||||||
|
|
||||||
export interface GetSuscriptionInitPointResponse {
|
export interface GetSuscriptionInitPointResponse {
|
||||||
init_point: string;
|
init_point: string;
|
||||||
@@ -84,6 +85,11 @@ export interface ISubscriptionInfo {
|
|||||||
lastPaymentPreferenceId?: string;
|
lastPaymentPreferenceId?: string;
|
||||||
lastPaymentAt?: Date;
|
lastPaymentAt?: Date;
|
||||||
lastPaymentType?: LastPlanPaymentType;
|
lastPaymentType?: LastPlanPaymentType;
|
||||||
|
downgradedFromPlanId?: string;
|
||||||
|
downgradedFromPlanName?: string;
|
||||||
|
downgradedFromPlanCode?: string;
|
||||||
|
downgradedAt?: Date;
|
||||||
|
downgradeReason?: DowngradeReason;
|
||||||
}
|
}
|
||||||
export const DefaultSubscriptionModel: ISubscriptionInfo = {
|
export const DefaultSubscriptionModel: ISubscriptionInfo = {
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -278,6 +278,43 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeNotice {
|
||||||
|
margin: 0;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border: 1px solid rgba(194, 65, 12, 0.24);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #7c2d12;
|
||||||
|
box-shadow: 0 12px 28px rgba(194, 65, 12, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeNotice span {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 5px 11px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(194, 65, 12, 0.12);
|
||||||
|
color: #9a3412;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeNotice strong {
|
||||||
|
display: block;
|
||||||
|
color: #7c2d12;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeNotice p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #9a3412;
|
||||||
|
line-height: 1.55;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ export default function CurrentPlanPage() {
|
|||||||
const plan = subscription?.plan;
|
const plan = subscription?.plan;
|
||||||
const isPaidPlan = Boolean(plan && plan.price > 0);
|
const isPaidPlan = Boolean(plan && plan.price > 0);
|
||||||
const statusText = isPaidPlan ? getSubStatusText(subscription?.mpStatus) : "ACTIVO";
|
const statusText = isPaidPlan ? getSubStatusText(subscription?.mpStatus) : "ACTIVO";
|
||||||
|
const showExpiredDowngradeNotice = Boolean(
|
||||||
|
isOwner &&
|
||||||
|
subscription?.downgradeReason === "expired" &&
|
||||||
|
subscription.downgradedAt
|
||||||
|
);
|
||||||
const canRenew = Boolean(
|
const canRenew = Boolean(
|
||||||
isOwner &&
|
isOwner &&
|
||||||
isPaidPlan &&
|
isPaidPlan &&
|
||||||
@@ -216,6 +221,17 @@ export default function CurrentPlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showExpiredDowngradeNotice && (
|
||||||
|
<div className={styles.expiredDowngradeNotice}>
|
||||||
|
<span>Plan vencido</span>
|
||||||
|
<strong>Tu plan pago venció y esta organización pasó al plan básico.</strong>
|
||||||
|
<p>
|
||||||
|
No borramos tu organización ni tus datos. Los límites cambiaron y las nuevas acciones se validan con el plan básico.
|
||||||
|
{subscription.downgradedFromPlanName && ` Tu plan anterior era ${subscription.downgradedFromPlanName}.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isPaidPlan && (
|
{!isPaidPlan && (
|
||||||
<p className={styles.notice}>Estás usando el plan gratis. No tiene vencimiento pago.</p>
|
<p className={styles.notice}>Estás usando el plan gratis. No tiene vencimiento pago.</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import MetricGraph from "@components/MetricGraph/MetricGraph";
|
|
||||||
import AnimatedContainer from "@core/app/components/AnimatedConainer/AnimatedContainer";
|
import AnimatedContainer from "@core/app/components/AnimatedConainer/AnimatedContainer";
|
||||||
import Button from "@core/app/components/Button/Button";
|
import Button from "@core/app/components/Button/Button";
|
||||||
import { useNavigation } from "@core/app/hooks/goto";
|
import { useNavigation } from "@core/app/hooks/goto";
|
||||||
@@ -36,7 +35,6 @@ import { blurElementsOnLoad } from "@core/helpers/blur";
|
|||||||
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ScrollToTop } from "@core/app/components/ScrollTop";
|
import { ScrollToTop } from "@core/app/components/ScrollTop";
|
||||||
import { isNull } from "@core/app/helpers/IsNull";
|
|
||||||
|
|
||||||
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
|
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
|
||||||
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
|
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
|
||||||
@@ -56,15 +54,7 @@ function DashboardContent() {
|
|||||||
const eventHandler = useEventHandlerStore();
|
const eventHandler = useEventHandlerStore();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const confirm = useConfirmStore();
|
const confirm = useConfirmStore();
|
||||||
|
const infoEmail = process.env.NEXT_PUBLIC_INFO_EMAIL || "info@turnosxpress.com.ar";
|
||||||
const [metrics, setMetrics] = useState({
|
|
||||||
organizationsCount: 0,
|
|
||||||
employeesCount: 0,
|
|
||||||
clientsCount: 0,
|
|
||||||
appointmentsCount: 0,
|
|
||||||
servicesCount: 0,
|
|
||||||
repeatsCount: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [showOnlyMine, setShowOnlyMine] = useState(false);
|
const [showOnlyMine, setShowOnlyMine] = useState(false);
|
||||||
const [isAgendaCollapsed, setIsAgendaCollapsed] = useState(true);
|
const [isAgendaCollapsed, setIsAgendaCollapsed] = useState(true);
|
||||||
@@ -164,6 +154,7 @@ function DashboardContent() {
|
|||||||
const loadCompanyMetrics = async (companyId: string) => {
|
const loadCompanyMetrics = async (companyId: string) => {
|
||||||
setCompanySubscription(null);
|
setCompanySubscription(null);
|
||||||
setCompanyMetrics(null);
|
setCompanyMetrics(null);
|
||||||
|
setIsCompanyOwner(false);
|
||||||
try {
|
try {
|
||||||
const res = await getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId });
|
const res = await getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId });
|
||||||
setCompanyMetrics(res.metrics);
|
setCompanyMetrics(res.metrics);
|
||||||
@@ -315,6 +306,7 @@ function DashboardContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isUsingSessionSubscriptionFallback = !companySubscription && Boolean(subscription.plan.id);
|
const isUsingSessionSubscriptionFallback = !companySubscription && Boolean(subscription.plan.id);
|
||||||
|
const selectedOrganization = organizations.find((organization) => organization.id === selectedOrgId);
|
||||||
const displayedSubscription = companySubscription ?? (subscription.plan.id ? subscription : null);
|
const displayedSubscription = companySubscription ?? (subscription.plan.id ? subscription : null);
|
||||||
const displayedPlan = displayedSubscription?.plan;
|
const displayedPlan = displayedSubscription?.plan;
|
||||||
const displayedPlanStatusText = displayedPlan?.price === 0 ? "ACTIVO" : getSubStatusText(displayedSubscription?.mpStatus);
|
const displayedPlanStatusText = displayedPlan?.price === 0 ? "ACTIVO" : getSubStatusText(displayedSubscription?.mpStatus);
|
||||||
@@ -325,11 +317,30 @@ function DashboardContent() {
|
|||||||
displayedSubscription.lastPaymentAt &&
|
displayedSubscription.lastPaymentAt &&
|
||||||
dayjs().diff(dayjs(displayedSubscription.lastPaymentAt), "day") <= 14
|
dayjs().diff(dayjs(displayedSubscription.lastPaymentAt), "day") <= 14
|
||||||
);
|
);
|
||||||
const showPaymentAction = Boolean(
|
const selectedCompanyHasPendingPayment = Boolean(
|
||||||
|
companySubscription &&
|
||||||
|
(companySubscription.mpStatus === MP_SUBS_STATUS.PENDING || companySubscription.pendingPaymentPreferenceId)
|
||||||
|
);
|
||||||
|
const showPaymentAction = selectedOrgId
|
||||||
|
? Boolean(selectedCompanyHasPendingPayment && isCompanyOwner)
|
||||||
|
: Boolean(
|
||||||
(displayedSubscription?.mpStatus === MP_SUBS_STATUS.PENDING || hasPendingPaymentMetadata) &&
|
(displayedSubscription?.mpStatus === MP_SUBS_STATUS.PENDING || hasPendingPaymentMetadata) &&
|
||||||
(isCompanyOwner || isUsingSessionSubscriptionFallback)
|
isUsingSessionSubscriptionFallback
|
||||||
);
|
);
|
||||||
const showFailedPaymentCard = Boolean(hasRecentFailedPayment && !showPaymentAction);
|
const showFailedPaymentCard = Boolean(hasRecentFailedPayment && !showPaymentAction);
|
||||||
|
const showExpiredDowngradeCard = Boolean(
|
||||||
|
displayedSubscription?.downgradeReason === "expired" &&
|
||||||
|
displayedSubscription.downgradedAt &&
|
||||||
|
isCompanyOwner
|
||||||
|
);
|
||||||
|
const selectedCompanyPlanSupportsRepeats = Boolean(
|
||||||
|
companySubscription?.plan &&
|
||||||
|
(companySubscription.plan.limitRepeats === -1 || companySubscription.plan.limitRepeats > 0)
|
||||||
|
);
|
||||||
|
const canLoadPendingRepeats = Boolean(
|
||||||
|
selectedCompanyPlanSupportsRepeats &&
|
||||||
|
(isCompanyOwner || selectedOrganization?.isAdmin)
|
||||||
|
);
|
||||||
const showFreePlanStarterContent = Boolean(displayedPlan && displayedPlan.price === 0 && organizations.length === 0);
|
const showFreePlanStarterContent = Boolean(displayedPlan && displayedPlan.price === 0 && organizations.length === 0);
|
||||||
const agendaItems = dashboardData?.personalAgenda ?? [];
|
const agendaItems = dashboardData?.personalAgenda ?? [];
|
||||||
const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId);
|
const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId);
|
||||||
@@ -365,7 +376,12 @@ function DashboardContent() {
|
|||||||
<div style={{ position: "absolute", top: "20px", right: "20px", display: "flex", alignItems: "center", gap: "10px" }}>
|
<div style={{ position: "absolute", top: "20px", right: "20px", display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<select
|
<select
|
||||||
value={selectedOrgId}
|
value={selectedOrgId}
|
||||||
onChange={e => setSelectedOrgId(e.target.value)}
|
onChange={e => {
|
||||||
|
setCompanySubscription(null);
|
||||||
|
setCompanyMetrics(null);
|
||||||
|
setIsCompanyOwner(false);
|
||||||
|
setSelectedOrgId(e.target.value);
|
||||||
|
}}
|
||||||
style={{ padding: "8px 12px", borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "14px", fontWeight: "500", backgroundColor: "#f9fafb", cursor: "pointer", outline: "none" }}
|
style={{ padding: "8px 12px", borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "14px", fontWeight: "500", backgroundColor: "#f9fafb", cursor: "pointer", outline: "none" }}
|
||||||
>
|
>
|
||||||
{organizations.map(org => (
|
{organizations.map(org => (
|
||||||
@@ -435,6 +451,7 @@ function DashboardContent() {
|
|||||||
<p>
|
<p>
|
||||||
Si ya pagaste, puede tardar unos minutos en actualizarse porque dependemos de la notificación de MercadoPago.
|
Si ya pagaste, puede tardar unos minutos en actualizarse porque dependemos de la notificación de MercadoPago.
|
||||||
No te preocupes: podés volver al checkout si quedó incompleto o pedirnos que verifiquemos el pago ahora mismo.
|
No te preocupes: podés volver al checkout si quedó incompleto o pedirnos que verifiquemos el pago ahora mismo.
|
||||||
|
{" "}Si el problema continúa, escribinos a <a className={style.pendingPaymentLink} href={`mailto:${infoEmail}`}>{infoEmail}</a> y te ayudamos a revisarlo.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.pendingPaymentActions}>
|
<div className={style.pendingPaymentActions}>
|
||||||
@@ -482,6 +499,25 @@ function DashboardContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showExpiredDowngradeCard && (
|
||||||
|
<div className={style.expiredDowngradeCard}>
|
||||||
|
<div className={style.pendingPaymentContent}>
|
||||||
|
<span className={style.expiredDowngradeEyebrow}>Plan vencido</span>
|
||||||
|
<h2>Tu plan pago venció y pasaste al plan básico</h2>
|
||||||
|
<p>
|
||||||
|
No borramos tu organización ni tus datos. Desde ahora las nuevas acciones usan los límites del plan básico.
|
||||||
|
{displayedSubscription?.downgradedFromPlanName && ` Tu plan anterior era ${displayedSubscription.downgradedFromPlanName}.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={style.expiredDowngradeButton}
|
||||||
|
onClick={() => goTo(selectedOrgId ? `/landing/upgrade-plan?org=${selectedOrgId}` : "/landing/pricing")}
|
||||||
|
>
|
||||||
|
Volver a contratar un plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showFreePlanStarterContent && (
|
{showFreePlanStarterContent && (
|
||||||
<div className={style.starterCard}>
|
<div className={style.starterCard}>
|
||||||
<div className={style.starterContent}>
|
<div className={style.starterContent}>
|
||||||
@@ -649,7 +685,7 @@ function DashboardContent() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Repeats Pending Card */}
|
{/* Repeats Pending Card */}
|
||||||
{dashboardData && selectedOrgId && (
|
{dashboardData && selectedOrgId && canLoadPendingRepeats && (
|
||||||
<PendingRepeatsCard
|
<PendingRepeatsCard
|
||||||
companyId={selectedOrgId}
|
companyId={selectedOrgId}
|
||||||
allowEmployeeFilter={true}
|
allowEmployeeFilter={true}
|
||||||
|
|||||||
@@ -185,6 +185,28 @@
|
|||||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeCard {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 26px 30px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff7ed;
|
||||||
|
border: 1px solid rgba(194, 65, 12, 0.24);
|
||||||
|
box-shadow: 0 12px 28px rgba(194, 65, 12, 0.08);
|
||||||
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeCard .pendingPaymentContent h2 {
|
||||||
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeCard .pendingPaymentContent p {
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
.pendingPaymentContent {
|
.pendingPaymentContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -216,6 +238,35 @@
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeEyebrow {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5px 11px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(194, 65, 12, 0.12);
|
||||||
|
color: #9a3412;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeButton {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid rgba(124, 45, 18, 0.22);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #7c2d12;
|
||||||
|
color: #fff7ed;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 14px 18px;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeButton:hover {
|
||||||
|
box-shadow: 0 10px 24px rgba(124, 45, 18, 0.18);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
.pendingPaymentContent h2 {
|
.pendingPaymentContent h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
@@ -228,6 +279,13 @@
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPaymentLink {
|
||||||
|
color: #9a4b00;
|
||||||
|
font-weight: 800;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.pendingPaymentActions {
|
.pendingPaymentActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -433,6 +491,16 @@
|
|||||||
padding: 22px;
|
padding: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeCard {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiredDowngradeButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.pendingPaymentActions {
|
.pendingPaymentActions {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user