Skip to content

Add Notification & Reminder Services + Verification Script#3

Open
PraveenYasas wants to merge 1 commit intomainfrom
jules-verify-system-functionality-11330419841980781785
Open

Add Notification & Reminder Services + Verification Script#3
PraveenYasas wants to merge 1 commit intomainfrom
jules-verify-system-functionality-11330419841980781785

Conversation

@PraveenYasas
Copy link
Copy Markdown
Collaborator

Implemented a comprehensive notification system including OTPs, appointment confirmations, and reminders. The system logs all notifications to the database. Also provided a verification script to test the functionality end-to-end.


PR created automatically by Jules for task 11330419841980781785 started by @PraveenYasas

- Created `NotificationService` to handle emails for OTPs, confirmations, and reminders, logging each to `notification_logs`.
- Updated `auth.controller.ts` to send OTPs via email instead of console logging.
- Updated `apointment.services.ts` to send appointment confirmation emails after booking.
- Implemented `ReminderService` to send reminders for upcoming appointments.
- Added `src/scripts/verify_system.ts` to verify the entire flow end-to-end.

Co-authored-by: PraveenYasas <149250835+PraveenYasas@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 26, 2026 08:32
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive notification system for the doctor appointment application, including email notifications for OTP verification, appointment confirmations, and appointment reminders. The system logs all notification attempts to the database for audit and retry purposes.

Changes:

  • Added NotificationService for sending emails (OTP, appointment confirmations, reminders) with database logging
  • Added ReminderService for processing daily appointment reminders for confirmed appointments scheduled the next day
  • Integrated notification sending into appointment booking and authentication flows
  • Created a verification script to test the complete notification system end-to-end

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 23 comments.

Show a summary per file
File Description
src/services/notification.service.ts New service for sending emails via nodemailer and logging to notification_logs table
src/services/reminder.service.ts New service for processing daily appointment reminders
src/services/apointment.services.ts Integrated appointment confirmation emails into booking flow
src/controllers/auth.controller.ts Integrated OTP emails into registration and password reset flows
src/scripts/verify_system.ts End-to-end verification script that tests registration, login, booking, and reminders

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +43 to +55
// 3. Check if already reminded
const existingLog = await prisma.notification_logs.findFirst({
where: {
appointmentId: appt.id,
type: "REMINDER",
status: "SENT",
},
});

