Skip to content

Implement Automatically Reminder Service #192

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

Open
wants to merge 9 commits into
base: development
Choose a base branch
from
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ SMTP_PASSWORD=your_smtp_password
LINKEDIN_CLIENT_ID=your_linkedin_client_id
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
LINKEDIN_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/linkedin/callback
# Cron schedule for reminders (default: "0 1 * * *" - 1 AM daily)
REMINDER_CRON_SCHEDULE="0 1 * * *"
Comment on lines +18 to +19
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
# Cron schedule for reminders (default: "0 1 * * *" - 1 AM daily)
REMINDER_CRON_SCHEDULE="0 1 * * *"
# Cron schedule for reminders (example: "0 1 * * *" - 1 AM daily)
REMINDER_CRON_SCHEDULE=

4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Use an official Node.js runtime as the base image
FROM node:18


# Install postgresql-client
RUN apt-get update && apt-get install -y postgresql-client

# Set the working directory in the Docker container to /app
WORKDIR /app/src

Expand Down
8 changes: 7 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ services:
- IMG_HOST=${IMG_HOST}
- SMTP_MAIL=${SMTP_MAIL}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- REMINDER_CRON_SCHEDULE=${REMINDER_CRON_SCHEDULE}
command: ["sh", "/app/src/init-db.sh"]
db:
image: postgres:15
Expand All @@ -31,4 +32,9 @@ services:
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:
22 changes: 18 additions & 4 deletions init-db.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
#!/bin/sh

# This is a script to initialize the database for the first time when the container is started.
# It will wait for the database to be ready before running the migrations.
# Wait for the

# Wait for the database to be ready
until psql "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" -c '\q'; do
echo "Waiting for database connection..."
sleep 5
done

echo "Database is ready. Running migrations..."

# Run the migrations
npm run sync:db
npm run seed

# Check if the database is already populated
SEED_CHECK=$(psql "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" -tAc "SELECT COUNT(*) FROM profile;")

if [ "$SEED_CHECK" -eq "0" ]; then
echo "Database is empty. Running seed script..."
npm run seed
else
echo "Database already contains data. Skipping seed script."
fi

echo "Migrations complete. Database is ready."

# Start the application

npm run dev
39 changes: 30 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"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",
Expand All @@ -56,6 +57,7 @@
"@types/jsonwebtoken": "^9.0.2",
"@types/multer": "^1.4.11",
"@types/node": "^20.1.4",
"@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
8 changes: 8 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 { ReminderCronService } from './services/cron/reminder.cron'

const app = express()
const staticFolder = 'uploads'
Expand Down Expand Up @@ -74,4 +75,11 @@ export const startServer = async (port: number): Promise<Express> => {
}
}

const reminderCron = new ReminderCronService()
reminderCron.start()

process.on('SIGTERM', () => {
reminderCron.stop()
})

export default startServer
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 './reminder.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/reminder.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',
SENT = 'sent',
FAILED = 'failed',
SCHEDULED = 'scheduled',
COMPLETED = 'completed',
SENDING = 'sending'
}
13 changes: 0 additions & 13 deletions src/migrations/1727636762101-monthlychecking.ts

This file was deleted.

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

