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 Automatic remainder sending email service #177

Closed
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 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
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
907 changes: 541 additions & 366 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"dotenv": "^16.3.1",
"ejs": "^3.1.10",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"node-cron": "^3.0.3",
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 if not required

Suggested change
"node-cron": "^3.0.3",

"nodemailer": "^6.9.13",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
Expand All @@ -56,6 +58,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.

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
16 changes: 15 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import cron from 'node-cron'
import type { Express } from 'express'
import express from 'express'
import fs from 'fs'
Expand All @@ -18,7 +19,7 @@ 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'

import { EmailReminderService } from './services/admin/reminder.service'
const app = express()
const staticFolder = 'uploads'
export const certificatesDir = path.join(__dirname, 'certificates')
Expand Down Expand Up @@ -59,6 +60,19 @@ if (!fs.existsSync(certificatesDir)) {
fs.mkdirSync(certificatesDir)
}

const sqlReminderService = new EmailReminderService()

// Setup periodic processing
setInterval(async () => {
await sqlReminderService.processReminders()
}, 60000) // Check every minute

// Schedule new reminders daily
cron.schedule('* * * * *', async () => {
console.log('Scheduling daily reminders')
await sqlReminderService.scheduleReminders()
})

export const startServer = async (port: number): Promise<Express> => {
try {
await dataSource.initialize()
Expand Down
12 changes: 12 additions & 0 deletions src/controllers/admin/email.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ export const sendEmailController = async (
throw err
}
}

export const enableEmailReminderHandler = async (
req: Request,
res: Response
): Promise<void> => {
try {
res.status(200).json({ message: 'Reminder enabled' })
Copy link
Member

Choose a reason for hiding this comment

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

What is the purpose of this?

Copy link
Member Author

@mayura-andrew mayura-andrew Oct 31, 2024

Choose a reason for hiding this comment

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

this is a API endpoint but not implemented yet. I'll implement this soon as possible :)

} catch (err) {
console.error('Error enabling reminder', err)
res.status(500).json({ message: 'Error enabling reminder' })
}
}
21 changes: 21 additions & 0 deletions src/entities/failedReminders.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm'
import BaseEntity from './baseEntity'
import Mentee from './mentee.entity'

@Entity('failed_reminders')
class FailedReminders extends BaseEntity {
@Column({ type: 'text' })
error: string

@ManyToOne(() => Mentee, (mentee) => mentee.failedReminders)
@JoinColumn({ name: 'menteeId' })
mentee: Mentee

constructor(error: string, mentee: Mentee) {
super()
this.error = error
this.mentee = mentee
}
}

export default FailedReminders
11 changes: 10 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 FailedReminders from './failedReminders.entity'

@Entity('mentee')
class Mentee extends BaseEntity {
Expand All @@ -21,6 +22,9 @@ class Mentee extends BaseEntity {
@Column({ type: 'timestamp', nullable: true })
status_updated_date!: Date

@Column({ type: 'timestamp', nullable: true })
last_monthlycheck_reminder_date!: Date

@Column({ type: 'json' })
application: Record<string, unknown>

Expand All @@ -39,19 +43,24 @@ class Mentee extends BaseEntity {
@OneToMany(() => MonthlyCheckIn, (checkIn) => checkIn.mentee)
checkIns?: MonthlyCheckIn[]

@OneToMany(() => FailedReminders, (failedReminder) => failedReminder.mentee)
failedReminders?: FailedReminders[]

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

Expand Down
51 changes: 51 additions & 0 deletions src/entities/mentee_reminder_configs.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Entity, Column, OneToOne, OneToMany, JoinColumn, Index } from 'typeorm'
import BaseEntity from './baseEntity'
import Mentee from './mentee.entity'
import ReminderAttempt from './reminder_attempts.entity'

@Entity('mentee_reminder_configs')
class MenteeReminderConfig extends BaseEntity {
@Column({ type: 'uuid', primary: true })
@Index({ unique: true }) // Add unique constraint
menteeId!: string

@Column({ type: 'varchar', length: 255 })
email!: string

@Column({ type: 'varchar', length: 255 })
firstName!: string

@Column({ type: 'timestamp', nullable: true })
firstReminderSentAt?: Date

@Column({ type: 'int', default: 0 })
currentSequence!: number

@Column({ type: 'timestamp', nullable: true })
lastReminderSentAt?: Date

@Column({ type: 'timestamp' })
nextReminderDue!: Date

@Column({ type: 'boolean', default: false })
isComplete!: boolean

@OneToOne(() => Mentee)
@JoinColumn({ name: 'menteeId' })
mentee!: Mentee

@OneToMany(() => ReminderAttempt, (attempt) => attempt.reminderConfig)
attempts?: ReminderAttempt[]

constructor(menteeId: string, email: string, firstName: string) {
super()
this.menteeId = menteeId
this.email = email
this.firstName = firstName
this.currentSequence = 0
this.nextReminderDue = new Date()
this.isComplete = false
}
}

export default MenteeReminderConfig
49 changes: 49 additions & 0 deletions src/entities/reminder_attempts.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ReminderStatus } from '../enums'
import BaseEntity from './baseEntity'
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'
import MenteeReminderConfig from './mentee_reminder_configs.entity'

@Entity('reminder_attempts')
class ReminderAttempt extends BaseEntity {
@Column({ type: 'uuid' }) // Changed to uuid to match parent table
menteeId!: string

@Column({
type: 'enum',
enum: ReminderStatus,
default: ReminderStatus.PENDING
})
status!: ReminderStatus

@Column({ type: 'int' })
sequence!: number

@Column({ default: 0 })
retryCount!: number

@Column({ type: 'timestamp', nullable: true })
processedAt?: Date

@Column({ type: 'timestamp' })
nextRetryAt!: Date

@Column({ type: 'text', nullable: true })
errorMessage?: string

@ManyToOne(() => MenteeReminderConfig, (config) => config.attempts, {
onDelete: 'CASCADE'
})
@JoinColumn({ name: 'menteeId', referencedColumnName: 'menteeId' })
reminderConfig?: MenteeReminderConfig

constructor(menteeId: string, sequence: number) {
super()
this.menteeId = menteeId
this.sequence = sequence
this.status = ReminderStatus.PENDING
this.retryCount = 0
this.nextRetryAt = new Date()
}
}

export default ReminderAttempt
8 changes: 8 additions & 0 deletions src/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ export enum StatusUpdatedBy {
ADMIN = 'admin',
MENTOR = 'mentor'
}

export enum ReminderStatus {
PENDING = 'pending',
PROCESSING = 'processing',
COMPLETE = 'complete',
FAILED = 'failed',
DONE = 'done'
}
14 changes: 0 additions & 14 deletions src/migrations/1727197270336-monthly-checking-tags.ts

This file was deleted.

22 changes: 22 additions & 0 deletions src/migrations/1730033358494-reminder_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class ReminderConfig1730033358494 implements MigrationInterface {
name = 'ReminderConfig1730033358494'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "reminder_attempts" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "menteeId" uuid NOT NULL, "status" "public"."reminder_attempts_status_enum" NOT NULL DEFAULT 'pending', "sequence" integer NOT NULL, "retryCount" integer NOT NULL DEFAULT '0', "processedAt" TIMESTAMP, "nextRetryAt" TIMESTAMP NOT NULL, "errorMessage" text, CONSTRAINT "PK_c5ba5d688c6a78a67c786f5cb2b" PRIMARY KEY ("uuid"))`);
await queryRunner.query(`CREATE TABLE "mentee_reminder_configs" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "menteeId" uuid NOT NULL, "email" character varying(255) NOT NULL, "firstName" character varying(255) NOT NULL, "firstReminderSentAt" TIMESTAMP, "currentSequence" integer NOT NULL DEFAULT '0', "lastReminderSentAt" TIMESTAMP, "nextReminderDue" TIMESTAMP NOT NULL, "isComplete" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_ce2ebbc0fd11a8e0e1c35f06108" PRIMARY KEY ("uuid", "menteeId"))`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_19e37bcbcb83cf968be2ffe4e6" ON "mentee_reminder_configs" ("menteeId") `);
await queryRunner.query(`ALTER TABLE "reminder_attempts" ADD CONSTRAINT "FK_5d0f136c64e679dd74878f496c6" FOREIGN KEY ("menteeId") REFERENCES "mentee_reminder_configs"("menteeId") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "mentee_reminder_configs" ADD CONSTRAINT "FK_19e37bcbcb83cf968be2ffe4e69" FOREIGN KEY ("menteeId") REFERENCES "mentee"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "mentee_reminder_configs" DROP CONSTRAINT "FK_19e37bcbcb83cf968be2ffe4e69"`);
await queryRunner.query(`ALTER TABLE "reminder_attempts" DROP CONSTRAINT "FK_5d0f136c64e679dd74878f496c6"`);
await queryRunner.query(`DROP INDEX "public"."IDX_19e37bcbcb83cf968be2ffe4e6"`);
await queryRunner.query(`DROP TABLE "mentee_reminder_configs"`);
await queryRunner.query(`DROP TABLE "reminder_attempts"`);
}

}
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('/enable', 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 { enableEmailReminderHandler } from '../../../controllers/admin/email.controller'

const reminderRouter = express.Router()

reminderRouter.get('/', enableEmailReminderHandler)

export default reminderRouter
Loading