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 all 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
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.

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
29 changes: 29 additions & 0 deletions src/controllers/admin/email.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type ApiResponse } from '../../types'
import type Email from '../../entities/email.entity'
import { ProfileTypes } from '../../enums'
import { sendEmail } from '../../services/admin/email.service'
import { ReminderService } from '../../services/admin/reminder.service'

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

const reminderService = new ReminderService()

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' })
}
}

export const scheduleNewReminderHandler = async (
req: Request,
res: Response
): Promise<void> => {
try {
const { statusCode, message } = await reminderService.scheduleNewReminders()
res.status(statusCode).json({ message })
} catch (err) {
console.error('Error scheduling reminder', err)
res.status(500).json({ message: 'Error scheduling reminder' })
}
}
3 changes: 3 additions & 0 deletions src/entities/mentee.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 Down
62 changes: 62 additions & 0 deletions src/entities/menteeReminders.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
BaseEntity,
Column,
Entity,
Index,
JoinColumn,
OneToOne
} from 'typeorm'
import Mentee from './mentee.entity'
import { ReminderStatus } from '../enums'

@Entity('mentee_reminders')
class MenteeReminder extends BaseEntity {
@Column({ type: 'uuid', primary: true })
@Index({ unique: true })
menteeId!: string

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

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

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

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

@Column({ type: 'timestamp', nullable: true })
nextReminderDue?: Date | null

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

@Column({ type: 'timestamp', nullable: true })
nextRetryAt?: Date | null

@Column({ type: 'varchar', nullable: true, length: 255 })
lastErrorMessage?: string | null

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

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

constructor(menteeId: string) {
super()
this.menteeId = menteeId
this.remindersSent = 0
this.retryCount = 0
this.isComplete = false
}
}

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

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

This file was deleted.

41 changes: 41 additions & 0 deletions src/migrations/1730362421047-mentee_reminders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm'

export class MenteeReminders1730362421047 implements MigrationInterface {
name = 'MenteeReminders1730362421047'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "mentee_reminders" ("menteeId" uuid NOT NULL, "remindersSent" integer NOT NULL DEFAULT '0', "firstReminderSentAt" TIMESTAMP, "lastReminderSentAt" TIMESTAMP, "nextReminderDue" TIMESTAMP, "retryCount" integer NOT NULL DEFAULT '0', "nextRetryAt" TIMESTAMP, "lastErrorMessage" character varying(255), "isComplete" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a2ae4434fe95f781453ed896f14" PRIMARY KEY ("menteeId"))`
)
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_a2ae4434fe95f781453ed896f1" ON "mentee_reminders" ("menteeId") `
)
await queryRunner.query(`ALTER TABLE "mentor" ADD "countryUuid" uuid`)
await queryRunner.query(
`ALTER TABLE "mentee_reminder_configs" ALTER COLUMN "nextReminderDue" DROP NOT NULL`
)
await queryRunner.query(
`ALTER TABLE "mentor" ADD CONSTRAINT "FK_3302c22eb1636f239d605eb61c3" FOREIGN KEY ("countryUuid") REFERENCES "country"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "mentee_reminders" ADD CONSTRAINT "FK_a2ae4434fe95f781453ed896f14" 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_reminders" DROP CONSTRAINT "FK_a2ae4434fe95f781453ed896f14"`
)
await queryRunner.query(
`ALTER TABLE "mentor" DROP CONSTRAINT "FK_3302c22eb1636f239d605eb61c3"`
)
await queryRunner.query(
`ALTER TABLE "mentee_reminder_configs" ALTER COLUMN "nextReminderDue" SET NOT NULL`
)
await queryRunner.query(`ALTER TABLE "mentor" DROP COLUMN "countryUuid"`)
await queryRunner.query(
`DROP INDEX "public"."IDX_a2ae4434fe95f781453ed896f1"`
)
await queryRunner.query(`DROP TABLE "mentee_reminders"`)
}
}
133 changes: 133 additions & 0 deletions src/migrations/1730443452480-AddReminderStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm'

