refactor: modularize notification job creation to support reminder-only flow for recurring appointments
This commit is contained in:
@@ -533,47 +533,18 @@ class AppointmentManager implements IAppointmentsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create jobs for email and whatsapp notifications
|
//Create jobs for email and whatsapp notifications
|
||||||
if (notification && newAppointment.id) {
|
if (this.shouldCreateCustomerNotificationJobs(notification, data.repeatId) && newAppointment.id) {
|
||||||
const emailContent = await this.tryToSendNotification({
|
await this.createAppointmentNotificationJobs({
|
||||||
appointmentId: String(newAppointment.id),
|
|
||||||
sessionUser: String(companyCheck.ownerId),
|
|
||||||
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
|
|
||||||
channel: "email",
|
|
||||||
});
|
|
||||||
const wapContent = await this.tryToSendNotification({
|
|
||||||
appointmentId: String(newAppointment.id),
|
|
||||||
sessionUser: String(companyCheck.ownerId),
|
|
||||||
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
|
|
||||||
channel: "whatsapp",
|
|
||||||
});
|
|
||||||
const clientPhoneNumber = await this.getOptionalClientWapNumber(checkClient);
|
|
||||||
const reminderEmailContent = await this.tryToSendNotification({
|
|
||||||
appointmentId: String(newAppointment.id),
|
|
||||||
sessionUser: String(companyCheck.ownerId),
|
|
||||||
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
|
|
||||||
channel: "email",
|
|
||||||
});
|
|
||||||
const reminderWapContent = await this.tryToSendNotification({
|
|
||||||
appointmentId: String(newAppointment.id),
|
|
||||||
sessionUser: String(companyCheck.ownerId),
|
|
||||||
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
|
|
||||||
channel: "whatsapp",
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.createCreationNotificationJobs({
|
|
||||||
appointmentId: String(newAppointment.id),
|
appointmentId: String(newAppointment.id),
|
||||||
companyId: String(companyCheck._id),
|
companyId: String(companyCheck._id),
|
||||||
clientId: String(checkClient._id),
|
clientId: String(checkClient._id),
|
||||||
clientUserId: checkClient.userId ? String(checkClient.userId) : undefined,
|
clientUserId: checkClient.userId ? String(checkClient.userId) : undefined,
|
||||||
clientEmail: checkClient.email,
|
clientEmail: checkClient.email,
|
||||||
clientPhoneNumber,
|
client: checkClient,
|
||||||
companyOwnerId: String(companyCheck.ownerId),
|
companyOwnerId: String(companyCheck.ownerId),
|
||||||
companyName: companyCheck.name,
|
companyName: companyCheck.name,
|
||||||
appointmentStart: new Date(data.start),
|
appointmentStart: new Date(data.start),
|
||||||
emailMessage: emailContent.message,
|
notification,
|
||||||
wapMessage: wapContent.message,
|
|
||||||
reminderEmailMessage: reminderEmailContent.message,
|
|
||||||
reminderWapMessage: reminderWapContent.message,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2239,7 +2210,6 @@ public async updateAppointment(data: UpdateAppointmentParams): Promise<void> {
|
|||||||
reminderWapMessage: string;
|
reminderWapMessage: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const systemSubject = `Turno reservado en ${data.companyName}`;
|
const systemSubject = `Turno reservado en ${data.companyName}`;
|
||||||
const reminderSystemSubject = `Recordatorio de turno en ${data.companyName}`;
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
email: data.clientEmail,
|
email: data.clientEmail,
|
||||||
@@ -2263,6 +2233,93 @@ public async updateAppointment(data: UpdateAppointmentParams): Promise<void> {
|
|||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.createReminderNotificationJobs(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldCreateCustomerNotificationJobs(notification: boolean, repeatId?: string): boolean {
|
||||||
|
return notification || Boolean(repeatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAppointmentNotificationJobs(data: {
|
||||||
|
appointmentId: string;
|
||||||
|
companyId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientUserId?: string;
|
||||||
|
clientEmail?: string;
|
||||||
|
client: IClientDocument;
|
||||||
|
companyOwnerId: string;
|
||||||
|
companyName: string;
|
||||||
|
appointmentStart: Date;
|
||||||
|
notification: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const clientPhoneNumber = await this.getOptionalClientWapNumber(data.client);
|
||||||
|
const reminderEmailContent = await this.tryToSendNotification({
|
||||||
|
appointmentId: data.appointmentId,
|
||||||
|
sessionUser: data.companyOwnerId,
|
||||||
|
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
|
||||||
|
channel: "email",
|
||||||
|
});
|
||||||
|
const reminderWapContent = await this.tryToSendNotification({
|
||||||
|
appointmentId: data.appointmentId,
|
||||||
|
sessionUser: data.companyOwnerId,
|
||||||
|
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
|
||||||
|
channel: "whatsapp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const notificationJobData = {
|
||||||
|
appointmentId: data.appointmentId,
|
||||||
|
companyId: data.companyId,
|
||||||
|
clientId: data.clientId,
|
||||||
|
clientUserId: data.clientUserId,
|
||||||
|
clientEmail: data.clientEmail,
|
||||||
|
clientPhoneNumber,
|
||||||
|
companyOwnerId: data.companyOwnerId,
|
||||||
|
companyName: data.companyName,
|
||||||
|
appointmentStart: data.appointmentStart,
|
||||||
|
reminderEmailMessage: reminderEmailContent.message,
|
||||||
|
reminderWapMessage: reminderWapContent.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.notification) {
|
||||||
|
await this.createReminderNotificationJobs(notificationJobData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailContent = await this.tryToSendNotification({
|
||||||
|
appointmentId: data.appointmentId,
|
||||||
|
sessionUser: data.companyOwnerId,
|
||||||
|
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
|
||||||
|
channel: "email",
|
||||||
|
});
|
||||||
|
const wapContent = await this.tryToSendNotification({
|
||||||
|
appointmentId: data.appointmentId,
|
||||||
|
sessionUser: data.companyOwnerId,
|
||||||
|
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
|
||||||
|
channel: "whatsapp",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.createCreationNotificationJobs({
|
||||||
|
...notificationJobData,
|
||||||
|
emailMessage: emailContent.message,
|
||||||
|
wapMessage: wapContent.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createReminderNotificationJobs(data: {
|
||||||
|
appointmentId: string;
|
||||||
|
companyId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientUserId?: string;
|
||||||
|
clientEmail?: string;
|
||||||
|
clientPhoneNumber?: string;
|
||||||
|
companyOwnerId: string;
|
||||||
|
companyName: string;
|
||||||
|
appointmentStart: Date;
|
||||||
|
reminderEmailMessage: string;
|
||||||
|
reminderWapMessage: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const reminderSystemSubject = `Recordatorio de turno en ${data.companyName}`;
|
||||||
|
|
||||||
await this.jobService.createReminderJobs({
|
await this.jobService.createReminderJobs({
|
||||||
companyId: data.companyId,
|
companyId: data.companyId,
|
||||||
clientId: data.clientId,
|
clientId: data.clientId,
|
||||||
|
|||||||
@@ -681,6 +681,73 @@ describe("creation notification jobs", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
it("falls back to organization WAP Alta template for email when email Alta is absent", async () => {
|
||||||
(CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({
|
(CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({
|
||||||
_id: "company-001",
|
_id: "company-001",
|
||||||
|
|||||||
Reference in New Issue
Block a user