feat: integrate reminder payloads into appointment creation and update notification jobs and adjust quiet hours logic

This commit is contained in:
2026-07-21 19:39:38 -03:00
parent 10ca449f88
commit aac53e13cc
5 changed files with 296 additions and 15 deletions
@@ -16,6 +16,7 @@ export interface CreateJobServiceParams {
scheduledAt?: Date;
appointmentStart?: Date;
payload?: CreateJobParams["payload"];
reminderPayload?: CreateJobParams["payload"];
}
export class NotificationJobService {
@@ -81,6 +82,7 @@ export class NotificationJobService {
channel,
type: "reminder",
scheduledAt: reminderScheduledAt,
payload: params.reminderPayload,
});
jobs.push(job);
}
@@ -153,12 +155,9 @@ export class NotificationJobService {
if (isInsideQuietHours) {
// Shift to quiet hours end
let adjusted = scheduled.hour(toHour).minute(toMinute).second(0);
// If quiet hours cross midnight (e.g. 22:00-07:00) and end is before start,
// the end is on the next day
if (toHour < fromHour) {
if (scheduledHour >= fromHour || scheduledHour < toHour) {
adjusted = adjusted.add(1, "day");
}
// If quiet hours cross midnight, only the pre-midnight segment ends next day.
if (toHour < fromHour && scheduledHour >= fromHour) {
adjusted = adjusted.add(1, "day");
}
return adjusted.toDate();
}
@@ -112,6 +112,45 @@ describe("NotificationJobService.createJob", () => {
]);
});
it("passes reminder snapshot payload to scheduled reminder jobs", async () => {
jest.useFakeTimers().setSystemTime(new Date("2026-07-21T14:00:00.000Z"));
const resolvedPolicy: ResolvedPolicy = {
channels: ["whatsapp", "email"],
timezone: "America/Argentina/Buenos_Aires",
reminderRules: [{ offset: 30, enabled: true }],
};
const payload = { message: "Creation body" };
const reminderPayload = {
email: "client@example.com",
phoneNumber: "5491112345678",
companyOwnerId: "owner-1",
emailMessage: "Reminder email body",
wapMessage: "Reminder WAP body",
};
mockResolverResolve.mockResolvedValue(resolvedPolicy);
mockJobCreate.mockResolvedValue({ id: "job-1" });
await service.createJob({
companyId: "company-1",
clientId: "client-1",
appointmentId: "appt-1",
type: "creation",
appointmentStart: new Date("2026-07-21T15:00:00.000Z"),
payload,
reminderPayload,
});
const createCalls = mockJobCreate.mock.calls.map((call) => call[0]);
expect(createCalls.filter((job) => job.type === "creation")).toEqual([
expect.objectContaining({ channel: "whatsapp", payload }),
expect.objectContaining({ channel: "email", payload }),
]);
expect(createCalls.filter((job) => job.type === "reminder")).toEqual([
expect.objectContaining({ channel: "whatsapp", payload: reminderPayload }),
expect.objectContaining({ channel: "email", payload: reminderPayload }),
]);
});
it("skips reminder jobs scheduled in the past", async () => {
jest.useFakeTimers().setSystemTime(new Date("2026-07-21T14:29:00.000Z"));
const resolvedPolicy: ResolvedPolicy = {
@@ -206,6 +245,27 @@ describe("NotificationJobService.createJob", () => {
expect(createCall.scheduledAt.getUTCDate()).toBe(22); // next day
});
it("adjusts early morning quiet hours crossing midnight to the same day", async () => {
const resolvedPolicy: ResolvedPolicy = {
channels: ["system"],
timezone: "America/Argentina/Buenos_Aires",
quietHours: { from: "22:00", to: "07:00" },
};
mockResolverResolve.mockResolvedValue(resolvedPolicy);
mockJobCreate.mockResolvedValue({ id: "job-1" });
await service.createJob({
companyId: "company-1",
clientId: "client-1",
appointmentId: "appt-1",
type: "creation",
scheduledAt: new Date("2026-07-21T05:00:00.000Z"),
});
const createCall = mockJobCreate.mock.calls[0][0];
expect(createCall.scheduledAt.toISOString()).toBe("2026-07-21T07:00:00.000Z");
});
it("does not adjust scheduledAt when outside quiet hours", async () => {
const resolvedPolicy: ResolvedPolicy = {
channels: ["system"],