export class Reminders1733219895951 implements MigrationInterface {
name = 'Reminders1733219895951'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "monthly-check-in" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "title" text NOT NULL, "generalUpdatesAndFeedback" text NOT NULL, "progressTowardsGoals" text NOT NULL, "mediaContentLinks" json, "mentorFeedback" text, "isCheckedByMentor" boolean NOT NULL DEFAULT false, "mentorCheckedDate" TIMESTAMP, "checkInDate" TIMESTAMP NOT NULL DEFAULT now(), "menteeId" uuid, CONSTRAINT "PK_44f1414b858e3eb6b8aacee7fbe" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TYPE "public"."monthly_reminders_status_enum" AS ENUM('pending', 'sent', 'failed', 'scheduled', 'completed', 'sending')`
)
await queryRunner.query(
`CREATE TABLE "monthly_reminders" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "status" "public"."monthly_reminders_status_enum" NOT NULL DEFAULT 'pending', "lastError" text, "remindersSent" integer NOT NULL DEFAULT '0', "nextReminderDate" TIMESTAMP, "lastSentDate" TIMESTAMP, "menteeUuid" uuid, CONSTRAINT "PK_f3b22313af4df0cb9cf9c41c681" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TYPE "public"."mentee_state_enum" AS ENUM('pending', 'rejected', 'approved', 'completed', 'revoked')`
)
await queryRunner.query(
`CREATE TYPE "public"."mentee_status_updated_by_enum" AS ENUM('admin', 'mentor')`
)
await queryRunner.query(
`CREATE TABLE "mentee" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "state" "public"."mentee_state_enum" NOT NULL DEFAULT 'pending', "status_updated_by" "public"."mentee_status_updated_by_enum", "status_updated_date" TIMESTAMP, "application" json NOT NULL, "certificate_id" uuid, "journal" character varying, "profileUuid" uuid, "mentorUuid" uuid, CONSTRAINT "PK_694f9d45c4a2a5bd2e4158df6a8" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TYPE "public"."profile_type_enum" AS ENUM('default', 'admin')`
)
await queryRunner.query(
`CREATE TABLE "profile" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "primary_email" character varying(255) NOT NULL, "first_name" character varying(255) NOT NULL, "last_name" character varying(255), "image_url" character varying(255) NOT NULL, "type" "public"."profile_type_enum" NOT NULL DEFAULT 'default', "password" character varying(255) NOT NULL, CONSTRAINT "UQ_def641b0892f8d810007e362eb0" UNIQUE ("primary_email"), CONSTRAINT "PK_fab5f83a1cc8ebe0076c733fd85" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TABLE "country" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "code" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_4e06beff3ecfb1a974312fe536d" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TYPE "public"."mentor_state_enum" AS ENUM('pending', 'rejected', 'approved')`
)
await queryRunner.query(
`CREATE TABLE "mentor" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "state" "public"."mentor_state_enum" NOT NULL DEFAULT 'pending', "application" json NOT NULL, "availability" boolean NOT NULL, "categoryUuid" uuid, "profileUuid" uuid, "countryUuid" uuid, CONSTRAINT "PK_50288edf84756228c143608b2be" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TABLE "category" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "category" character varying(255) NOT NULL, CONSTRAINT "PK_86ee096735ccbfa3fd319af1833" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`CREATE TYPE "public"."email_state_enum" AS ENUM('sent', 'delivered', 'failed')`
)
await queryRunner.query(
`CREATE TABLE "email" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "recipient" character varying(255) NOT NULL, "subject" character varying(255) NOT NULL, "content" character varying NOT NULL, "state" "public"."email_state_enum" NOT NULL, CONSTRAINT "PK_a6db4f191b9a83ca3c8f4149d13" PRIMARY KEY ("uuid"))`
)
await queryRunner.query(
`ALTER TABLE "monthly-check-in" ADD CONSTRAINT "FK_6de642444085d9599d3f260d566" FOREIGN KEY ("menteeId") REFERENCES "mentee"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "monthly_reminders" ADD CONSTRAINT "FK_3c019691242e748da81b48747ff" FOREIGN KEY ("menteeUuid") REFERENCES "mentee"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "mentee" ADD CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7" FOREIGN KEY ("profileUuid") REFERENCES "profile"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "mentee" ADD CONSTRAINT "FK_1fd04826f894fd63a0ce080f6e4" FOREIGN KEY ("mentorUuid") REFERENCES "mentor"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "mentor" ADD CONSTRAINT "FK_59a1e655aa15be5f068a11f0d23" FOREIGN KEY ("categoryUuid") REFERENCES "category"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "mentor" ADD CONSTRAINT "FK_ee49a3436192915d20e07378f16" FOREIGN KEY ("profileUuid") REFERENCES "profile"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "mentor" ADD CONSTRAINT "FK_3302c22eb1636f239d605eb61c3" FOREIGN KEY ("countryUuid") REFERENCES "country"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "mentor" DROP CONSTRAINT "FK_3302c22eb1636f239d605eb61c3"`
)
await queryRunner.query(
`ALTER TABLE "mentor" DROP CONSTRAINT "FK_ee49a3436192915d20e07378f16"`
)
await queryRunner.query(
`ALTER TABLE "mentor" DROP CONSTRAINT "FK_59a1e655aa15be5f068a11f0d23"`
)
await queryRunner.query(
`ALTER TABLE "mentee" DROP CONSTRAINT "FK_1fd04826f894fd63a0ce080f6e4"`
)
await queryRunner.query(
`ALTER TABLE "mentee" DROP CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7"`
)
await queryRunner.query(
`ALTER TABLE "monthly_reminders" DROP CONSTRAINT "FK_3c019691242e748da81b48747ff"`
)
await queryRunner.query(
`ALTER TABLE "monthly-check-in" DROP CONSTRAINT "FK_6de642444085d9599d3f260d566"`
)
await queryRunner.query(`DROP TABLE "email"`)
await queryRunner.query(`DROP TYPE "public"."email_state_enum"`)
await queryRunner.query(`DROP TABLE "category"`)
await queryRunner.query(`DROP TABLE "mentor"`)
await queryRunner.query(`DROP TYPE "public"."mentor_state_enum"`)
await queryRunner.query(`DROP TABLE "country"`)
await queryRunner.query(`DROP TABLE "profile"`)
await queryRunner.query(`DROP TYPE "public"."profile_type_enum"`)
await queryRunner.query(`DROP TABLE "mentee"`)
await queryRunner.query(
`DROP TYPE "public"."mentee_status_updated_by_enum"`
)
await queryRunner.query(`DROP TYPE "public"."mentee_state_enum"`)
await queryRunner.query(`DROP TABLE "monthly_reminders"`)
await queryRunner.query(
`DROP TYPE "public"."monthly_reminders_status_enum"`
)
await queryRunner.query(`DROP TABLE "monthly-check-in"`)
}
}
Loading