feat: implement collaborator reactivation logic, add access denied handling, and improve metric filtering
This commit is contained in:
@@ -12,6 +12,7 @@ export type FindAppointmentsParams = {
|
||||
clientId?: string;
|
||||
start?: Date;
|
||||
dateDay?: Date;
|
||||
sessionUser?: string;
|
||||
};
|
||||
|
||||
export type FindAppointmentsByUserParams = {
|
||||
|
||||
@@ -1052,6 +1052,9 @@ class AppointmentManager implements IAppointmentsManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Historical read: Employee lookup includes removed employees (historical reads allow them)
|
||||
// No snapshot fallback needed here as employee.calendarColor is used directly
|
||||
|
||||
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
||||
const clientUserName = userClient
|
||||
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
||||
@@ -1251,9 +1254,17 @@ class AppointmentManager implements IAppointmentsManager {
|
||||
_id: String(employee.userId),
|
||||
});
|
||||
|
||||
if (!userEmployee) {
|
||||
throw new Error("No se ha encontrado el usuario del colaborador");
|
||||
}
|
||||
// Historical read snapshot fallback: use Employee snapshot if User is deleted
|
||||
const collaboratorFirstName = userEmployee
|
||||
? isNull<string>(userEmployee.firstName, "")
|
||||
: isNull<string>(employee.profileSnapshot?.firstName, "");
|
||||
const collaboratorLastName = userEmployee
|
||||
? isNull<string>(userEmployee.lastName, "")
|
||||
: isNull<string>(employee.profileSnapshot?.lastName, "");
|
||||
const collaboratorFullName = joinStrings([collaboratorFirstName, collaboratorLastName], " ");
|
||||
const collaboratorAvatar = userEmployee
|
||||
? getAvatar(employee.userId, userEmployee.avatar, collaboratorFullName)
|
||||
: (employee.profileSnapshot?.avatar || collaboratorFullName);
|
||||
|
||||
const service = await ServiceList.services.findOne({
|
||||
_id: String(appointment.serviceId),
|
||||
@@ -1271,14 +1282,6 @@ class AppointmentManager implements IAppointmentsManager {
|
||||
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
||||
: clientUserName;
|
||||
|
||||
const employeeFirstName = isNull<string>(userEmployee.firstName, "");
|
||||
const employeeLastName = isNull<string>(userEmployee.lastName, "");
|
||||
const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");
|
||||
|
||||
const employeeAvatar = userEmployee
|
||||
? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
|
||||
: clientUserName;
|
||||
|
||||
const discountData = await Discounts.getDiscountData(appointment.discountId, appointment.companyId);
|
||||
|
||||
let ctaBalance = 0;
|
||||
@@ -1300,8 +1303,8 @@ class AppointmentManager implements IAppointmentsManager {
|
||||
serviceName: service.name,
|
||||
serviceDescription: service.description,
|
||||
collaboratorId: employee.id,
|
||||
collaboratorName: employeeFullName,
|
||||
collaboratorAvatar: employeeAvatar,
|
||||
collaboratorName: collaboratorFullName,
|
||||
collaboratorAvatar: collaboratorAvatar,
|
||||
appointmentDate: dayjs(appointment.start).toISOString(),
|
||||
startTime: appointment.startHour,
|
||||
endTime: appointment.endHour,
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Tests for Appointments.getAppointmentEvent snapshot fallback
|
||||
* when a removed employee's User account has been deleted.
|
||||
*
|
||||
* The fallback uses Employee.profileSnapshot fields instead of crashing.
|
||||
*/
|
||||
|
||||
// Mock all external dependencies
|
||||
jest.mock("../../Users/Users", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
users: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
getUserFullName: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Clients/Clients", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
clients: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
getClientFullName: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Companies/Companies", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
companies: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Employees/Employee", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
employees: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Services/Service", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
services: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../HeatMap/HeatMap", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getHeatMap: jest.fn(),
|
||||
addHeatMap: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Schedules/Schedules", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SchedulesEnabled/SchedulesEnabled", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SchedulesDisabled/SchedulesDisabled", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SchedulesOverrides/SchedulesOverrides", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Metrics/Metrics", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
addAppointment: jest.fn(),
|
||||
canAddAppointment: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Notifications/Notifications", () => ({
|
||||
__esModule: true,
|
||||
NotificationsManager: {
|
||||
sendSystemNotification: jest.fn(),
|
||||
sendPushNotification: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SystemNotifications/SystemNotification", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../PlanSubscriptions/PlanSubscriptons", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Repeats/Repeats", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
repeats: {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
existDisabledDate: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Discounts/Discounts", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getDiscountData: jest.fn().mockResolvedValue({
|
||||
name: "",
|
||||
value: 0,
|
||||
type: "PERCENTAGE",
|
||||
code: "",
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../../helpers/check", () => ({
|
||||
validatePermissionsByCompany: jest.fn(),
|
||||
validateSessionUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../Templates/Templates", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
sendEmail: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../ClientAccounts/ClientAccount", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getAvailableAmount: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../DiscountsEmail/DiscountsEmail", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../../index", () => ({
|
||||
io: {
|
||||
to: jest.fn(() => ({ emit: jest.fn() })),
|
||||
},
|
||||
}));
|
||||
|
||||
import AppointmentsList from "../Appointments";
|
||||
import UsersManager from "../../Users/Users";
|
||||
import EmployeesList from "../../Employees/Employee";
|
||||
import ClientsManager from "../../Clients/Clients";
|
||||
import ServiceList from "../../Services/Service";
|
||||
|
||||
describe("getAppointmentEvent — historical read snapshot fallback", () => {
|
||||
let originalAdapter: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Save and replace adapter
|
||||
originalAdapter = AppointmentsList.Appointments;
|
||||
const mockAppointmentFindOne = jest.fn();
|
||||
(AppointmentsList as any).Appointments = {
|
||||
find: jest.fn(),
|
||||
findOne: mockAppointmentFindOne,
|
||||
AppointmentList: {
|
||||
findOne: mockAppointmentFindOne,
|
||||
find: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(AppointmentsList as any).Appointments = originalAdapter;
|
||||
});
|
||||
|
||||
it("falls back to Employee snapshot when User account is deleted", async () => {
|
||||
const mockAppointment = {
|
||||
_id: "appt-001",
|
||||
id: "appt-001",
|
||||
companyId: "company-001",
|
||||
clientId: "client-001",
|
||||
employeeId: "emp-removed",
|
||||
serviceId: "svc-001",
|
||||
start: new Date("2026-07-20T10:00:00Z"),
|
||||
startHour: 10,
|
||||
endHour: 11,
|
||||
dateDay: new Date("2026-07-20"),
|
||||
price: 100,
|
||||
length: 60,
|
||||
payment: false,
|
||||
comments: "",
|
||||
discountId: undefined,
|
||||
};
|
||||
|
||||
const mockClient = {
|
||||
_id: "client-001",
|
||||
id: "client-001",
|
||||
userId: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
};
|
||||
|
||||
const mockClientUser = {
|
||||
_id: "client-user-001",
|
||||
id: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
avatar: "client-avatar",
|
||||
};
|
||||
|
||||
// Employee exists (removed, with snapshot)
|
||||
const mockRemovedEmployee = {
|
||||
_id: "emp-removed",
|
||||
id: "emp-removed",
|
||||
companyId: "company-001",
|
||||
userId: "deleted-user-id",
|
||||
calendarColor: "#ff0000",
|
||||
removed: true,
|
||||
profileSnapshot: {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john@example.com",
|
||||
avatar: "old-avatar-url",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock adapter.findOne for the appointment
|
||||
const mockAppointmentFindOne = (AppointmentsList as any).Appointments.AppointmentList.findOne;
|
||||
mockAppointmentFindOne.mockResolvedValue(mockAppointment);
|
||||
|
||||
// Mock lookups
|
||||
(ClientsManager.clients.findOne as jest.Mock).mockResolvedValue(mockClient);
|
||||
(ServiceList.services.findOne as jest.Mock).mockResolvedValue({
|
||||
_id: "svc-001",
|
||||
name: "Haircut",
|
||||
description: "Professional haircut",
|
||||
});
|
||||
|
||||
// Call order for UsersManager.users.findOne (same mock as UserList):
|
||||
// 1. Line 1217: UserList.users.findOne (NOT awaited, but consumes a value)
|
||||
// 2. Line 1241: UsersManager.users.findOne for userClient
|
||||
// 3. Line 1253: UsersManager.users.findOne for userEmployee
|
||||
(UsersManager.users.findOne as jest.Mock)
|
||||
.mockResolvedValueOnce({ _id: "session-user" }) // line 1217: not awaited, just needs truthy
|
||||
.mockResolvedValueOnce(mockClientUser) // line 1241: client user
|
||||
.mockResolvedValueOnce(null); // line 1253: employee user DELETED → triggers fallback
|
||||
(EmployeesList.employees.findOne as jest.Mock).mockResolvedValue(mockRemovedEmployee);
|
||||
(EmployeesList as any).findById = jest.fn().mockResolvedValue({
|
||||
id: "emp-removed",
|
||||
fullName: "John Doe",
|
||||
avatar: "old-avatar-url",
|
||||
});
|
||||
|
||||
const result = await AppointmentsList.getAppointmentEvent({
|
||||
appointmentId: "appt-001",
|
||||
sessionUser: "session-user",
|
||||
});
|
||||
|
||||
// Verify snapshot fields are used for collaboratorName
|
||||
expect(result.collaboratorName).toBe("John Doe");
|
||||
|
||||
// Verify snapshot avatar is used
|
||||
expect(result.collaboratorAvatar).toBe("old-avatar-url");
|
||||
|
||||
// Verify collaboratorId comes from employee, not user
|
||||
expect(result.collaboratorId).toBe("emp-removed");
|
||||
});
|
||||
|
||||
it("uses live User data when User account still exists", async () => {
|
||||
const mockAppointment = {
|
||||
_id: "appt-002",
|
||||
id: "appt-002",
|
||||
companyId: "company-001",
|
||||
clientId: "client-001",
|
||||
employeeId: "emp-active",
|
||||
serviceId: "svc-001",
|
||||
start: new Date("2026-07-20T10:00:00Z"),
|
||||
startHour: 10,
|
||||
endHour: 11,
|
||||
dateDay: new Date("2026-07-20"),
|
||||
price: 100,
|
||||
length: 60,
|
||||
payment: false,
|
||||
comments: "",
|
||||
discountId: undefined,
|
||||
};
|
||||
|
||||
const mockClient = {
|
||||
_id: "client-001",
|
||||
id: "client-001",
|
||||
userId: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
};
|
||||
|
||||
const mockClientUser = {
|
||||
_id: "client-user-001",
|
||||
id: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
avatar: "client-avatar",
|
||||
};
|
||||
|
||||
const mockActiveEmployee = {
|
||||
_id: "emp-active",
|
||||
id: "emp-active",
|
||||
companyId: "company-001",
|
||||
userId: "active-user-id",
|
||||
calendarColor: "#00ff00",
|
||||
removed: false,
|
||||
profileSnapshot: undefined,
|
||||
};
|
||||
|
||||
const mockActiveUser = {
|
||||
_id: "active-user-id",
|
||||
id: "active-user-id",
|
||||
firstName: "Jane",
|
||||
lastName: "Roe",
|
||||
avatar: "live-avatar",
|
||||
email: "jane@example.com",
|
||||
};
|
||||
|
||||
const mockAppointmentFindOne = (AppointmentsList as any).Appointments.AppointmentList.findOne;
|
||||
mockAppointmentFindOne.mockResolvedValue(mockAppointment);
|
||||
|
||||
(ClientsManager.clients.findOne as jest.Mock).mockResolvedValue(mockClient);
|
||||
(ServiceList.services.findOne as jest.Mock).mockResolvedValue({
|
||||
_id: "svc-001",
|
||||
name: "Haircut",
|
||||
description: "Professional haircut",
|
||||
});
|
||||
|
||||
// Call order for UsersManager.users.findOne (same mock as UserList):
|
||||
// 1. Line 1217: UserList.users.findOne (NOT awaited, but consumes a value)
|
||||
// 2. Line 1241: UsersManager.users.findOne for userClient
|
||||
// 3. Line 1253: UsersManager.users.findOne for userEmployee
|
||||
(UsersManager.users.findOne as jest.Mock)
|
||||
.mockResolvedValueOnce({ _id: "session-user" }) // line 1217: not awaited
|
||||
.mockResolvedValueOnce(mockClientUser) // line 1241: client user
|
||||
.mockResolvedValueOnce(mockActiveUser); // line 1253: live user exists
|
||||
(EmployeesList.employees.findOne as jest.Mock).mockResolvedValue(mockActiveEmployee);
|
||||
(EmployeesList as any).findById = jest.fn().mockResolvedValue({
|
||||
id: "emp-active",
|
||||
fullName: "Jane Roe",
|
||||
avatar: "live-avatar",
|
||||
});
|
||||
|
||||
const result = await AppointmentsList.getAppointmentEvent({
|
||||
appointmentId: "appt-002",
|
||||
sessionUser: "session-user",
|
||||
});
|
||||
|
||||
// Live user data is used, not snapshot
|
||||
expect(result.collaboratorName).toBe("Jane Roe");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user