Skip to content

Comments

Feat/implement scheduled email reminders#65

Open
laryhills wants to merge 6 commits intoAURORALAOrg:mainfrom
laryhills:feat/implement-scheduled-email-reminders
Open

Feat/implement scheduled email reminders#65
laryhills wants to merge 6 commits intoAURORALAOrg:mainfrom
laryhills:feat/implement-scheduled-email-reminders

Conversation

@laryhills
Copy link

@laryhills laryhills commented Jul 30, 2025

Pull Request for STARKLA - Close Issue

Pull Request Information

This PR Implements email notification reminders for inactive users to try to re-engage them.

Add here some information

🗒️ Summary of Changes

  • Activity Tracking via lastLoginAt and lastActivityAt fields
  • Scheduled Jobs to check for inactive users
  • Emails reminders sent to inactive users with retry
  • Configurable Job scheduling and logging
  • Updated .env.exmaple

⚒️ Testing

Evidence After Solution

Screenshot 2025-07-30 at 23 39 14

Screenshot 2025-08-12 at 09 43 28

Screenshot 2025-08-12 at 09 43 47

📂 Related Issue

This pull request will **close #49 ** upon merging.


🎉 Thank you for reviewing this PR! 🎉

Summary by CodeRabbit

  • New Features
    • Automated inactivity reminder emails with configurable schedule and retry handling.
    • Background job scheduler with health/status and graceful startup/shutdown.
  • Chores
    • Added scheduling configuration and dependencies; introduced a seed script.
    • Switched email transport to Gmail SMTP with connection timeouts for more reliable delivery.
  • Documentation
    • Updated environment example with reminder settings (enable flag, cron schedule, max retries, retry interval).

@coderabbitai
Copy link

coderabbitai bot commented Jul 30, 2025

Walkthrough

Introduces inactivity reminder feature: adds user timestamp fields, environment settings, cron-based scheduler, notification and email sending services, a reminder job, and integrates job initialization/shutdown into server. Auth controller and middleware now record login/activity. SMTP transporter settings updated.

Changes

Cohort / File(s) Summary
Environment & Config
backend/.env.example, backend/src/core/config/settings.ts, backend/package.json
Adds reminder env vars (enable, cron, retries, retry interval). Validates/maps to server settings. Adds node-cron and types; new seed script.
Database Schema
backend/prisma/schema.prisma
Adds optional DateTime fields to User: lastLoginAt, lastActivityAt, lastReminderSent.
Auth & Activity Tracking
backend/src/controllers/auth.controller.ts, backend/src/middlewares/authentication.ts
On login, updates lastLoginAt (and activity) with error-tolerant logging. Middleware updates lastActivityAt upon auth.
Job Scheduling & Lifecycle
backend/src/services/jobScheduler.service.ts, backend/src/server.ts
Introduces JobSchedulerService using node-cron; initializes on startup; graceful shutdown stops jobs; manual trigger support and status reporting.
Reminder Job
backend/src/jobs/userReminder.job.ts
Adds UserReminderJob to compute candidates, send emails with retries, update lastReminderSent, and report health/results.
Notification & User Services
backend/src/services/notification.service.ts, backend/src/services/user.service.ts
NotificationService finds reminder candidates and stats; UserService adds methods for activity timestamps, inactive-user queries, and batch reminder updates.
Email Utilities
backend/src/utils/service/emailNotifier.ts, backend/src/utils/service/nodeMailer.ts
EmailNotifier gains reminder templates and bulk send with retry. NodeMailer switches SMTP host to smtp.gmail.com and adds timeouts.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Timer as Cron (node-cron)
  participant Server
  participant Scheduler as JobSchedulerService
  participant Job as UserReminderJob
  participant Notify as NotificationService
  participant UserSvc as UserService
  participant Mail as EmailNotifier

  Server->>Scheduler: initializeJobs()
  Scheduler->>Timer: schedule(CRON_SCHEDULE, job)
  note over Scheduler,Timer: Scheduled in UTC

  Timer-->>Scheduler: tick at schedule
  Scheduler->>Job: checkInactiveUsers(maxRetries)
  Job->>Notify: getReminderStats()
  Notify->>UserSvc: countInactiveUsers(>=7d)
  UserSvc-->>Notify: totalInactive
  Notify-->>Job: stats

  Job->>Notify: findReminderCandidates()
  Notify->>UserSvc: findInactiveUsers(>=7d)
  UserSvc-->>Notify: inactive users
  Notify-->>Job: candidates (with types)

  Job->>Mail: sendReminderEmails(reminders[], retries)
  alt partial failures
    Mail-->>Job: results (success/failed/errors)
  else all success
    Mail-->>Job: results (all success)
  end

  Job->>UserSvc: updateReminderSentBatch(userIds)
  UserSvc-->>Job: ok
  Job-->>Scheduler: JobResult (counts, errors, duration)
  Scheduler-->>Server: log outcome

  rect rgba(230,245,255,0.4)
  note over Server,UserSvc: Separate flow on requests
  end
