-
Notifications
You must be signed in to change notification settings - Fork 49
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
Changes from 32 commits
19b6e96
cfd59e8
3604289
2cdce35
350d485
adda905
5013738
996760f
1401096
e1ef71e
20f0de5
c0af853
b01ef18
574f92f
a9631d3
dc1f126
cab216e
5c8383f
f27ed70
7681a50
37e63cb
b878909
d31bb0e
d738745
368a7f9
cedd5cd
87a7e02
7d6395c
d1ae4bc
34e7ff2
42a6918
45401bd
a9e5e7c
df8354f
01e8645
b9b9e6a
5835878
c872e71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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", | ||||
"nodemailer": "^6.9.13", | ||||
"passport": "^0.6.0", | ||||
"passport-google-oauth20": "^2.0.0", | ||||
|
@@ -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", | ||||
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
|
||||
"@types/nodemailer": "^6.4.15", | ||||
"@types/passport": "^1.0.12", | ||||
"@types/passport-google-oauth20": "^2.0.14", | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' }) | ||
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. What is the purpose of this? 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. 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' }) | ||
} | ||
} |
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 |
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 |
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 |
This file was deleted.
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"`); | ||
} | ||
|
||
} |
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 |
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 if not required