Skip to content
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
Closed
Show file tree
Hide file tree
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 Jul 14, 2024
cfd59e8
Merge branch 'sef-global:main' into main
mayura-andrew Jul 28, 2024
3604289
Merge branch 'sef-global:main' into main
mayura-andrew Jul 31, 2024
2cdce35
Merge branch 'sef-global:main' into main
mayura-andrew Aug 3, 2024
350d485
Merge branch 'sef-global:main' into main
mayura-andrew Aug 10, 2024
adda905
Merge branch 'sef-global:main' into main
mayura-andrew Aug 17, 2024
5013738
Merge branch 'sef-global:main' into main
mayura-andrew Aug 30, 2024
996760f
Merge branch 'sef-global:main' into main
mayura-andrew Sep 4, 2024
1401096
Merge branch 'sef-global:main' into main
mayura-andrew Sep 8, 2024
e1ef71e
Refactor profile update logic to use a single updateData object
mayura-andrew Sep 9, 2024
20f0de5
Refactor profile update logic to use a single updateData object
mayura-andrew Sep 9, 2024
c0af853
Merge branch 'sef-global:main' into main
mayura-andrew Sep 10, 2024
b01ef18
Merge branch 'sef-global:main' into main
mayura-andrew Sep 17, 2024
574f92f
Refactor Mentee entity to include MonthlyCheckIn relationship
mayura-andrew Sep 17, 2024
a9631d3
Refactor Mentee entity to include MonthlyCheckIn relationship
mayura-andrew Sep 18, 2024
dc1f126
Refactor Mentee entity to include MonthlyCheckIn relationship
mayura-andrew Sep 18, 2024
cab216e
Refactor Mentee service to include MonthlyCheckIn relationship
mayura-andrew Sep 18, 2024
5c8383f
Refactor Mentee service to include MonthlyCheckIn relationship
mayura-andrew Sep 20, 2024
f27ed70
Merge branch 'sef-global:main' into main
mayura-andrew Sep 23, 2024
7681a50
Added tags column into monthly-checking-in table
mayura-andrew Sep 24, 2024
37e63cb
Refactor Mentee service to include MonthlyCheckIn feedback functionality
mayura-andrew Sep 24, 2024
b878909
Refactor Mentee service to include MonthlyCheckIn feedback functionality
mayura-andrew Sep 27, 2024
d31bb0e
Refactor Mentee service to include MonthlyCheckIn feedback functionality
mayura-andrew Sep 27, 2024
d738745
Refactor Mentee service to remove unnecessary code
mayura-andrew Sep 27, 2024
368a7f9
Refactor: Separate Monthly Checking Services and Controllers
mayura-andrew Sep 29, 2024
cedd5cd
removed sudo file
mayura-andrew Sep 29, 2024
87a7e02
Refactor: Make mentor feedback optional in addFeedbackMonthlyCheckInS…
mayura-andrew Sep 29, 2024
7d6395c
Refactor MonthlyChecking service to include MonthlyCheckInResponse type
mayura-andrew Oct 6, 2024
d1ae4bc
Merge branch 'sef-global:development' into monthly-checking-feature
mayura-andrew Oct 15, 2024
34e7ff2
Merge pull request #5 from mayura-andrew/monthly-checking-feature
mayura-andrew Oct 15, 2024
42a6918
Add email reminder endpoint and handler (not completed)
mayura-andrew Oct 23, 2024
45401bd
implementation of remainder email (draft)
mayura-andrew Oct 27, 2024
a9e5e7c
Merge branch 'development' into remainder-email
mayura-andrew Oct 27, 2024
df8354f
Refactor reminder service to calculate next reminder date dynamically
mayura-andrew Oct 27, 2024
01e8645
Refactor reminder service to handle maximum reminder sequence correctly
mayura-andrew Oct 27, 2024
b9b9e6a
Refactor reminder entities: remove failed reminders and reminder atte…
mayura-andrew Oct 31, 2024
5835878
Refactor reminder system: update reminder status enums, modify routes…
mayura-andrew Nov 1, 2024
c872e71
Refactor reminder logic: remove unused dependencies, enhance reminder…
mayura-andrew Nov 2, 2024
6c8d4fd
Implement monthly reminder service
mayura-andrew Nov 4, 2024
13c0ac9
Implement month condition
mayura-andrew Nov 6, 2024
e72f294
fix docker setup
mayura-andrew Dec 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
892 changes: 522 additions & 370 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@types/jsonwebtoken": "^9.0.2",
"@types/multer": "^1.4.11",
"@types/node": "^20.1.4",
"@types/node-cron": "^3.0.11",
Copy link
Member

Choose a reason for hiding this comment

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

Remove this

Suggested change
"@types/node-cron": "^3.0.11",