Loading
sequenceDiagram
  autonumber
  participant Client
  participant Auth as Auth Controller
  participant MW as Auth Middleware
  participant UserSvc as UserService

  Client->>Auth: POST /login
  Auth->>UserSvc: updateLastLogin(userId)
  UserSvc-->>Auth: ok
  Auth-->>Client: JWT

  Client->>MW: Authenticated request
  MW->>UserSvc: updateLastActivity(userId)
  UserSvc-->>MW: ok
  MW-->>Client: proceed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Extend User Model; track login/activity (#49)
Create Notification Service; reminder logic & templates (#49)
Install CRON; configure scheduling at startup (#49)
Create Scheduled Job; different reminder types (#49)
Configurable thresholds & reminder intervals (#49) Thresholds and intervals appear hardcoded; no env-based configuration for 7/14/30/60-day or per-type intervals.
Job Logging and Error Handling with retries (#49)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Change SMTP host to smtp.gmail.com and add timeouts (backend/src/utils/service/nodeMailer.ts) SMTP provider change is unrelated to implementing reminder scheduling and could affect all email sending; not specified in the issue.

Suggested reviewers

  • Gerson2102

Poem

Thump-thump goes the scheduler’s beat at two,
I twitch my ears—reminders hopping through.
Seven, fourteen—carrots on a string,
Thirty, sixty—gentle nudges wing.
Cron bells chime, the inboxes bloom—
Come back and play! I’ve saved you room.
— your busy bunny 🥕✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 80c6ba9 and 5440caf.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • backend/package.json (4 hunks)
  • backend/prisma/schema.prisma (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/prisma/schema.prisma
  • backend/package.json
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (5)
backend/.env.example (2)

34-34: Remove extra blank line.

The linter detected an unnecessary blank line here.

-

35-41: Fix formatting issues and key ordering.

The reminder configuration values are appropriate, but there are formatting issues that should be addressed based on linter feedback.

 # ========================
 # User Reminder CronJob Settings
 # ========================
-REMINDER_ENABLED=true
 CRON_SCHEDULE="0 2 * * *"
+REMINDER_ENABLED=true
 REMINDER_MAX_RETRIES=2
 REMINDER_RETRY_INTERVAL_HOURS=24
+

The CRON schedule "0 2 * * *" (daily at 2 AM UTC) is well-chosen for off-peak execution as specified in the objectives.

backend/src/middlewares/authentication.ts (1)

28-29: Consider performance impact of synchronous activity tracking.

Adding a database write operation to every authenticated request could impact response times and increase database load. Consider making this operation asynchronous or implementing a batching mechanism for high-traffic applications.

Consider this alternative approach for better performance:

-      // Track user activity
-      await UserService.updateLastActivity(user.id);
+      // Track user activity asynchronously
+      UserService.updateLastActivity(user.id).catch(err => 
+        console.error("Failed to track user activity:", err)
+      );

Or implement a batching system to reduce database writes.

backend/src/services/jobScheduler.service.ts (1)

81-87: Improve job status checking reliability

The string comparison job.getStatus() === "scheduled" is fragile and depends on internal implementation details of node-cron.

Consider using the built-in method or storing state locally:

       status.push({
         name,
-        isRunning: job.getStatus() === "scheduled",
+        isRunning: job.running !== false, // node-cron sets running to false when stopped
         schedule: name === "userReminder" ? settings.reminders.cronSchedule : "unknown",
       });
backend/src/jobs/userReminder.job.ts (1)

141-144: Health status should reflect actual job execution history

The method always returns the current time as lastRun, which doesn't reflect when the job actually last executed.

Track actual execution times:

+  private static lastExecutionTime?: Date;
+
   public static async checkInactiveUsers(maxRetries: number = 2): Promise<JobResult> {
     const startTime = Date.now();
+    this.lastExecutionTime = new Date();
     console.log("🔄 Starting user reminder job...");

   // In getHealthStatus:
       return {
         isHealthy: true,
-        lastRun: new Date(),
+        lastRun: this.lastExecutionTime,
         nextRun: "Based on CRON schedule",
       };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3935e89 and f573a0d.

⛔ Files ignored due to path filters (2)
  • backend/logs/error.log is excluded by !**/*.log
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • backend/.env.example (1 hunks)
  • backend/package.json (4 hunks)
  • backend/prisma/migrations/migration_lock.toml (1 hunks)
  • backend/prisma/schema.prisma (1 hunks)
  • backend/src/controllers/auth.controller.ts (1 hunks)
  • backend/src/core/config/settings.ts (2 hunks)
  • backend/src/db.ts (1 hunks)
  • backend/src/jobs/userReminder.job.ts (1 hunks)
  • backend/src/middlewares/authentication.ts (1 hunks)
  • backend/src/server.ts (1 hunks)
  • backend/src/services/jobScheduler.service.ts (1 hunks)
  • backend/src/services/notification.service.ts (1 hunks)
  • backend/src/services/user.service.ts (1 hunks)
  • backend/src/utils/service/emailNotifier.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
backend/src/server.ts (1)

Learnt from: PoulavBhowmick03
PR: #58
File: backend/src/jobs/dailyStreak.ts:2-2
Timestamp: 2025-07-11T05:46:56.577Z
Learning: In the AURORALAOrg/aurora-backend project, Prisma is used by importing { PrismaClient } from "@prisma/client" and then instantiating it with const prisma = new PrismaClient(). This pattern is used consistently across service files like user.service.ts and question.service.ts.

backend/src/services/user.service.ts (1)

Learnt from: PoulavBhowmick03
PR: #58
File: backend/src/jobs/dailyStreak.ts:2-2
Timestamp: 2025-07-11T05:46:56.577Z
Learning: In the AURORALAOrg/aurora-backend project, Prisma is used by importing { PrismaClient } from "@prisma/client" and then instantiating it with const prisma = new PrismaClient(). This pattern is used consistently across service files like user.service.ts and question.service.ts.

🧬 Code Graph Analysis (1)
backend/src/services/user.service.ts (1)
backend/src/core/api/ApiError.ts (1)
  • InternalError (71-75)
🪛 dotenv-linter (3.3.0)
backend/.env.example

[warning] 34-34: [ExtraBlankLine] Extra blank line detected


[warning] 39-39: [UnorderedKey] The CRON_SCHEDULE key should go before the REMINDER_ENABLED key


[warning] 41-41: [EndingBlankLine] No blank line at the end of the file

🔇 Additional comments (11)
backend/src/db.ts (1)

23-23: LGTM! Minor grammatical improvement.

The change from "error" to "errors" is a small but appropriate improvement since the error parameter could contain multiple error details.

backend/prisma/migrations/migration_lock.toml (1)

4-4: LGTM! Proper file ending.

Adding a newline at the end of the file follows best practices and prevents potential issues with tools that expect files to end with newlines.

backend/prisma/schema.prisma (1)

39-41: LGTM! Well-designed schema additions for user activity tracking.

The three new optional DateTime fields are perfectly aligned with the PR objectives:

  • lastLoginAt - tracks user login times
  • lastActivityAt - tracks general user activity
  • lastReminderSent - prevents duplicate reminder emails

The fields are appropriately optional and follow consistent naming conventions.

backend/package.json (2)

14-14: LGTM! Seed script addition is appropriate.

The new seed script supports database initialization for the user activity tracking fields.


23-23: node-cron dependency versions verified

  • Confirmed backend/package.json declares
    • "node-cron": "^4.2.1" (latest stable: 4.2.1)
    • "@types/node-cron": "^3.0.11" (latest stable: 3.0.11)
  • No known security advisories against these releases

Dependencies are up-to-date and safe to merge.

backend/src/core/config/settings.ts (2)

25-36: LGTM! Well-structured reminder configuration.

The reminder configuration follows good practices with:

  • Sensible defaults (2 AM daily schedule, 2 retries, 24-hour intervals)
  • Proper Joi validation
  • Clear descriptions
  • Feature toggle with REMINDER_ENABLED

59-64: LGTM! Configuration mapping is consistent.

The reminders section properly maps environment variables to the settings object with consistent naming.

backend/src/services/user.service.ts (4)

61-74: LGTM! Proper login tracking implementation.

Updates both login and activity timestamps appropriately with proper error handling.


76-86: LGTM! Smart error handling for activity tracking.

The decision not to throw errors for activity tracking is wise - it prevents authentication failures due to non-critical tracking issues.


88-125: LGTM! Well-designed inactive user query.

The query logic correctly handles both scenarios:

  • Users with lastActivityAt older than cutoff
  • Users with null lastActivityAt but createdAt older than cutoff

The field selection is appropriate for reminder processing, and error handling is consistent.


127-137: LGTM! Standard reminder tracking update.

Proper implementation for tracking when reminders were sent with consistent error handling.

@Gerson2102
Copy link
Contributor

Hello sir @laryhills. Thanks for your contribution. Please address every coderabbit comment/feedback please.

@laryhills
Copy link
Author

@Gerson2102

I have resolved the comments

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
backend/src/services/notification.service.ts (1)

75-77: Handle potential edge case with Infinity.

When user.lastReminderSent is null, daysSinceLastReminder is set to Infinity. While this works mathematically, it could be more explicit and readable.

Consider using a more explicit approach:

-        const daysSinceLastReminder = user.lastReminderSent
-          ? this.daysBetween(user.lastReminderSent, now)
-          : Infinity;
+        const daysSinceLastReminder = user.lastReminderSent
+          ? this.daysBetween(user.lastReminderSent, now)
+          : 999; // Large number to indicate no previous reminder
backend/src/services/user.service.ts (1)

169-183: Optimize batch update implementation.

The current batch update uses individual UPDATE statements within a transaction. For better performance with large user sets, consider using a single updateMany operation.

  public static async updateReminderSentBatch(userIds: string[]): Promise<void> {
    try {
-      await prisma.$transaction(
-        userIds.map(userId =>
-          prisma.user.update({
-            where: { id: userId },
-            data: { lastReminderSent: new Date() }
-          })
-        )
-      );
+      await prisma.user.updateMany({
+        where: {
+          id: {
+            in: userIds
+          }
+        },
+        data: { lastReminderSent: new Date() }
+      });
    } catch (error) {
      console.error("Failed to batch update reminder sent:", error);
      throw new InternalError("Failed to batch update reminder sent");
    }
  }

This approach is more efficient for large batches and simpler to understand.

backend/src/utils/service/emailNotifier.ts (1)

112-112: Consider making retry parameters configurable.

While the implementation is correct, hardcoded default values for maxRetries and retryDelayMs should ideally come from configuration.

Consider reading these values from environment variables or configuration:

-  public static async sendReminderEmails(reminders: ReminderEmailData[], maxRetries: number = 2, retryDelayMs: number = 5000) {
+  public static async sendReminderEmails(
+    reminders: ReminderEmailData[], 
+    maxRetries: number = parseInt(process.env.EMAIL_MAX_RETRIES || '2'), 
+    retryDelayMs: number = parseInt(process.env.EMAIL_RETRY_DELAY_MS || '5000')
+  ) {

This would allow for environment-specific tuning without code changes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f573a0d and cd330b6.

📒 Files selected for processing (7)
  • backend/src/controllers/auth.controller.ts (1 hunks)
  • backend/src/jobs/userReminder.job.ts (1 hunks)
  • backend/src/server.ts (1 hunks)
  • backend/src/services/jobScheduler.service.ts (1 hunks)
  • backend/src/services/notification.service.ts (1 hunks)
  • backend/src/services/user.service.ts (1 hunks)
  • backend/src/utils/service/emailNotifier.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/src/server.ts
  • backend/src/controllers/auth.controller.ts
  • backend/src/services/jobScheduler.service.ts
  • backend/src/jobs/userReminder.job.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: in the auroralaorg/aurora-backend project, prisma is used by importing { prismaclient } from "@prism...
Learnt from: PoulavBhowmick03
PR: AURORALAOrg/aurora-backend#58
File: backend/src/jobs/dailyStreak.ts:2-2
Timestamp: 2025-07-11T05:46:56.577Z
Learning: In the AURORALAOrg/aurora-backend project, Prisma is used by importing { PrismaClient } from "@prisma/client" and then instantiating it with `const prisma = new PrismaClient()`. This pattern is used consistently across service files like user.service.ts and question.service.ts.

Applied to files:

  • backend/src/services/user.service.ts
🔇 Additional comments (9)
backend/src/services/notification.service.ts (3)

23-26: Good fix - Past issue addressed correctly.

The daysBetween calculation now uses Math.floor instead of Math.ceil, ensuring only complete days are counted. This addresses the previous review concern about partial days being rounded up incorrectly.


107-109: Excellent optimization - Redundant query eliminated.

The redundant database query has been properly addressed by using UserService.countInactiveUsers(7) instead of calling findInactiveUsers twice. This eliminates duplicate database queries while maintaining the same functionality.


49-60: Review the reminder interval logic for consistency.

The shouldSendReminder method defines minimum intervals that may not align with business expectations:

  • 7-day reminders can be sent every 7 days
  • 14-day reminders can be sent every 7 days
  • 30-day reminders can be sent every 14 days
  • 60-day reminders can be sent every 30 days

This means a user could receive a 14-day reminder, then another 14-day reminder 7 days later (at 21 days of inactivity), which seems excessive.

Consider if these intervals align with your business requirements. The current logic allows:

  • A user inactive for 14 days gets a 14-day reminder
  • 7 days later (21 days inactive), they get another 14-day reminder
  • 9 days later (30 days inactive), they get a 30-day reminder

Would it be more appropriate to ensure each reminder type is sent only once per inactivity period?

backend/src/services/user.service.ts (3)

61-74: LGTM! Well-implemented login tracking.

The updateLastLogin method correctly updates both lastLoginAt and lastActivityAt timestamps, which makes sense since login is a form of activity. Error handling is appropriate with logging and throwing InternalError.


76-86: Good error handling strategy for activity tracking.

The updateLastActivity method wisely doesn't throw errors to avoid breaking user requests if activity tracking fails. This is a good design decision since activity tracking is supplementary functionality.


88-125: Verify inactive user query logic

The query correctly targets inactive users by combining:

  • lastActivityAt ≤ cutoffDate
  • OR lastActivityAt is null AND createdAt ≤ cutoffDate
  • Only ACTIVE users with isEmailVerified: true are included

No other similar queries were found in the codebase, so please manually confirm the following edge cases:

  • Cutoff boundary: users with timestamps exactly equal to cutoffDate
  • Users created within the cutoff window who never had activity (should be excluded)
  • Time zone consistency between application server and database
  • Behavior when lastActivityAt or createdAt is invalid or missing
backend/src/utils/service/emailNotifier.ts (3)

11-23: Excellent security improvement - URL validation implemented.

The getValidatedAppUrl method properly addresses the previous security concern by validating the environment variable and ensuring it's a valid URL format before use in email templates.


33-97: Well-structured email content generation.

The getReminderEmailContent method provides appropriate messaging for different reminder types with escalating urgency. The email content is professional and engaging.


112-152: Improved retry implementation with proper delay.

The retry logic has been significantly improved from the previous version:

  • Proper delay between retries using setTimeout
  • Tracking of successful emails in successfulEmails array
  • Changed parameter from retryIntervalHours to retryDelayMs for more precise control

This addresses the previous review concerns effectively.

@Gerson2102
Copy link
Contributor

Hello @laryhills. Thanks for addressing the coderabbit suggestions. Could you please explain me how to check if everything that you did is working successfully?

@laryhills
Copy link
Author

@Gerson2102
You could make the CRON_SCHEDULE timing to run every 2 mins 2 * * * *
And you would see it trigger the cronjob.

@Gerson2102
Copy link
Contributor

Hello @laryhills. I just run npm run dev on the backend side and the first thing that I see is the following errors:

/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
src/services/user.service.ts:66:11 - error TS2353: Object literal may only specify known properties, and 'lastLoginAt' does not exist in type '(Without<UserUpdateInput, UserUncheckedUpdateInput> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput)'.

66           lastLoginAt: new Date(),
             ~~~~~~~~~~~

  node_modules/.prisma/client/index.d.ts:2466:5
    2466     data: XOR<UserUpdateInput, UserUncheckedUpdateInput>
             ~~~~
    The expected type comes from property 'data' which is declared here on type '{ select?: UserSelect<DefaultArgs> | null | undefined; omit?: UserOmit<DefaultArgs> | null | undefined; include?: UserInclude<DefaultArgs> | null | undefined; data: (Without<...> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput); where: UserWhereUniqueInput; }'
src/services/user.service.ts:80:17 - error TS2353: Object literal may only specify known properties, and 'lastActivityAt' does not exist in type '(Without<UserUpdateInput, UserUncheckedUpdateInput> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput)'.

80         data: { lastActivityAt: new Date() },
                   ~~~~~~~~~~~~~~

  node_modules/.prisma/client/index.d.ts:2466:5
    2466     data: XOR<UserUpdateInput, UserUncheckedUpdateInput>
             ~~~~
    The expected type comes from property 'data' which is declared here on type '{ select?: UserSelect<DefaultArgs> | null | undefined; omit?: UserOmit<DefaultArgs> | null | undefined; include?: UserInclude<DefaultArgs> | null | undefined; data: (Without<...> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput); where: UserWhereUniqueInput; }'
src/services/user.service.ts:99:15 - error TS2353: Object literal may only specify known properties, and 'lastActivityAt' does not exist in type 'UserWhereInput'.

99               lastActivityAt: {
                 ~~~~~~~~~~~~~~
src/services/user.service.ts:104:15 - error TS2353: Object literal may only specify known properties, and 'lastActivityAt' does not exist in type 'UserWhereInput'.

104               lastActivityAt: null,
                  ~~~~~~~~~~~~~~
src/services/user.service.ts:116:11 - error TS2353: Object literal may only specify known properties, and 'lastActivityAt' does not exist in type 'UserSelect<DefaultArgs>'.

116           lastActivityAt: true,
              ~~~~~~~~~~~~~~
src/services/user.service.ts:138:15 - error TS2353: Object literal may only specify known properties, and 'lastActivityAt' does not exist in type 'UserWhereInput'.

138               lastActivityAt: {
                  ~~~~~~~~~~~~~~
src/services/user.service.ts:143:15 - error TS2353: Object literal may only specify known properties, and 'lastActivityAt' does not exist in type 'UserWhereInput'.

143               lastActivityAt: null,
                  ~~~~~~~~~~~~~~
src/services/user.service.ts:161:17 - error TS2353: Object literal may only specify known properties, and 'lastReminderSent' does not exist in type '(Without<UserUpdateInput, UserUncheckedUpdateInput> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput)'.

161         data: { lastReminderSent: new Date() },
                    ~~~~~~~~~~~~~~~~

  node_modules/.prisma/client/index.d.ts:2466:5
    2466     data: XOR<UserUpdateInput, UserUncheckedUpdateInput>
             ~~~~
    The expected type comes from property 'data' which is declared here on type '{ select?: UserSelect<DefaultArgs> | null | undefined; omit?: UserOmit<DefaultArgs> | null | undefined; include?: UserInclude<DefaultArgs> | null | undefined; data: (Without<...> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput); where: UserWhereUniqueInput; }'
src/services/user.service.ts:175:21 - error TS2353: Object literal may only specify known properties, and 'lastReminderSent' does not exist in type '(Without<UserUpdateInput, UserUncheckedUpdateInput> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput)'.

175             data: { lastReminderSent: new Date() }
                        ~~~~~~~~~~~~~~~~

  node_modules/.prisma/client/index.d.ts:2466:5
    2466     data: XOR<UserUpdateInput, UserUncheckedUpdateInput>
             ~~~~
    The expected type comes from property 'data' which is declared here on type '{ select?: UserSelect<DefaultArgs> | null | undefined; omit?: UserOmit<DefaultArgs> | null | undefined; include?: UserInclude<DefaultArgs> | null | undefined; data: (Without<...> & UserUncheckedUpdateInput) | (Without<...> & UserUpdateInput); where: UserWhereUniqueInput; }'

    at createTSError (/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:859:12)
    at reportTSError (/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:863:19)
    at getOutput (/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:1077:36)
    at Object.compile (/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:1433:41)
    at Module.m._compile (/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:1617:30)
    at node:internal/modules/cjs/loader:1699:10
    at Object.require.extensions.<computed> [as .ts] (/root/AURORA-Backend/backend/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1313:32)
    at Function._load (node:internal/modules/cjs/loader:1123:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14) {
  diagnosticCodes: [
    2353, 2353, 2353,
    2353, 2353, 2353,
    2353, 2353, 2353
  ]
}

@Gerson2102
Copy link
Contributor

Could you help me on telling me the list of commands that I should run to have everything setted up to see if everything is working coorectly?

I ran npm run dev but should I do something before or what?

@laryhills
Copy link
Author

laryhills commented Aug 8, 2025

@Gerson2102
Did you run Prisma migrate?

@laryhills
Copy link
Author

@Josue19-08

@ChrisFernandezVivas
Copy link
Contributor

hey @laryhills all goood
Captura de pantalla 2025-08-18 a las 11 58 26 p  m

this is my .env I forgot something?
Captura de pantalla 2025-08-18 a las 11 59 27 p  m
Also I created a user in the db , but I don't know , why is not working?

i migrated the db

@ChrisFernandezVivas
Copy link
Contributor

for better communication , feel free to contact me by telegram group Chris742001, thanks

@laryhills
Copy link
Author

@ChrisFernandezVivas
Should this also work for inactive users, as the user you created is inactive?

@Gerson2102
Copy link
Contributor

Hello @laryhills please rebase sir.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
backend/src/middlewares/authentication.ts (2)

29-31: Make activity tracking non-blocking and throttle updates to reduce DB load and request latency

Updating lastActivityAt on every authorized request as an awaited call will amplify writes and add avoidable latency. Fire-and-forget and only update if stale (e.g., older than 5 minutes). Also, log failures at warn level without failing the request.

Apply within this hunk:

-      // Track user activity
-      await UserService.updateLastActivity(user.id);
+      // Track user activity (non-blocking + throttle to once per 5 minutes)
+      const last = user.lastActivityAt ? new Date(user.lastActivityAt).getTime() : 0;
+      const now = Date.now();
+      if (now - last > 5 * 60 * 1000) {
+        void UserService.updateLastActivity(user.id).catch((err) =>
+          logger.warn("lastActivityAt update failed", {
+            userId: user.id,
+            error: err instanceof Error ? err.message : String(err),
+          })
+        );
+      }

Additionally (outside this hunk), consider typing req.user and aligning locals naming:

// at top of file
import type { User } from "@prisma/client";

// below: interface
interface AuthenticatedRequest extends Request {
  user?: User;
}

// where you set locals
res.locals.user = user; // prefer `user` over `account` for consistency

32-34: Unify locals naming for clarity

res.locals.account mixes terminology with req.user. Prefer res.locals.user for consistency across the codebase and templates. This reduces mental overhead in handlers/views.

backend/src/services/user.service.ts (4)

80-90: Don’t return the updated user and avoid console.error; keep it lightweight

Since callers shouldn’t depend on this value (and you already swallow errors), return void to make intent explicit and cut payload. Replace console.error with central logger for uniform observability.

-  public static async updateLastActivity(userId: string) {
+  public static async updateLastActivity(userId: string): Promise<void> {
     try {
-      return await prisma.user.update({
+      await prisma.user.update({
         where: { id: userId },
         data: { lastActivityAt: new Date() },
       });
     } catch (error) {
-      console.error("Failed to update last activity:", error);
+      // logger.warn({ err: error, userId }, "Failed to update last activity");
+      console.warn("Failed to update last activity:", error);
       // Don't throw error for activity tracking to avoid breaking requests
     }
   }

Note: With this change, ensure callers do not rely on a return value (they shouldn’t).


161-171: LGTM; consider reusing a single Date and central logger

The method is correct. Minor polish: use a single now and central logger to match the rest of the service.

-  public static async updateReminderSent(userId: string) {
+  public static async updateReminderSent(userId: string) {
     try {
-      return await prisma.user.update({
+      const now = new Date();
+      return await prisma.user.update({
         where: { id: userId },
-        data: { lastReminderSent: new Date() },
+        data: { lastReminderSent: now },
       });
     } catch (error) {
-      console.error("Failed to update reminder sent:", error);
+      // logger.error({ err: error, userId }, "Failed to update reminder sent");
+      console.error("Failed to update reminder sent:", error);
       throw new InternalError("Failed to update reminder sent");
     }
   }

173-187: Batch update: use updateMany with a single timestamp and guard empty input

A single updateMany is more efficient than N updates in a transaction. Also avoid a no-op transaction on empty arrays and reuse a single timestamp.

-  public static async updateReminderSentBatch(userIds: string[]): Promise<void> {
+  public static async updateReminderSentBatch(userIds: string[]): Promise<void> {
     try {
-      await prisma.$transaction(
-        userIds.map(userId =>
-          prisma.user.update({
-            where: { id: userId },
-            data: { lastReminderSent: new Date() }
-          })
-        )
-      );
+      if (!userIds.length) return;
+      const now = new Date();
+      await prisma.user.updateMany({
+        where: { id: { in: userIds } },
+        data: { lastReminderSent: now },
+      });
     } catch (error) {
       console.error("Failed to batch update reminder sent:", error);
       throw new InternalError("Failed to batch update reminder sent");
     }
   }

65-78: All Prisma schema fields verified

The Prisma schema at backend/prisma/schema.prisma already includes lastLoginAt, lastActivityAt, and lastReminderSent, so you won’t run into missing-field errors at compile time or during migration.

Optional refactoring suggestions for backend/src/services/user.service.ts:

  • Allocate the current timestamp once to ensure both fields use the exact same value.
  • Swap console.error for your central logger.error to standardize logging across services.

Suggested diff:

--- a/backend/src/services/user.service.ts
+++ b/backend/src/services/user.service.ts
@@ public static async updateLastLogin(userId: string) {
-      return await prisma.user.update({
+      const now = new Date();
+      return await prisma.user.update({
         where: { id: userId },
         data: {
-          lastLoginAt: new Date(),
-          lastActivityAt: new Date()
+          lastLoginAt: now,
+          lastActivityAt: now
         },
       });
@@ public static async updateLastLogin(userId: string) {
-      console.error("Failed to update last login:", error);
+      // TODO: replace console.error with your central logger import
+      // logger.error({ error, userId }, "Failed to update last login");
+      console.error("Failed to update last login:", error);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd330b6 and 7fa1aa9.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • backend/package.json (4 hunks)
  • backend/src/controllers/auth.controller.ts (1 hunks)
  • backend/src/middlewares/authentication.ts (1 hunks)
  • backend/src/services/user.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/src/controllers/auth.controller.ts
  • backend/package.json
🧰 Additional context used
🧬 Code graph analysis (1)
backend/src/services/user.service.ts (1)
backend/src/core/api/ApiError.ts (1)
  • InternalError (71-75)
🔇 Additional comments (1)
backend/src/services/user.service.ts (1)

92-129: Sanity checks complete – schema, cooldown logic, and cron wiring are all correct

All requested verifications passed:

  • Prisma schema includes lastLoginAt, lastActivityAt, and lastReminderSent on the User model (backend/prisma/schema.prisma lines 39–41).
  • NotificationService applies a cooldown via shouldSendReminder(daysSinceLastReminder, reminderType), with minimum intervals mapped correctly for each reminder type (backend/src/services/notification.service.ts lines 49–57).
  • The job scheduler reads CRON_SCHEDULE (default "0 2 * * *") from settings and registers it with cron.schedule(cronSchedule, …) in jobScheduler.service.ts (backend/src/services/jobScheduler.service.ts lines 35–38).

No further changes are needed here.

Comment on lines +92 to +129
public static async findInactiveUsers(days: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);

try {
return await prisma.user.findMany({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
OR: [
{
lastActivityAt: {
lte: cutoffDate,
},
},
{
lastActivityAt: null,
createdAt: {
lte: cutoffDate,
},
},
],
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
lastActivityAt: true,
lastReminderSent: true,
createdAt: true,
},
});
} catch (error) {
console.error("Failed to find inactive users:", error);
throw new InternalError("Failed to find inactive users");
}
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Filter out recently reminded users at the DB layer to prevent duplicate emails

Unless NotificationService already enforces a cooldown using lastReminderSent, push that predicate into the query so candidates are correct by construction. Default a 24-hour cooldown.

Apply this refactor in-place:

-  public static async findInactiveUsers(days: number) {
+  public static async findInactiveUsers(days: number, minHoursBetweenReminders = 24) {
     const cutoffDate = new Date();
     cutoffDate.setDate(cutoffDate.getDate() - days);
+    const remindCutoff = new Date(Date.now() - minHoursBetweenReminders * 60 * 60 * 1000);

     try {
       return await prisma.user.findMany({
         where: {
           status: Status.ACTIVE,
           isEmailVerified: true,
-          OR: [
-            {
-              lastActivityAt: {
-                lte: cutoffDate,
-              },
-            },
-            {
-              lastActivityAt: null,
-              createdAt: {
-                lte: cutoffDate,
-              },
-            },
-          ],
+          AND: [
+            {
+              OR: [
+                { lastActivityAt: { lte: cutoffDate } },
+                { lastActivityAt: null, createdAt: { lte: cutoffDate } },
+              ],
+            },
+            {
+              OR: [
+                { lastReminderSent: null },
+                { lastReminderSent: { lt: remindCutoff } },
+              ],
+            },
+          ],
         },
         select: {
           id: true,
           email: true,
           firstName: true,
           lastName: true,
           lastActivityAt: true,
           lastReminderSent: true,
           createdAt: true,
         },
       });

Operational advice: add DB indexes to keep this fast under load (Prisma schema indices example):

@@index([status, isEmailVerified, lastActivityAt])
@@index([status, isEmailVerified, createdAt])
@@index([status, isEmailVerified, lastReminderSent])

If NotificationService already applies cooldown logic, keep that and consider narrowing the DB result set anyway for efficiency.


🏁 Script executed:

#!/bin/bash
set -e

# 1. Locate NotificationService implementation
echo "🔍 Searching for NotificationService class..."
rg -n "class\s\+NotificationService" -g "backend/src/services/**/*.ts" || true

# 2. Search for lastReminderSent references in NotificationService
echo "🔍 Checking for lastReminderSent usage in NotificationService..."
rg -n "lastReminderSent" -g "backend/src/services/notification.service.ts" || true

# 3. Search for findInactiveUsers occurrences to see how it's consumed
echo "🔍 Finding usages of findInactiveUsers..."
rg -n "findInactiveUsers" -g "backend/src/services/**/*.ts" || true

# 4. Locate the Prisma schema file
echo "🔍 Locating schema.prisma..."
SCHEMA_FILE=$(find . -maxdepth 2 -type f -name "schema.prisma" | head -n1)
if [[ -z "$SCHEMA_FILE" ]]; then
  echo "schema.prisma not found in root or prisma/ directory"
  exit 1
else
  echo "Found schema: $SCHEMA_FILE"
fi

# 5. Extract the User model definition from the schema
echo "🔍 Extracting model User from $SCHEMA_FILE..."
sed -n '/model User {/,/}/p' "$SCHEMA_FILE" || true

Length of output: 1341


🏁 Script executed:

#!/bin/bash
set -e

echo "🔍 Showing NotificationService (lines 1-120)"
sed -n '1,120p' backend/src/services/notification.service.ts || true

echo -e "\n🔍 Searching for cooldown logic in NotificationService"
rg -nP --context 3 "daysSinceLastReminder" -g backend/src/services/notification.service.ts || true

echo -e "\n🔍 Finding all .prisma files"
find . -type f -name "*.prisma"

Length of output: 7388


Add DB-layer cooldown filtering to findInactiveUsers

Currently, findInactiveUsers returns all users inactive for the given period, and the NotificationService applies a reminder cooldown in-memory. Pushing at least a 24-hour cooldown into the Prisma query both prevents duplicate sends at the source and shrinks the result set sent over the wire.

• In backend/src/services/user.service.ts, update findInactiveUsers to accept an optional minHoursBetweenReminders (defaulting to 24), compute remindCutoff, and add an AND clause that only returns users whose lastReminderSent is null or older than remindCutoff.
• In backend/prisma/schema.prisma, add an index on [status, isEmailVerified, lastReminderSent] to keep the new filter performant.

--- a/backend/src/services/user.service.ts
+++ b/backend/src/services/user.service.ts
@@ public static async findInactiveUsers(days: number) {
-  public static async findInactiveUsers(days: number) {
+  public static async findInactiveUsers(
+    days: number,
+    minHoursBetweenReminders = 24,
+  ) {
     const cutoffDate = new Date();
     cutoffDate.setDate(cutoffDate.getDate() - days);
+    // Only re-remind users after at least minHoursBetweenReminders have passed
+    const remindCutoff = new Date(Date.now() - minHoursBetweenReminders * 60 * 60 * 1000);
 
     try {
       return await prisma.user.findMany({
         where: {
           status: Status.ACTIVE,
           isEmailVerified: true,
-          OR: [
-            { lastActivityAt: { lte: cutoffDate } },
-            { lastActivityAt: null, createdAt: { lte: cutoffDate } },
-          ],
+          AND: [
+            {
+              OR: [
+                { lastActivityAt: { lte: cutoffDate } },
+                { lastActivityAt: null,    createdAt: { lte: cutoffDate } },
+              ],
+            },
+            {
+              OR: [
+                { lastReminderSent: null },
+                { lastReminderSent: { lt: remindCutoff } },
+              ],
+            },
+          ],
         },
         select: {
           id: true,
           email: true,
           firstName: true,
           lastName: true,
           lastActivityAt: true,
           lastReminderSent: true,
           createdAt: true,
         },
       });
     } catch (error) {
       console.error("Failed to find inactive users:", error);
       throw new InternalError("Failed to find inactive users");
     }
   }
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ model User {
     status           String
     isEmailVerified  Boolean
     lastActivityAt   DateTime?
     lastReminderSent DateTime?
     createdAt        DateTime @default(now())
 
     // existing indices...
+    @@index([status, isEmailVerified, lastReminderSent])
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static async findInactiveUsers(days: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
try {
return await prisma.user.findMany({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
OR: [
{
lastActivityAt: {
lte: cutoffDate,
},
},
{
lastActivityAt: null,
createdAt: {
lte: cutoffDate,
},
},
],
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
lastActivityAt: true,
lastReminderSent: true,
createdAt: true,
},
});
} catch (error) {
console.error("Failed to find inactive users:", error);
throw new InternalError("Failed to find inactive users");
}
}
// File: backend/src/services/user.service.ts
public static async findInactiveUsers(
days: number,
minHoursBetweenReminders = 24,
) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
// Only re-remind users after at least minHoursBetweenReminders have passed
const remindCutoff = new Date(
Date.now() - minHoursBetweenReminders * 60 * 60 * 1000,
);
try {
return await prisma.user.findMany({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
AND: [
{
OR: [
{ lastActivityAt: { lte: cutoffDate } },
{ lastActivityAt: null, createdAt: { lte: cutoffDate } },
],
},
{
OR: [
{ lastReminderSent: null },
{ lastReminderSent: { lt: remindCutoff } },
],
},
],
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
lastActivityAt: true,
lastReminderSent: true,
createdAt: true,
},
});
} catch (error) {
console.error("Failed to find inactive users:", error);
throw new InternalError("Failed to find inactive users");
}
}
Suggested change
public static async findInactiveUsers(days: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
try {
return await prisma.user.findMany({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
OR: [
{
lastActivityAt: {
lte: cutoffDate,
},
},
{
lastActivityAt: null,
createdAt: {
lte: cutoffDate,
},
},
],
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
lastActivityAt: true,
lastReminderSent: true,
createdAt: true,
},
});
} catch (error) {
console.error("Failed to find inactive users:", error);
throw new InternalError("Failed to find inactive users");
}
}
// File: backend/prisma/schema.prisma
model User {
status String
isEmailVerified Boolean
lastActivityAt DateTime?
lastReminderSent DateTime?
createdAt DateTime @default(now())
// existing indices…
@@index([status, isEmailVerified, lastReminderSent])
}

Comment on lines +131 to +159
public static async countInactiveUsers(days: number): Promise<number> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);

try {
return await prisma.user.count({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
OR: [
{
lastActivityAt: {
lte: cutoffDate,
},
},
{
lastActivityAt: null,
createdAt: {
lte: cutoffDate,
},
},
],
},
});
} catch (error) {
console.error("Failed to count inactive users:", error);
throw new InternalError("Failed to count inactive users");
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Keep count in sync with candidate criteria (add cooldown predicate too)

countInactiveUsers should mirror findInactiveUsers to reflect the same cooldown and avoid misleading metrics.

-  public static async countInactiveUsers(days: number): Promise<number> {
+  public static async countInactiveUsers(days: number, minHoursBetweenReminders = 24): Promise<number> {
     const cutoffDate = new Date();
     cutoffDate.setDate(cutoffDate.getDate() - days);
+    const remindCutoff = new Date(Date.now() - minHoursBetweenReminders * 60 * 60 * 1000);

     try {
       return await prisma.user.count({
         where: {
           status: Status.ACTIVE,
           isEmailVerified: true,
-          OR: [
-            {
-              lastActivityAt: {
-                lte: cutoffDate,
-              },
-            },
-            {
-              lastActivityAt: null,
-              createdAt: {
-                lte: cutoffDate,
-              },
-            },
-          ],
+          AND: [
+            {
+              OR: [
+                { lastActivityAt: { lte: cutoffDate } },
+                { lastActivityAt: null, createdAt: { lte: cutoffDate } },
+              ],
+            },
+            {
+              OR: [
+                { lastReminderSent: null },
+                { lastReminderSent: { lt: remindCutoff } },
+              ],
+            },
+          ],
         },
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static async countInactiveUsers(days: number): Promise<number> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
try {
return await prisma.user.count({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
OR: [
{
lastActivityAt: {
lte: cutoffDate,
},
},
{
lastActivityAt: null,
createdAt: {
lte: cutoffDate,
},
},
],
},
});
} catch (error) {
console.error("Failed to count inactive users:", error);
throw new InternalError("Failed to count inactive users");
}
}
public static async countInactiveUsers(
days: number,
minHoursBetweenReminders = 24
): Promise<number> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const remindCutoff = new Date(Date.now() - minHoursBetweenReminders * 60 * 60 * 1000);
try {
return await prisma.user.count({
where: {
status: Status.ACTIVE,
isEmailVerified: true,
AND: [
{
OR: [
{ lastActivityAt: { lte: cutoffDate } },
{ lastActivityAt: null, createdAt: { lte: cutoffDate } },
],
},
{
OR: [
{ lastReminderSent: null },
{ lastReminderSent: { lt: remindCutoff } },
],
},
],
},
});
} catch (error) {
console.error("Failed to count inactive users:", error);
throw new InternalError("Failed to count inactive users");
}
}
🤖 Prompt for AI Agents
In backend/src/services/user.service.ts around lines 131 to 159, the where
clause used for counting inactive users does not include the cooldown predicate
and therefore diverges from findInactiveUsers; update the prisma.user.count
where filter to exactly mirror the predicates used in findInactiveUsers (status,
isEmailVerified, the same OR on lastActivityAt/createdAt AND include the
cooldown check used there) so metrics match the selection logic — copy the same
cooldown condition (same field name and comparison) and combine it with the
existing predicates so countInactiveUsers returns the same set as
findInactiveUsers.

@Gerson2102
Copy link
Contributor

Hello @laryhills. Please address every coderabbit suggestion.

@Gerson2102
Copy link
Contributor

Gerson2102 commented Aug 27, 2025

Hello @laryhills please pull the changes and rebase and fix conflicts sir.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/utils/service/nodeMailer.ts (1)

45-50: Fix logs that never print messageId/error.

Template strings contain literal “info.messageId”/“error” instead of variables.

Apply:

-      console.log(`📨 Text email sent to ${email}:, info.messageId`);
+      console.log(`📨 Text email sent to ${email}: ${info.messageId}`);
...
-      console.error(`❌ Failed to send text email to ${email}:, error`);
+      console.error(`❌ Failed to send text email to ${email}:`, error);
...
-      console.log(`📧 HTML email sent to ${email}:, info.messageId`);
+      console.log(`📧 HTML email sent to ${email}: ${info.messageId}`);
...
-      console.error(`❌ Failed to send HTML email to ${email}:, error`);
+      console.error(`❌ Failed to send HTML email to ${email}:`, error);

Consider masking PII in logs (e.g., j***@d***.com).

Also applies to: 63-69

🧹 Nitpick comments (6)
backend/src/utils/service/nodeMailer.ts (1)

11-16: Avoid hardcoding Gmail; drive SMTP from config (and class name mismatch).

Reading host/port/secure from settings keeps environments flexible and prevents accidental prod breakage. Also, the class is named ZohoMailer but now points to Gmail; consider renaming to SmtpMailer for clarity.

Apply:

-      host: "smtp.gmail.com",
-      port: 587,
-      secure: false,
+      host: serverSettings.email.host,
+      port: serverSettings.email.port ?? 587,
+      secure: serverSettings.email.secure ?? false,
+      requireTLS: serverSettings.email.requireTLS ?? true,
       connectionTimeout: 10000, // 10 seconds
       greetingTimeout: 10000,   // 10 seconds
       socketTimeout: 10000,     // 10 seconds

Optional (helps throughput, fewer handshakes):

-    this.transporter = nodemailer.createTransport({
+    this.transporter = nodemailer.createTransport({
+      pool: serverSettings.email.pool ?? true,
+      maxConnections: serverSettings.email.maxConnections ?? 3,
+      maxMessages: serverSettings.email.maxMessages ?? 100,
backend/src/utils/service/emailNotifier.ts (5)

11-23: LGTM: validated app URL.

Good defensive check; matches prior feedback. Consider sourcing from centralized settings to avoid direct env reads in services.


99-105: Reuse a shared mailer; avoid per-email SMTP handshakes.

Instantiating ZohoMailer for every email is costly and risks throttling. Keep a static/shared instance.

Apply:

-      const mailer = new ZohoMailer();
-
-      await mailer.sendTextEmail(data.email, subject, message);
+      await EmailNotifier.mailer.sendTextEmail(data.email, subject, message);

Add once in this class (outside the shown range):

// inside class EmailNotifier
private static mailer = new ZohoMailer();

Optional: prefer HTML for better UX (clickable link) via sendHtmlEmail.


33-97: Reduce template duplication; optional HTML templates.

The switch repeats structure. A map improves maintainability; HTML templates would also allow richer content and tracking links.

Example refactor:

-    switch (reminderType) {
-      case '7-day':
-        return { subject: "...", message: `... ${this.getValidatedAppUrl()}` };
-      ...
-      default:
-        throw new Error(`Unknown reminder type: ${reminderType}`);
-    }
+    const name = firstName || "there";
+    const appUrl = this.getValidatedAppUrl();
+    const templates: Record<ReminderEmailData['reminderType'], {subject:string; message:string}> = {
+      '7-day': { subject: "We miss you! Come back to AURORA", message: `Hi ${name},\n\nWe noticed ... ${appUrl}\n\nKeep learning!\nThe AURORA Team` },
+      '14-day': { subject: "Don't lose your progress - Continue your journey", message: `Hi ${name},\n\nIt's been ${daysSinceActivity} days ... ${appUrl}\n\nWe believe ...` },
+      '30-day': { subject: "Special learning resources waiting for you", message: `Hi ${name},\n\nWe haven't seen you ... ${appUrl}\n\nLet's restart ...` },
+      '60-day': { subject: "Your journey awaits - Final reminder", message: `Hi ${name},\n\nIt's been ${daysSinceActivity} days ... ${appUrl}\n\nWishing you success,\nThe AURORA Team` }
+    };
+    return templates[reminderType];

1-1: Naming mismatch: ZohoMailer now uses Gmail.

Rename to SmtpMailer (and update imports) to avoid future confusion.


112-152: Clarify retry semantics and consider bounded concurrency

  • The current loop (while (attempts <= maxRetries)) treats maxRetries as number of retries (so with the default of 2 it actually attempts up to 3 sends: 1 initial + 2 retries). If that’s intended, leave it as maxRetries but add a comment explaining this. Otherwise, you can:

    • Rename the parameter to maxAttempts (and update all call‐sites in userReminder.job.ts and settings.ts), then change to while (attempts < maxAttempts).
    • Or keep the name maxRetries and change the loop to while (attempts < maxRetries) if you truly want “total attempts = maxRetries.”
  • For large batches, sequential awaits can be slow. Consider a bounded‐concurrency approach (e.g., use a promise‐pool or libraries like p-map with a concurrency of 5–10) so you parallelize sendReminderEmail calls without overwhelming the SMTP server.

Example optional diff if you opt for maxAttempts semantics:

- public static async sendReminderEmails(reminders: ReminderEmailData[], maxRetries: number = 2, retryDelayMs: number = 1000) {
+ public static async sendReminderEmails(
+   reminders: ReminderEmailData[],
+   maxAttempts: number = 3,     // total send attempts
+   retryDelayMs: number = 1000
+ ) {-    while (attempts <= maxRetries && !success) {
+    while (attempts < maxAttempts && !success) {-      if (attempts > maxRetries) {
+      if (attempts >= maxAttempts) {
         results.failed++;
         …
      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa1aa9 and 80c6ba9.

📒 Files selected for processing (2)
  • backend/src/utils/service/emailNotifier.ts (2 hunks)
  • backend/src/utils/service/nodeMailer.ts (1 hunks)

@laryhills
Copy link
Author

@Gerson2102 Will do so as soon as possible

@laryhills
Copy link
Author

@Gerson2102
All merge conflicts resolved.

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.

[BACKEND] - Implement a scheduled job system to send email reminders

3 participants