Add Notification & Reminder Services + Verification Script#3
Add Notification & Reminder Services + Verification Script#3PraveenYasas wants to merge 1 commit intomainfrom
Conversation
- 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>
|
👋 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 New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
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.
| // 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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}`); | ||
| } |
There was a problem hiding this comment.
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.
| 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++; |
| } catch (error: any) { | ||
| console.error(`[NotificationService] Email failed:`, error); | ||
| status = "FAILED"; | ||
| failureReason = error.message; |
There was a problem hiding this comment.
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.
| } 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); |
| tomorrowStart.setDate(tomorrowStart.getDate() + 1); | ||
| tomorrowStart.setHours(0, 0, 0, 0); | ||
|
|
||
| const tomorrowEnd = new Date(tomorrowStart); | ||
| tomorrowEnd.setHours(23, 59, 59, 999); |
There was a problem hiding this comment.
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.
| 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); |
| 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 | ||
| }); |
There was a problem hiding this comment.
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.
| 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 | ||
| }); |
There was a problem hiding this comment.
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.
| 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> | ||
| `; |
There was a problem hiding this comment.
The HTML template is missing a closing tag for the paragraph on line 91. This could cause rendering issues in some email clients.
| 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 | ||
| }); |
There was a problem hiding this comment.
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.
| 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, | |
| }); | |
| } |
| 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) { |
There was a problem hiding this comment.
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.
| 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)) { |
| 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", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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.
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