828 lines
30 KiB
TypeScript
828 lines
30 KiB
TypeScript
/**
|
|
* 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(),
|
|
},
|
|
getCompanyAddress: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock("../../Employees/Employee", () => ({
|
|
__esModule: true,
|
|
default: {
|
|
employees: {
|
|
findOne: jest.fn(),
|
|
},
|
|
checkPermission: 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(),
|
|
sendEmail: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock("../../SystemNotifications/SystemNotification", () => ({
|
|
__esModule: true,
|
|
default: {
|
|
create: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock("../../PlanSubscriptions/PlanSubscriptons", () => ({
|
|
__esModule: true,
|
|
default: {
|
|
findOne: jest.fn(),
|
|
checkFeature: 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: {
|
|
templates: {
|
|
findOne: jest.fn(),
|
|
},
|
|
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";
|
|
import CompaniesManager from "../../Companies/Companies";
|
|
import Templates from "../../Templates/Templates";
|
|
import PlanSubscriptionsList from "../../PlanSubscriptions/PlanSubscriptons";
|
|
import { NotificationsManager } from "../../Notifications/Notifications";
|
|
import { APPOINTMENT_NOTIFICATION_TYPE } from "../Appointments.Interface";
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
describe("cancellation notification jobs", () => {
|
|
let originalAdapter: any;
|
|
let originalJobService: any;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
originalAdapter = AppointmentsList.Appointments;
|
|
originalJobService = (AppointmentsList as any).jobService;
|
|
|
|
(AppointmentsList as any).Appointments = {
|
|
findOne: jest.fn().mockResolvedValue({
|
|
_id: "appt-001",
|
|
companyId: "company-001",
|
|
clientId: "client-001",
|
|
employeeId: "employee-001",
|
|
serviceId: "service-001",
|
|
start: new Date("2026-07-22T11:00:00.000Z"),
|
|
price: 1500,
|
|
}),
|
|
};
|
|
(AppointmentsList as any).jobService = {
|
|
createJob: jest.fn().mockResolvedValue([]),
|
|
createMandatoryCreationJob: jest.fn().mockResolvedValue([]),
|
|
createReminderJobs: jest.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
(CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "company-001",
|
|
name: "clases llavallol",
|
|
ownerId: "owner-001",
|
|
templateEmailCancellationId: "email-template-001",
|
|
templateWapCancellationId: "wap-template-001",
|
|
address: "Av. Siempre Viva 123",
|
|
});
|
|
(CompaniesManager.getCompanyAddress as jest.Mock).mockReturnValue("Av. Siempre Viva 123");
|
|
(ClientsManager.clients.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "client-001",
|
|
userId: "client-user-001",
|
|
firstName: "Ada",
|
|
lastName: "Lovelace",
|
|
email: "ada@example.com",
|
|
});
|
|
(ClientsManager.getClientFullName as jest.Mock).mockReturnValue("Ada Lovelace");
|
|
(EmployeesList.employees.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "employee-001",
|
|
userId: "employee-user-001",
|
|
});
|
|
(EmployeesList.checkPermission as jest.Mock | undefined)?.mockResolvedValue?.(true);
|
|
(UsersManager.users.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "employee-user-001",
|
|
firstName: "Grace",
|
|
lastName: "Hopper",
|
|
});
|
|
(UsersManager.getUserFullName as jest.Mock).mockReturnValue("Grace Hopper");
|
|
(ServiceList.services.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "service-001",
|
|
name: "Contabilidad",
|
|
});
|
|
(Templates.templates.findOne as jest.Mock).mockImplementation(({ id }) => {
|
|
if (id === "email-template-001") {
|
|
return Promise.resolve({
|
|
template: "EMAIL CANCEL <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->",
|
|
});
|
|
}
|
|
|
|
if (id === "wap-template-001") {
|
|
return Promise.resolve({
|
|
template: "WAP CANCEL <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->",
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
(AppointmentsList as any).Appointments = originalAdapter;
|
|
(AppointmentsList as any).jobService = originalJobService;
|
|
});
|
|
|
|
it("stores organization-rendered cancellation template content per channel", async () => {
|
|
const emailContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "cancellation",
|
|
channel: "email",
|
|
});
|
|
const wapContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "cancellation",
|
|
channel: "whatsapp",
|
|
});
|
|
|
|
await (AppointmentsList as any).createCancellationNotificationJobs({
|
|
appointmentId: "appt-001",
|
|
companyId: "company-001",
|
|
clientId: "client-001",
|
|
clientUserId: "client-user-001",
|
|
clientEmail: "ada@example.com",
|
|
clientPhoneNumber: "5491112345678",
|
|
companyOwnerId: "owner-001",
|
|
companyName: "clases llavallol",
|
|
emailMessage: emailContent.message,
|
|
wapMessage: wapContent.message,
|
|
});
|
|
|
|
expect((AppointmentsList as any).jobService.createJob).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: "cancellation",
|
|
payload: expect.objectContaining({
|
|
email: "ada@example.com",
|
|
phoneNumber: "5491112345678",
|
|
userId: "client-user-001",
|
|
companyOwnerId: "owner-001",
|
|
emailSubject: "TurnosXpress :: Turno cancelado",
|
|
emailMessage: "EMAIL CANCEL Ada Lovelace Contabilidad clases llavallol 22/07/2026 08:00hs.",
|
|
wapMessage: "WAP CANCEL Ada Lovelace Contabilidad clases llavallol 22/07/2026 08:00hs.",
|
|
systemSubject: "Turno cancelado en clases llavallol",
|
|
systemMessage: "EMAIL CANCEL Ada Lovelace Contabilidad clases llavallol 22/07/2026 08:00hs.",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends update email notifications with a non-empty subject", async () => {
|
|
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
|
|
|
await (AppointmentsList as any).sendEmailNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: APPOINTMENT_NOTIFICATION_TYPE.UPDATE,
|
|
});
|
|
|
|
expect(NotificationsManager.sendEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
email: "ada@example.com",
|
|
subject: "TurnosXpress :: Turno actualizado",
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("creation notification jobs", () => {
|
|
let originalAdapter: any;
|
|
let originalJobService: any;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
originalAdapter = AppointmentsList.Appointments;
|
|
originalJobService = (AppointmentsList as any).jobService;
|
|
|
|
(AppointmentsList as any).Appointments = {
|
|
findOne: jest.fn().mockResolvedValue({
|
|
_id: "appt-001",
|
|
companyId: "company-001",
|
|
clientId: "client-001",
|
|
employeeId: "employee-001",
|
|
serviceId: "service-001",
|
|
start: new Date("2026-07-22T11:00:00.000Z"),
|
|
price: 1500,
|
|
}),
|
|
};
|
|
(AppointmentsList as any).jobService = {
|
|
createJob: jest.fn().mockResolvedValue([]),
|
|
createMandatoryCreationJob: jest.fn().mockResolvedValue([]),
|
|
createReminderJobs: jest.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
(CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "company-001",
|
|
name: "clases llavallol",
|
|
ownerId: "owner-001",
|
|
templateEmailAltaId: "email-alta-template-001",
|
|
templateWapAltaId: "wap-alta-template-001",
|
|
address: "Av. Siempre Viva 123",
|
|
});
|
|
(CompaniesManager.getCompanyAddress as jest.Mock).mockReturnValue("Av. Siempre Viva 123");
|
|
(ClientsManager.clients.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "client-001",
|
|
userId: "client-user-001",
|
|
firstName: "Ada",
|
|
lastName: "Lovelace",
|
|
email: "ada@example.com",
|
|
});
|
|
(ClientsManager.getClientFullName as jest.Mock).mockReturnValue("Ada Lovelace");
|
|
(EmployeesList.employees.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "employee-001",
|
|
userId: "employee-user-001",
|
|
});
|
|
(EmployeesList.checkPermission as jest.Mock | undefined)?.mockResolvedValue?.(true);
|
|
(UsersManager.users.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "employee-user-001",
|
|
firstName: "Grace",
|
|
lastName: "Hopper",
|
|
});
|
|
(UsersManager.getUserFullName as jest.Mock).mockReturnValue("Grace Hopper");
|
|
(ServiceList.services.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "service-001",
|
|
name: "Historia",
|
|
});
|
|
(Templates.templates.findOne as jest.Mock).mockImplementation(({ id }) => {
|
|
if (id === "email-alta-template-001") {
|
|
return Promise.resolve({
|
|
template: "EMAIL ALTA <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->",
|
|
});
|
|
}
|
|
|
|
if (id === "wap-alta-template-001") {
|
|
return Promise.resolve({
|
|
template: "WAP ALTA <---CLIENTE---> <---SERVICIO---> <---ORGANIZACION---> <---FECHA---> <---HORARIO--->",
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
(AppointmentsList as any).Appointments = originalAdapter;
|
|
(AppointmentsList as any).jobService = originalJobService;
|
|
});
|
|
|
|
it("stores organization-rendered Alta template content per channel", async () => {
|
|
const emailContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "creation",
|
|
channel: "email",
|
|
});
|
|
const wapContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "creation",
|
|
channel: "whatsapp",
|
|
});
|
|
|
|
await (AppointmentsList as any).createCreationNotificationJobs({
|
|
appointmentId: "appt-001",
|
|
companyId: "company-001",
|
|
clientId: "client-001",
|
|
clientUserId: "client-user-001",
|
|
clientEmail: "ada@example.com",
|
|
clientPhoneNumber: "5491112345678",
|
|
companyOwnerId: "owner-001",
|
|
companyName: "clases llavallol",
|
|
appointmentStart: new Date("2026-07-22T11:00:00.000Z"),
|
|
emailMessage: emailContent.message,
|
|
wapMessage: wapContent.message,
|
|
reminderEmailMessage: "EMAIL REMINDER Ada Lovelace",
|
|
reminderWapMessage: "WAP REMINDER Ada Lovelace",
|
|
});
|
|
|
|
expect((AppointmentsList as any).jobService.createMandatoryCreationJob).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: "creation",
|
|
payload: expect.objectContaining({
|
|
emailSubject: "TurnosXpress :: Alta de turno",
|
|
emailMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.",
|
|
wapMessage: "WAP ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.",
|
|
systemSubject: "Turno reservado en clases llavallol",
|
|
systemMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.",
|
|
}),
|
|
})
|
|
);
|
|
expect((AppointmentsList as any).jobService.createReminderJobs).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: "reminder",
|
|
reminderPayload: expect.objectContaining({
|
|
email: "ada@example.com",
|
|
phoneNumber: "5491112345678",
|
|
userId: "client-user-001",
|
|
companyOwnerId: "owner-001",
|
|
emailSubject: "TurnosXpress :: Recordatorio",
|
|
emailMessage: "EMAIL REMINDER Ada Lovelace",
|
|
wapMessage: "WAP REMINDER Ada Lovelace",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("routes customer notification jobs for creation, repeat reminder-only, and disabled notifications", () => {
|
|
expect((AppointmentsList as any).shouldCreateCustomerNotificationJobs(true, undefined)).toBe(true);
|
|
expect((AppointmentsList as any).shouldCreateCustomerNotificationJobs(false, "repeat-001")).toBe(true);
|
|
expect((AppointmentsList as any).shouldCreateCustomerNotificationJobs(false, undefined)).toBe(false);
|
|
});
|
|
|
|
it("creates reminder-only jobs without rendering Alta content for repeat-created appointments", async () => {
|
|
const tryToSendNotificationSpy = jest
|
|
.spyOn(AppointmentsList as any, "tryToSendNotification")
|
|
.mockImplementation(({ type, channel }: any) => {
|
|
if (type === APPOINTMENT_NOTIFICATION_TYPE.CREATION) {
|
|
throw new Error("Creation content should not be rendered");
|
|
}
|
|
|
|
return Promise.resolve({
|
|
message: channel === "email" ? "EMAIL REMINDER Ada Lovelace" : "WAP REMINDER Ada Lovelace",
|
|
});
|
|
});
|
|
const getOptionalClientWapNumberSpy = jest
|
|
.spyOn(AppointmentsList as any, "getOptionalClientWapNumber")
|
|
.mockResolvedValue("5491112345678");
|
|
|
|
try {
|
|
await (AppointmentsList as any).createAppointmentNotificationJobs({
|
|
appointmentId: "appt-001",
|
|
companyId: "company-001",
|
|
clientId: "client-001",
|
|
clientUserId: "client-user-001",
|
|
clientEmail: "ada@example.com",
|
|
client: { _id: "client-001" },
|
|
companyOwnerId: "owner-001",
|
|
companyName: "clases llavallol",
|
|
appointmentStart: new Date("2026-07-22T11:00:00.000Z"),
|
|
notification: false,
|
|
});
|
|
|
|
expect(tryToSendNotificationSpy).toHaveBeenCalledTimes(2);
|
|
expect(tryToSendNotificationSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER, channel: "email" })
|
|
);
|
|
expect(tryToSendNotificationSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER, channel: "whatsapp" })
|
|
);
|
|
expect((AppointmentsList as any).jobService.createMandatoryCreationJob).not.toHaveBeenCalled();
|
|
expect((AppointmentsList as any).jobService.createReminderJobs).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: "reminder",
|
|
appointmentStart: new Date("2026-07-22T11:00:00.000Z"),
|
|
reminderPayload: expect.objectContaining({
|
|
email: "ada@example.com",
|
|
phoneNumber: "5491112345678",
|
|
userId: "client-user-001",
|
|
companyOwnerId: "owner-001",
|
|
emailSubject: "TurnosXpress :: Recordatorio",
|
|
emailMessage: "EMAIL REMINDER Ada Lovelace",
|
|
wapMessage: "WAP REMINDER Ada Lovelace",
|
|
systemSubject: "Recordatorio de turno en clases llavallol",
|
|
systemMessage: "EMAIL REMINDER Ada Lovelace",
|
|
}),
|
|
})
|
|
);
|
|
} finally {
|
|
tryToSendNotificationSpy.mockRestore();
|
|
getOptionalClientWapNumberSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("falls back to organization WAP Alta template for email when email Alta is absent", async () => {
|
|
(CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({
|
|
_id: "company-001",
|
|
name: "clases llavallol",
|
|
ownerId: "owner-001",
|
|
templateWapAltaId: "wap-alta-template-001",
|
|
address: "Av. Siempre Viva 123",
|
|
});
|
|
|
|
const emailContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "creation",
|
|
channel: "email",
|
|
});
|
|
|
|
expect(emailContent.message).toBe("WAP ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.");
|
|
});
|
|
|
|
it("stores organization-rendered update template content with contact snapshot fields", async () => {
|
|
const emailContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "update",
|
|
channel: "email",
|
|
});
|
|
const wapContent = await (AppointmentsList as any).tryToSendNotification({
|
|
appointmentId: "appt-001",
|
|
sessionUser: "owner-001",
|
|
type: "update",
|
|
channel: "whatsapp",
|
|
});
|
|
|
|
await (AppointmentsList as any).createUpdateNotificationJobs({
|
|
appointmentId: "appt-001",
|
|
companyId: "company-001",
|
|
clientId: "client-001",
|
|
clientUserId: "client-user-001",
|
|
clientEmail: "ada@example.com",
|
|
clientPhoneNumber: "5491112345678",
|
|
companyOwnerId: "owner-001",
|
|
companyName: "clases llavallol",
|
|
appointmentStart: new Date("2026-07-22T11:00:00.000Z"),
|
|
emailMessage: emailContent.message,
|
|
wapMessage: wapContent.message,
|
|
reminderEmailMessage: "EMAIL REMINDER Ada Lovelace",
|
|
reminderWapMessage: "WAP REMINDER Ada Lovelace",
|
|
});
|
|
|
|
expect((AppointmentsList as any).jobService.createJob).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: "update",
|
|
appointmentStart: new Date("2026-07-22T11:00:00.000Z"),
|
|
payload: expect.objectContaining({
|
|
email: "ada@example.com",
|
|
phoneNumber: "5491112345678",
|
|
userId: "client-user-001",
|
|
companyOwnerId: "owner-001",
|
|
emailSubject: "TurnosXpress :: Turno actualizado",
|
|
emailMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.",
|
|
wapMessage: "WAP ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.",
|
|
systemSubject: "Turno actualizado en clases llavallol",
|
|
systemMessage: "EMAIL ALTA Ada Lovelace Historia clases llavallol 22/07/2026 08:00hs.",
|
|
}),
|
|
reminderPayload: expect.objectContaining({
|
|
email: "ada@example.com",
|
|
phoneNumber: "5491112345678",
|
|
userId: "client-user-001",
|
|
companyOwnerId: "owner-001",
|
|
emailSubject: "TurnosXpress :: Recordatorio",
|
|
emailMessage: "EMAIL REMINDER Ada Lovelace",
|
|
wapMessage: "WAP REMINDER Ada Lovelace",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|