"@types/nodemailer": "^6.4.15",
"@types/passport": "^1.0.12",
"@types/passport-google-oauth20": "^2.0.14",
Expand Down
1 change: 0 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import mentorRouter from './routes/mentor/mentor.route'
import profileRouter from './routes/profile/profile.route'
import path from 'path'
import countryRouter from './routes/country/country.route'

const app = express()
const staticFolder = 'uploads'
export const certificatesDir = path.join(__dirname, 'certificates')
Expand Down
22 changes: 22 additions & 0 deletions src/controllers/admin/email.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { type ApiResponse } from '../../types'
import type Email from '../../entities/email.entity'
import { ProfileTypes } from '../../enums'
import { sendEmail } from '../../services/admin/email.service'
import { MonthlyReminderService } from '../../services/admin/reminder.service'
import { dataSource } from '../../configs/dbConfig'
import { MonthlyReminder } from '../../entities/monthlyReminders.entity'
import Mentee from '../../entities/mentee.entity'

export const sendEmailController = async (
req: Request,
Expand All @@ -30,3 +34,21 @@ export const sendEmailController = async (
throw err
}
}

const reminderService = new MonthlyReminderService(
dataSource.getRepository(MonthlyReminder),
dataSource.getRepository(Mentee)
)

export const processEmailReminderHandler = async (
req: Request,
res: Response
): Promise<void> => {
try {
const { statusCode, message } = await reminderService.processReminders()
res.status(statusCode).json({ message })
} catch (err) {
console.error('Error enabling reminder', err)
res.status(500).json({ message: 'Error enabling reminder' })
}
}
8 changes: 7 additions & 1 deletion src/entities/mentee.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MenteeApplicationStatus, StatusUpdatedBy } from '../enums'
import BaseEntity from './baseEntity'
import { UUID } from 'typeorm/driver/mongodb/bson.typings'
import MonthlyCheckIn from './checkin.entity'
import { MonthlyReminder } from './monthlyReminders.entity'

@Entity('mentee')
class Mentee extends BaseEntity {
Expand Down Expand Up @@ -39,19 +40,24 @@ class Mentee extends BaseEntity {
@OneToMany(() => MonthlyCheckIn, (checkIn) => checkIn.mentee)
checkIns?: MonthlyCheckIn[]

@OneToMany(() => MonthlyReminder, (reminder) => reminder.mentee)
reminders?: MonthlyReminder[]

constructor(
state: MenteeApplicationStatus,
application: Record<string, unknown>,
profile: profileEntity,
mentor: Mentor,
checkIns?: MonthlyCheckIn[]
checkIns?: MonthlyCheckIn[],
reminders?: MonthlyReminder[]
) {
super()
this.state = state || MenteeApplicationStatus.PENDING
this.application = application
this.profile = profile
this.mentor = mentor
this.checkIns = checkIns
this.reminders = reminders
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/entities/monthlyReminders.entity.ts
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
}
9 changes: 9 additions & 0 deletions src/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ export enum StatusUpdatedBy {
ADMIN = 'admin',
MENTOR = 'mentor'
}

export enum ReminderStatus {
PENDING = 'pending',
SENDING = 'sending',
COMPLETED = 'completed',
FAILED = 'failed',
SENT = 'sent',
SCHEDULED = 'scheduled'
}
14 changes: 0 additions & 14 deletions src/migrations/1727197270336-monthly-checking-tags.ts

This file was deleted.

32 changes: 32 additions & 0 deletions src/migrations/1730893956260-MonthlyReminder.ts
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"`);
}

}
2 changes: 2 additions & 0 deletions src/routes/admin/admin.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import userRouter from './user/user.route'
import mentorRouter from './mentor/mentor.route'
import categoryRouter from './category/category.route'
import menteeRouter from './mentee/mentee.route'
import reminderRouter from './remainder/remainder.route'

const adminRouter = express()

adminRouter.use('/users', userRouter)
adminRouter.use('/mentors', mentorRouter)
adminRouter.use('/mentees', menteeRouter)
adminRouter.use('/categories', categoryRouter)
adminRouter.use('/reminders', reminderRouter)

export default adminRouter
8 changes: 8 additions & 0 deletions src/routes/admin/remainder/remainder.route.ts
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
1 change: 1 addition & 0 deletions src/services/admin/mentee.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const updateStatus = async (
certificate_id: content?.uniqueId
}
)

return {
statusCode: 200,
message: 'Mentee application state successfully updated'
Expand Down
163 changes: 163 additions & 0 deletions src/services/admin/reminder.service.ts
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)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
console.log(todayDateString)


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)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
console.log(newMentees)


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`)
}
}
}
1 change: 0 additions & 1 deletion src/services/mentee.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
capitalizeFirstLetter
} from '../utils'
import { sendEmail } from './admin/email.service'

export const addMentee = async (
user: Profile,
application: Record<string, unknown>,
Expand Down
Loading