Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions migrations/1775347200000-migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class Migrations1775347200000 implements MigrationInterface {
name = 'Migrations1775347200000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "cohort_week" ADD "scheduledDate" TIMESTAMP WITH TIME ZONE`,
);

await queryRunner.query(`
UPDATE "cohort_week"
SET "scheduledDate" = c."startDate" + ("cohort_week"."week" * 7) * INTERVAL '1 day'
FROM "cohort" c
WHERE c."id" = "cohort_week"."cohortId"
`);

await queryRunner.query(
`ALTER TABLE "cohort_week" ALTER COLUMN "scheduledDate" SET NOT NULL`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "cohort_week" DROP COLUMN "scheduledDate"`,
);
}
}
3 changes: 1 addition & 2 deletions src/cohort-calendar/cohort-calendar.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ export class CohortCalendarService {
const sortedWeeks = [...cohort.weeks].sort((a, b) => a.week - b.week);

for (const week of sortedWeeks) {
const eventDate = new Date(cohort.startDate);
eventDate.setUTCDate(eventDate.getUTCDate() + week.week * 7);
const eventDate = new Date(week.scheduledDate);
eventDate.setUTCHours(14, 30, 0, 0);

const endDate = new Date(eventDate);
Expand Down
3 changes: 1 addition & 2 deletions src/cohorts/cohort-reminder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@
);
}

const sessionDate = new Date(cohort.startDate);
sessionDate.setUTCDate(sessionDate.getUTCDate() + cohortWeek.week * 7);
const sessionDate = new Date(cohortWeek.scheduledDate);

const usersWithEmail = cohort.users.filter((u) => u.email);

Expand Down Expand Up @@ -235,7 +234,7 @@
user.discordUserName;

await this.mailService.sendCohortFeedbackReminderEmail(
user.email!,

Check warning on line 237 in src/cohorts/cohort-reminder.service.ts

View workflow job for this annotation

GitHub Actions / check-formatting-and-lint

Forbidden non-null assertion

Check warning on line 237 in src/cohorts/cohort-reminder.service.ts

View workflow job for this annotation

GitHub Actions / check-formatting-and-lint

Forbidden non-null assertion
userName,
cohortName,
season,
Expand Down
4 changes: 4 additions & 0 deletions src/cohorts/cohorts.request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export class UpdateCohortWeekRequestDto {
})
@IsNotEmpty()
classroomAssignmentId!: string | undefined;

@IsOptional()
@IsDateString({ strict: true })
scheduledDate!: string | undefined;
}

export class JoinWaitlistRequestDto {
Expand Down
3 changes: 3 additions & 0 deletions src/cohorts/cohorts.response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class GetCohortWeekResponseDto {
bonusQuestion!: string[];
classroomInviteLink!: string | null;
classroomAssignmentUrl!: string | null;
scheduledDate!: string;

constructor(obj: GetCohortWeekResponseDto) {
this.id = obj.id;
Expand All @@ -20,6 +21,7 @@ export class GetCohortWeekResponseDto {
this.bonusQuestion = obj.bonusQuestion;
this.classroomInviteLink = obj.classroomInviteLink;
this.classroomAssignmentUrl = obj.classroomAssignmentUrl;
this.scheduledDate = obj.scheduledDate;
}
}

Expand Down Expand Up @@ -66,6 +68,7 @@ export class GetCohortResponseDto {
bonusQuestion: week.bonusQuestion || [],
classroomInviteLink: week.classroomInviteLink || null,
classroomAssignmentUrl: week.classroomAssignmentUrl ?? null,
scheduledDate: week.scheduledDate.toISOString(),
})),
});
}
Expand Down
128 changes: 103 additions & 25 deletions src/cohorts/cohorts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ConfigService } from '@nestjs/config';
import { CohortType, CohortWeekType } from '@/common/enum';
import { CohortWaitlist } from '@/entities/cohort-waitlist.entity';
import { APITask } from '@/entities/api-task.entity';
import { TaskType } from '@/task-processor/task.enums';
import { APITaskStatus, TaskType } from '@/task-processor/task.enums';
import { MailService } from '@/mail/mail.service';
import { CohortsConfigService } from '@/cohorts/cohorts.config.service';
import { CohortCalendarService } from '@/cohort-calendar/cohort-calendar.service';
Expand Down Expand Up @@ -269,6 +269,12 @@ export class CohortsService {
week.week = weekNumber;
week.cohort = cohort;

const scheduledDate = new Date(startDate);
scheduledDate.setUTCDate(
scheduledDate.getUTCDate() + weekNumber * 7,
);
week.scheduledDate = scheduledDate;

if (weekNumber === 0) {
week.type = CohortWeekType.ORIENTATION;
week.hasExercise = false;
Expand Down Expand Up @@ -298,29 +304,10 @@ export class CohortsService {
apiTask.data = { cohortId: cohort.id };
await manager.save(apiTask);

// Schedule reminder email tasks for each week (except graduation)
const reminderTasks: APITask<TaskType.SEND_COHORT_REMINDER_EMAILS>[] =
cohort.weeks.map((week) => {
const executeOnTime = new Date(startDate);
executeOnTime.setUTCDate(
executeOnTime.getUTCDate() + week.week * 7,
);
// 12:00 PM IST = 06:30 UTC
executeOnTime.setUTCHours(6, 30, 0, 0);

const reminderTask =
new APITask<TaskType.SEND_COHORT_REMINDER_EMAILS>();
reminderTask.type =
TaskType.SEND_COHORT_REMINDER_EMAILS;
reminderTask.data = {
cohortId: cohort.id,
cohortWeekId: week.id,
};
reminderTask.executeOnTime = executeOnTime;

return reminderTask;
});

// Schedule reminder email tasks for each week
const reminderTasks = cohort.weeks.map((week) =>
this.createReminderTask(cohort.id, week),
);
await manager.save(reminderTasks);

// Schedule feedback reminder emails (day after 4th GD session)
Expand Down Expand Up @@ -348,6 +335,7 @@ export class CohortsService {
): Promise<void> {
const cohort: Cohort | null = await this.cohortRepository.findOne({
where: { id: cohortId },
relations: { weeks: true },
});

if (!cohort) {
Expand Down Expand Up @@ -384,6 +372,36 @@ export class CohortsService {
cohortData.startDate &&
cohort.startDate.getTime() !== originalStartDate.getTime()
) {
// Shift all week scheduledDates by the same offset
const offsetMs =
cohort.startDate.getTime() - originalStartDate.getTime();

for (const week of cohort.weeks) {
week.scheduledDate = new Date(
week.scheduledDate.getTime() + offsetMs,
);
}
await manager.save(CohortWeek, cohort.weeks);

// Cancel all unprocessed reminder tasks and recreate with new dates
await manager
.createQueryBuilder()
.update(APITask)
.set({ status: APITaskStatus.CANCELLED })
.where('type = :type', {
type: TaskType.SEND_COHORT_REMINDER_EMAILS,
})
.andWhere('status = :status', {
status: APITaskStatus.UNPROCESSED,
})
.andWhere("data->>'cohortId' = :cohortId", { cohortId })
.execute();

const reminderTasks = cohort.weeks.map((week) =>
this.createReminderTask(cohort.id, week),
);
await manager.save(APITask, reminderTasks);

const apiTask =
new APITask<TaskType.SEND_CALENDAR_UPDATE_EMAILS>();
apiTask.type = TaskType.SEND_CALENDAR_UPDATE_EMAILS;
Expand All @@ -400,6 +418,7 @@ export class CohortsService {
const cohortWeek: CohortWeek | null =
await this.cohortWeekRepository.findOne({
where: { id: cohortWeekId },
relations: { cohort: true },
});

if (!cohortWeek) {
Expand All @@ -419,7 +438,66 @@ export class CohortsService {
cohortWeekData.classroomAssignmentId;
}

await this.cohortWeekRepository.save(cohortWeek);
let scheduledDateChanged = false;

if (cohortWeekData.scheduledDate) {
const scheduledDate = new Date(cohortWeekData.scheduledDate);
scheduledDate.setUTCHours(0, 0, 0, 0);
scheduledDateChanged =
scheduledDate.getTime() !== cohortWeek.scheduledDate.getTime();
cohortWeek.scheduledDate = scheduledDate;
}

await this.dbTransactionService.execute(async (manager) => {
await manager.save(CohortWeek, cohortWeek);

if (scheduledDateChanged) {
// Cancel existing unprocessed reminder task for this week
await manager
.createQueryBuilder()
.update(APITask)
.set({ status: APITaskStatus.CANCELLED })
.where('type = :type', {
type: TaskType.SEND_COHORT_REMINDER_EMAILS,
})
.andWhere('status = :status', {
status: APITaskStatus.UNPROCESSED,
})
.andWhere("data->>'cohortWeekId' = :cohortWeekId", {
cohortWeekId,
})
.execute();

// Create new reminder task with updated date
const reminderTask = this.createReminderTask(
cohortWeek.cohort.id,
cohortWeek,
);
await manager.save(APITask, reminderTask);

// Send calendar update emails
const calendarTask =
new APITask<TaskType.SEND_CALENDAR_UPDATE_EMAILS>();
calendarTask.type = TaskType.SEND_CALENDAR_UPDATE_EMAILS;
calendarTask.data = { cohortId: cohortWeek.cohort.id };
await manager.save(APITask, calendarTask);
}
});
}

private createReminderTask(
cohortId: string,
week: CohortWeek,
): APITask<TaskType.SEND_COHORT_REMINDER_EMAILS> {
const executeOnTime = new Date(week.scheduledDate);
// 12:00 PM IST = 06:30 UTC
executeOnTime.setUTCHours(6, 30, 0, 0);

const task = new APITask<TaskType.SEND_COHORT_REMINDER_EMAILS>();
task.type = TaskType.SEND_COHORT_REMINDER_EMAILS;
task.data = { cohortId, cohortWeekId: week.id };
task.executeOnTime = executeOnTime;
return task;
}

async assignDiscordRole(userId: string, cohortType: CohortType) {
Expand Down
3 changes: 3 additions & 0 deletions src/entities/cohort-week.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export class CohortWeek extends BaseEntity {
@Column('text', { nullable: true })
classroomAssignmentId!: string | null;

@Column({ type: 'timestamp with time zone' })
scheduledDate!: Date;

@ManyToOne(() => Cohort, (c) => c.weeks)
cohort!: Cohort;

Expand Down
Loading