export class AddReminderStatus1730443452480 implements MigrationInterface {
name = 'AddReminderStatus1730443452480'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."mentee_reminders_status_enum" AS ENUM('pending', 'sending', 'complete', 'failed', 'done', 'scheduled', 'sent')`
)
await queryRunner.query(
`ALTER TABLE "mentee_reminders" ADD "status" "public"."mentee_reminders_status_enum" NOT NULL DEFAULT 'pending'`
)
await queryRunner.query(
`ALTER TYPE "public"."email_reminders_status_enum" RENAME TO "email_reminders_status_enum_old"`
)
await queryRunner.query(
`CREATE TYPE "public"."email_reminders_status_enum" AS ENUM('pending', 'sending', 'complete', 'failed', 'done', 'scheduled', 'sent')`
)
await queryRunner.query(
`ALTER TABLE "email_reminders" ALTER COLUMN "status" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "email_reminders" ALTER COLUMN "status" TYPE "public"."email_reminders_status_enum" USING "status"::"text"::"public"."email_reminders_status_enum"`
)
await queryRunner.query(
`ALTER TABLE "email_reminders" ALTER COLUMN "status" SET DEFAULT 'pending'`
)
await queryRunner.query(
`DROP TYPE "public"."email_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', 'complete', 'failed', 'done', 'scheduled', 'sent')`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" TYPE "public"."reminder_attempts_status_enum" USING "status"::"text"::"public"."reminder_attempts_status_enum"`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" SET DEFAULT 'pending'`
)
await queryRunner.query(
`DROP TYPE "public"."reminder_attempts_status_enum_old"`
)
await queryRunner.query(
`ALTER TABLE "mentee_reminder_configs" ALTER COLUMN "nextReminderDue" SET NOT NULL`
)
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', 'complete', 'failed', 'done', 'scheduled', 'sent')`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" TYPE "public"."reminder_attempts_status_enum" USING "status"::"text"::"public"."reminder_attempts_status_enum"`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" SET DEFAULT 'pending'`
)
await queryRunner.query(
`DROP TYPE "public"."reminder_attempts_status_enum_old"`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."reminder_attempts_status_enum_old" AS ENUM('pending', 'processing', 'complete', 'failed', 'done')`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" TYPE "public"."reminder_attempts_status_enum_old" USING "status"::"text"::"public"."reminder_attempts_status_enum_old"`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" SET DEFAULT 'pending'`
)
await queryRunner.query(
`DROP TYPE "public"."reminder_attempts_status_enum"`
)
await queryRunner.query(
`ALTER TYPE "public"."reminder_attempts_status_enum_old" RENAME TO "reminder_attempts_status_enum"`
)
await queryRunner.query(
`ALTER TABLE "mentee_reminder_configs" ALTER COLUMN "nextReminderDue" DROP NOT NULL`
)
await queryRunner.query(
`CREATE TYPE "public"."reminder_attempts_status_enum_old" AS ENUM('pending', 'processing', 'complete', 'failed', 'done')`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" TYPE "public"."reminder_attempts_status_enum_old" USING "status"::"text"::"public"."reminder_attempts_status_enum_old"`
)
await queryRunner.query(
`ALTER TABLE "reminder_attempts" ALTER COLUMN "status" SET DEFAULT 'pending'`
)
await queryRunner.query(
`DROP TYPE "public"."reminder_attempts_status_enum"`
)
await queryRunner.query(
`ALTER TYPE "public"."reminder_attempts_status_enum_old" RENAME TO "reminder_attempts_status_enum"`
)
await queryRunner.query(
`CREATE TYPE "public"."email_reminders_status_enum_old" AS ENUM('pending', 'processing', 'complete', 'failed', 'done')`
)
await queryRunner.query(
`ALTER TABLE "email_reminders" ALTER COLUMN "status" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "email_reminders" ALTER COLUMN "status" TYPE "public"."email_reminders_status_enum_old" USING "status"::"text"::"public"."email_reminders_status_enum_old"`
)
await queryRunner.query(
`ALTER TABLE "email_reminders" ALTER COLUMN "status" SET DEFAULT 'pending'`
)
await queryRunner.query(`DROP TYPE "public"."email_reminders_status_enum"`)
await queryRunner.query(
`ALTER TYPE "public"."email_reminders_status_enum_old" RENAME TO "email_reminders_status_enum"`
)
await queryRunner.query(
`ALTER TABLE "mentee_reminders" DROP COLUMN "status"`
)
await queryRunner.query(`DROP TYPE "public"."mentee_reminders_status_enum"`)
}
}
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
12 changes: 12 additions & 0 deletions src/routes/admin/remainder/remainder.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import express from 'express'
import {
processEmailReminderHandler,
scheduleNewReminderHandler
} from '../../../controllers/admin/email.controller'

const reminderRouter = express.Router()

reminderRouter.get('/process', processEmailReminderHandler)
reminderRouter.get('/schedule', scheduleNewReminderHandler)

export default reminderRouter
Loading