if (existingLog) {
console.log(`[ReminderService] Reminder already sent for ${appt.id}`);
continue;
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reminder service queries for appointments that have already been sent reminders, but only checks the notification_logs table. If the notification log creation fails (line 56-73 in notification.service.ts catches and logs the error), the reminder will be attempted again on the next run. This could result in duplicate reminders being sent to patients.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +69
if (appt.patientEmail) {
const success = await NotificationService.sendAppointmentReminder(appt.patientEmail, {
...appt,
doctorName: appt.doctor.name,
date: appt.session.scheduledAt,
// time: appt.session.startTime // Session might handle time differently, but scheduledAt is good enough for now
});

if (success) sentCount++;
} else {
console.warn(`[ReminderService] No email for appointment ${appt.id}`);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code assumes appointments have a 'patientEmail' field at line 58, but according to the schema (line 320), this field is required and non-nullable in the Appointment model. The check on line 58 and the warning on line 68 suggest uncertainty about whether this field exists. Either remove the unnecessary null check or clarify the data model.

Suggested change
if (appt.patientEmail) {
const success = await NotificationService.sendAppointmentReminder(appt.patientEmail, {
...appt,
doctorName: appt.doctor.name,
date: appt.session.scheduledAt,
// time: appt.session.startTime // Session might handle time differently, but scheduledAt is good enough for now
});
if (success) sentCount++;
} else {
console.warn(`[ReminderService] No email for appointment ${appt.id}`);
}
const success = await NotificationService.sendAppointmentReminder(appt.patientEmail, {
...appt,
doctorName: appt.doctor.name,
date: appt.session.scheduledAt,
// time: appt.session.startTime // Session might handle time differently, but scheduledAt is good enough for now
});
if (success) sentCount++;

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
} catch (error: any) {
console.error(`[NotificationService] Email failed:`, error);
status = "FAILED";
failureReason = error.message;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error type 'any' loses type safety. Using 'any' for the error parameter defeats TypeScript's type checking. Consider using 'unknown' and type narrowing, or a more specific error type.

Suggested change
} catch (error: any) {
console.error(`[NotificationService] Email failed:`, error);
status = "FAILED";
failureReason = error.message;
} catch (error: unknown) {
console.error(`[NotificationService] Email failed:`, error);
status = "FAILED";
failureReason = error instanceof Error ? error.message : String(error);

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +16
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
tomorrowStart.setHours(0, 0, 0, 0);

const tomorrowEnd = new Date(tomorrowStart);
tomorrowEnd.setHours(23, 59, 59, 999);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date range calculation for "tomorrow" doesn't account for timezones. The code uses the server's local timezone, which may not match the user's timezone. If the server is in a different timezone than the appointments, reminders might be sent at the wrong time or not at all. Consider using UTC consistently or storing timezone information with appointments.

Suggested change
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
tomorrowStart.setHours(0, 0, 0, 0);
const tomorrowEnd = new Date(tomorrowStart);
tomorrowEnd.setHours(23, 59, 59, 999);
// Calculate "tomorrow" boundaries in UTC to avoid server-local timezone issues
tomorrowStart.setUTCDate(tomorrowStart.getUTCDate() + 1);
tomorrowStart.setUTCHours(0, 0, 0, 0);
const tomorrowEnd = new Date(tomorrowStart);
tomorrowEnd.setUTCHours(23, 59, 59, 999);

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +64
const success = await NotificationService.sendAppointmentReminder(appt.patientEmail, {
...appt,
doctorName: appt.doctor.name,
date: appt.session.scheduledAt,
// time: appt.session.startTime // Session might handle time differently, but scheduledAt is good enough for now
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the confirmation email, spreading the entire appointment object at line 60 exposes potentially sensitive patient data (medical history, NIC, insurance info, etc.) in the metadata. The reminder template only uses patientName, doctorName, and date. Consider passing only the necessary fields to avoid logging sensitive information.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +90
await NotificationService.sendAppointmentConfirmation(patientEmail, {
...result.appointment,
doctorName: doctor?.name || "the doctor",
date: slot.date, // Assuming slot has the date
time: slot.time // Assuming slot has the time
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The appointment object spread at line 86 includes many fields from the database model (appointmentNumber, patientEmail, consultationFee, etc.) that are not used in the email template (lines 86-92 in notification.service.ts). The template only uses patientName, doctorName, date, and appointmentNumber. Spreading the entire appointment object exposes potentially sensitive data (like NIC, medical history, insurance info) in the metadata logged to the database. Consider only passing the specific fields needed for the template.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +92
const html = `
<h1>Appointment Confirmed</h1>
<p>Dear ${appointmentDetails.patientName},</p>
<p>Your appointment with Dr. ${appointmentDetails.doctorName} has been confirmed.</p>
<p><strong>Date:</strong> ${new Date(appointmentDetails.date).toLocaleString()}</p>
<p><strong>Reference:</strong> ${appointmentDetails.appointmentNumber}</p>
`;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTML template is missing a closing tag for the paragraph on line 91. This could cause rendering issues in some email clients.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +90
await NotificationService.sendAppointmentConfirmation(patientEmail, {
...result.appointment,
doctorName: doctor?.name || "the doctor",
date: slot.date, // Assuming slot has the date
time: slot.time // Assuming slot has the time
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notification is sent outside the database transaction that creates the appointment. If the email sending fails or the database logging fails, the appointment will still be created but no confirmation will be sent. Consider either: 1) Moving the notification inside the transaction with proper error handling, 2) Using a message queue for reliable delivery, or 3) At minimum, logging failures prominently so they can be retried.

Suggested change
await NotificationService.sendAppointmentConfirmation(patientEmail, {
...result.appointment,
doctorName: doctor?.name || "the doctor",
date: slot.date, // Assuming slot has the date
time: slot.time // Assuming slot has the time
});
try {
await NotificationService.sendAppointmentConfirmation(patientEmail, {
...result.appointment,
doctorName: doctor?.name || "the doctor",
date: slot.date, // Assuming slot has the date
time: slot.time // Assuming slot has the time
});
} catch (error) {
// Prominently log failures so they can be monitored and retried
console.error("Failed to send appointment confirmation email", {
error: error instanceof Error ? error.message : error,
appointmentId: (result.appointment as any).id,
patientEmail,
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +52
let sentCount = 0;

for (const appt of appointments) {
// 3. Check if already reminded
const existingLog = await prisma.notification_logs.findFirst({
where: {
appointmentId: appt.id,
type: "REMINDER",
status: "SENT",
},
});

if (existingLog) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N+1 query problem: For each appointment, the code makes a separate database query to check if a reminder was already sent (line 44). With many appointments, this could result in hundreds of queries. Consider fetching all existing reminder logs in a single query before the loop and checking them in memory.

Suggested change
let sentCount = 0;
for (const appt of appointments) {
// 3. Check if already reminded
const existingLog = await prisma.notification_logs.findFirst({
where: {
appointmentId: appt.id,
type: "REMINDER",
status: "SENT",
},
});
if (existingLog) {
const appointmentIds = appointments.map((appt) => appt.id);
const existingLogs = appointmentIds.length
? await prisma.notification_logs.findMany({
where: {
appointmentId: { in: appointmentIds },
type: "REMINDER",
status: "SENT",
},
})
: [];
const remindedAppointmentIds = new Set(existingLogs.map((log) => log.appointmentId));
let sentCount = 0;
for (const appt of appointments) {
// 3. Check if already reminded
if (remindedAppointmentIds.has(appt.id)) {

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +15
const transporter = nodemailer.createTransport({
service: "gmail", // or use host/port
auth: {
user: process.env.EMAIL_USER || "test@example.com",
pass: process.env.EMAIL_PASS || "password",
},
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transporter is configured at module load time with potentially missing environment variables. If EMAIL_USER or EMAIL_PASS are not set, the transporter will be created with invalid credentials but won't fail until the first email is sent. Consider validating required environment variables at startup or using lazy initialization to provide clearer error messages.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants