-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Monthly Reminder Service #181
Closed
mayura-andrew
wants to merge
41
commits into
sef-global:development
from
mayura-andrew:reimp_reminder
Closed
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
19b6e96
Merge pull request #4 from sef-global/main
mayura-andrew cfd59e8
Merge branch 'sef-global:main' into main
mayura-andrew 3604289
Merge branch 'sef-global:main' into main
mayura-andrew 2cdce35
Merge branch 'sef-global:main' into main
mayura-andrew 350d485
Merge branch 'sef-global:main' into main
mayura-andrew adda905
Merge branch 'sef-global:main' into main
mayura-andrew 5013738
Merge branch 'sef-global:main' into main
mayura-andrew 996760f
Merge branch 'sef-global:main' into main
mayura-andrew 1401096
Merge branch 'sef-global:main' into main
mayura-andrew e1ef71e
Refactor profile update logic to use a single updateData object
mayura-andrew 20f0de5
Refactor profile update logic to use a single updateData object
mayura-andrew c0af853
Merge branch 'sef-global:main' into main
mayura-andrew b01ef18
Merge branch 'sef-global:main' into main
mayura-andrew 574f92f
Refactor Mentee entity to include MonthlyCheckIn relationship
mayura-andrew a9631d3
Refactor Mentee entity to include MonthlyCheckIn relationship
mayura-andrew dc1f126
Refactor Mentee entity to include MonthlyCheckIn relationship
mayura-andrew cab216e
Refactor Mentee service to include MonthlyCheckIn relationship
mayura-andrew 5c8383f
Refactor Mentee service to include MonthlyCheckIn relationship
mayura-andrew f27ed70
Merge branch 'sef-global:main' into main
mayura-andrew 7681a50
Added tags column into monthly-checking-in table
mayura-andrew 37e63cb
Refactor Mentee service to include MonthlyCheckIn feedback functionality
mayura-andrew b878909
Refactor Mentee service to include MonthlyCheckIn feedback functionality
mayura-andrew d31bb0e
Refactor Mentee service to include MonthlyCheckIn feedback functionality
mayura-andrew d738745
Refactor Mentee service to remove unnecessary code
mayura-andrew 368a7f9
Refactor: Separate Monthly Checking Services and Controllers
mayura-andrew cedd5cd
removed sudo file
mayura-andrew 87a7e02
Refactor: Make mentor feedback optional in addFeedbackMonthlyCheckInS…
mayura-andrew 7d6395c
Refactor MonthlyChecking service to include MonthlyCheckInResponse type
mayura-andrew d1ae4bc
Merge branch 'sef-global:development' into monthly-checking-feature
mayura-andrew 34e7ff2
Merge pull request #5 from mayura-andrew/monthly-checking-feature
mayura-andrew 42a6918
Add email reminder endpoint and handler (not completed)
mayura-andrew 45401bd
implementation of remainder email (draft)
mayura-andrew a9e5e7c
Merge branch 'development' into remainder-email
mayura-andrew df8354f
Refactor reminder service to calculate next reminder date dynamically
mayura-andrew 01e8645
Refactor reminder service to handle maximum reminder sequence correctly
mayura-andrew b9b9e6a
Refactor reminder entities: remove failed reminders and reminder atte…
mayura-andrew 5835878
Refactor reminder system: update reminder status enums, modify routes…
mayura-andrew c872e71
Refactor reminder logic: remove unused dependencies, enhance reminder…
mayura-andrew 6c8d4fd
Implement monthly reminder service
mayura-andrew 13c0ac9
Implement month condition
mayura-andrew e72f294
fix docker setup
mayura-andrew File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Column, Entity, ManyToOne } from 'typeorm' | ||
import BaseEntity from './baseEntity' | ||
import { ReminderStatus } from '../enums' | ||
import Mentee from './mentee.entity' | ||
|
||
@Entity('monthly_reminders') | ||
export class MonthlyReminder extends BaseEntity { | ||
@ManyToOne(() => Mentee, (mentee) => mentee.reminders) | ||
mentee!: Mentee | ||
|
||
@Column({ | ||
type: 'enum', | ||
enum: ReminderStatus, | ||
default: ReminderStatus.PENDING | ||
}) | ||
status!: ReminderStatus | ||
|
||
@Column({ type: 'text', nullable: true }) | ||
lastError!: string | ||
|
||
@Column({ type: 'int', default: 0 }) | ||
remindersSent!: number | ||
|
||
@Column({ type: 'timestamp', nullable: true }) | ||
nextReminderDate!: Date | null | ||
|
||
@Column({ type: 'timestamp', nullable: true }) | ||
lastSentDate!: Date | null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { MigrationInterface, QueryRunner } from "typeorm"; | ||
|
||
export class MonthlyReminder1730893956260 implements MigrationInterface { | ||
name = 'MonthlyReminder1730893956260' | ||
|
||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" RENAME COLUMN "sentAt" TO "nextReminderDate"`); | ||
await queryRunner.query(`ALTER TABLE "mentee" DROP COLUMN "last_monthlycheck_send_at"`); | ||
await queryRunner.query(`ALTER TYPE "public"."monthly_reminders_status_enum" RENAME TO "monthly_reminders_status_enum_old"`); | ||
await queryRunner.query(`CREATE TYPE "public"."monthly_reminders_status_enum" AS ENUM('pending', 'sending', 'completed', 'failed', 'sent', 'scheduled')`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" ALTER COLUMN "status" DROP DEFAULT`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" ALTER COLUMN "status" TYPE "public"."monthly_reminders_status_enum" USING "status"::"text"::"public"."monthly_reminders_status_enum"`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" ALTER COLUMN "status" SET DEFAULT 'pending'`); | ||
await queryRunner.query(`DROP TYPE "public"."monthly_reminders_status_enum_old"`); | ||
await queryRunner.query(`ALTER TYPE "public"."reminder_attempts_status_enum" RENAME TO "reminder_attempts_status_enum_old"`); | ||
await queryRunner.query(`CREATE TYPE "public"."reminder_attempts_status_enum" AS ENUM('pending', 'sending', 'completed', 'failed', 'sent', 'scheduled')`); | ||
await queryRunner.query(`DROP TYPE "public"."reminder_attempts_status_enum_old"`); | ||
await queryRunner.query(`CREATE TYPE "public"."reminder_attempts_status_enum" AS ENUM('pending', 'sending', 'completed', 'failed', 'sent', 'scheduled')`); | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(`CREATE TYPE "public"."monthly_reminders_status_enum_old" AS ENUM('pending', 'sending', 'completed', 'failed', 'done', 'scheduled', 'sent', 'waiting', 'processing')`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" ALTER COLUMN "status" DROP DEFAULT`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" ALTER COLUMN "status" TYPE "public"."monthly_reminders_status_enum_old" USING "status"::"text"::"public"."monthly_reminders_status_enum_old"`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" ALTER COLUMN "status" SET DEFAULT 'pending'`); | ||
await queryRunner.query(`DROP TYPE "public"."monthly_reminders_status_enum"`); | ||
await queryRunner.query(`ALTER TYPE "public"."monthly_reminders_status_enum_old" RENAME TO "monthly_reminders_status_enum"`); | ||
await queryRunner.query(`ALTER TABLE "mentee" ADD "last_monthlycheck_send_at" TIMESTAMP`); | ||
await queryRunner.query(`ALTER TABLE "monthly_reminders" RENAME COLUMN "nextReminderDate" TO "sentAt"`); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import express from 'express' | ||
import { processEmailReminderHandler } from '../../../controllers/admin/email.controller' | ||
|
||
const reminderRouter = express.Router() | ||
|
||
reminderRouter.get('/process', processEmailReminderHandler) | ||
|
||
export default reminderRouter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,163 @@ | ||||
import { type Repository } from 'typeorm' | ||||
import { MonthlyReminder } from '../../entities/monthlyReminders.entity' | ||||
import type Mentee from '../../entities/mentee.entity' | ||||
import { MenteeApplicationStatus, ReminderStatus } from '../../enums' | ||||
import { getReminderEmailContent } from '../../utils' | ||||
import { sendEmail } from './email.service' | ||||
|
||||
export class MonthlyReminderService { | ||||
constructor( | ||||
private readonly reminderRepo: Repository<MonthlyReminder>, | ||||
private readonly menteeRepo: Repository<Mentee> | ||||
) {} | ||||
|
||||
async processReminders(): Promise<{ | ||||
statusCode: number | ||||
message: string | ||||
}> { | ||||
// Schedule new reminders | ||||
try { | ||||
await this.scheduleNewReminder() | ||||
|
||||
const today = new Date() | ||||
const todayDateString = today.toDateString() | ||||
console.log(todayDateString) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
|
||||
const pendingReminders = await this.reminderRepo | ||||
.createQueryBuilder('reminder') | ||||
.leftJoinAndSelect('reminder.mentee', 'mentee') | ||||
.leftJoinAndSelect('mentee.profile', 'profile') | ||||
.where('reminder.remindersSent <= :maxReminders', { maxReminders: 6 }) | ||||
.andWhere('reminder.status != :status', { | ||||
status: ReminderStatus.COMPLETED | ||||
}) | ||||
.andWhere( | ||||
"(reminder.lastSentDate IS NULL OR DATE_PART('month', reminder.lastSentDate) != DATE_PART('month', CURRENT_DATE))" | ||||
) | ||||
.getMany() | ||||
|
||||
if (pendingReminders.length === 0) { | ||||
return { | ||||
statusCode: 200, | ||||
message: 'No reminders to process' | ||||
} | ||||
} | ||||
|
||||
const remindersToSend = pendingReminders.filter( | ||||
(reminder) => | ||||
reminder.nextReminderDate && | ||||
new Date(reminder?.nextReminderDate).toDateString() === | ||||
todayDateString | ||||
) | ||||
|
||||
console.log(remindersToSend) | ||||
|
||||
if (remindersToSend.length === 0) { | ||||
return { | ||||
statusCode: 200, | ||||
message: 'No reminders to process today' | ||||
} | ||||
} | ||||
|
||||
for (const reminder of remindersToSend) { | ||||
try { | ||||
reminder.status = ReminderStatus.SENDING | ||||
await this.reminderRepo.save(reminder) | ||||
|
||||
// Send email | ||||
const emailContent = getReminderEmailContent( | ||||
reminder.mentee.profile.first_name | ||||
) | ||||
|
||||
await sendEmail( | ||||
reminder.mentee.profile.primary_email, | ||||
emailContent.subject, | ||||
emailContent.message | ||||
) | ||||
|
||||
// Update reminder status | ||||
reminder.remindersSent += 1 | ||||
reminder.lastSentDate = new Date() | ||||
reminder.status = | ||||
reminder.remindersSent >= 6 | ||||
? ReminderStatus.COMPLETED | ||||
: ReminderStatus.SCHEDULED | ||||
|
||||
// Calculate next reminder date if not completed | ||||
|
||||
if (reminder.status !== ReminderStatus.COMPLETED) { | ||||
reminder.nextReminderDate = await this.calculateNextReminderDate( | ||||
today | ||||
) | ||||
} | ||||
await this.reminderRepo.save(reminder) | ||||
} catch (error) { | ||||
reminder.status = ReminderStatus.FAILED | ||||
await this.reminderRepo.save(reminder) | ||||
console.error( | ||||
`Failed to process reminder for mentee ${reminder.mentee.uuid}:`, | ||||
error | ||||
) | ||||
} | ||||
} | ||||
|
||||
return { | ||||
statusCode: 200, | ||||
message: `Processed ${pendingReminders.length} reminders` | ||||
} | ||||
} catch (error) { | ||||
console.error('Error processing reminders:', error) | ||||
return { | ||||
statusCode: 500, | ||||
message: 'Failed to process reminders' | ||||
} | ||||
} | ||||
} | ||||
|
||||
private async calculateNextReminderDate(currentDate: Date): Promise<Date> { | ||||
const nextDate = new Date(currentDate) | ||||
nextDate.setDate(nextDate.getDate() + 25) | ||||
return nextDate | ||||
} | ||||
|
||||
private async scheduleNewReminder(): Promise<void> { | ||||
const today = new Date() | ||||
|
||||
const newMentees = await this.menteeRepo | ||||
.createQueryBuilder('mentee') | ||||
.leftJoinAndSelect('mentee.profile', 'profile') | ||||
.where('mentee.state = :state', { | ||||
state: MenteeApplicationStatus.APPROVED | ||||
}) | ||||
.andWhere((qb) => { | ||||
const subQuery = qb | ||||
.subQuery() | ||||
.select('reminder.mentee.uuid') | ||||
.from(MonthlyReminder, 'reminder') | ||||
.getQuery() | ||||
return 'mentee.uuid NOT IN ' + subQuery | ||||
}) | ||||
.getMany() | ||||
|
||||
console.log(newMentees) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
|
||||
if (newMentees.length > 0) { | ||||
const reminders = await Promise.all( | ||||
newMentees.map(async (mentee) => { | ||||
const reminder = new MonthlyReminder() | ||||
reminder.mentee = mentee | ||||
reminder.remindersSent = 0 | ||||
reminder.status = ReminderStatus.SCHEDULED | ||||
reminder.lastSentDate = null | ||||
reminder.nextReminderDate = await this.calculateNextReminderDate( | ||||
today | ||||
) | ||||
return reminder | ||||
}) | ||||
) | ||||
|
||||
await this.reminderRepo.save(reminders) | ||||
console.log(`Scheduled ${reminders.length} new reminders`) | ||||
} | ||||
} | ||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this