From c3ba0baed814d096af3e260c1260efceb025d95a Mon Sep 17 00:00:00 2001 From: Anmol Sharma Date: Fri, 10 Apr 2026 23:08:05 +0530 Subject: [PATCH] refactor: derive cohort endDate dynamically from cohort weeks Remove the stored endDate column from the Cohort entity and replace it with a getEndDate() method that computes the end date as the latest scheduledDate across cohort weeks. This eliminates redundant stored state and ensures the end date naturally adjusts when individual weeks are postponed. Co-Authored-By: Claude Opus 4.6 (1M context) --- migrations/1775842011163-migrations.ts | 26 +++++++++++++++++++ .../certificates-generation.service.ts | 2 +- src/certificates/certificates.service.ts | 9 ++++--- src/cohorts/cohort-reminder.service.ts | 4 +-- src/cohorts/cohorts.response.dto.ts | 2 +- src/cohorts/cohorts.service.ts | 10 ++----- src/entities/cohort.entity.ts | 11 +++++--- .../github-classroom.service.ts | 2 +- 8 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 migrations/1775842011163-migrations.ts diff --git a/migrations/1775842011163-migrations.ts b/migrations/1775842011163-migrations.ts new file mode 100644 index 0000000..bda7651 --- /dev/null +++ b/migrations/1775842011163-migrations.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Migrations1775842011163 implements MigrationInterface { + name = 'Migrations1775842011163'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "cohort" DROP COLUMN "endDate"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "cohort" ADD "endDate" TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query(` + UPDATE "cohort" c + SET "endDate" = ( + SELECT MAX(cw."scheduledDate") + FROM "cohort_week" cw + WHERE cw."cohortId" = c."id" + ) + `); + await queryRunner.query( + `ALTER TABLE "cohort" ALTER COLUMN "endDate" SET NOT NULL`, + ); + } +} diff --git a/src/certificates/certificates-generation.service.ts b/src/certificates/certificates-generation.service.ts index 5f7e448..10b20f5 100644 --- a/src/certificates/certificates-generation.service.ts +++ b/src/certificates/certificates-generation.service.ts @@ -154,7 +154,7 @@ export class CertificatesGenerationService { certificate.name, certificate.cohort.type, certificate.type, - certificate.cohort.endDate, + certificate.cohort.getEndDate(), certificate.withExercises, certificate.rank, ); diff --git a/src/certificates/certificates.service.ts b/src/certificates/certificates.service.ts index bacfcec..698a14e 100644 --- a/src/certificates/certificates.service.ts +++ b/src/certificates/certificates.service.ts @@ -50,13 +50,14 @@ export class CertificatesService { ): Promise<{ cohort: Cohort; certificateEntities: Certificate[] }> { const cohort = await this.cohortRepository.findOne({ where: { id: cohortId }, + relations: { weeks: true }, }); if (!cohort) { throw new ServiceError(`Cohort with id ${cohortId} not found`); } - if (cohort.endDate > new Date()) { + if (cohort.getEndDate() > new Date()) { throw new BadRequestException( `Cohort with id ${cohortId} has not ended yet. Certificates can only be generated after the cohort ends.`, ); @@ -212,7 +213,7 @@ export class CertificatesService { const certificates = await this.certificateRepository.find({ where: { cohort: { id: cohortId } }, - relations: { cohort: true, user: true }, + relations: { cohort: { weeks: true }, user: true }, }); if (certificates.length === 0) { @@ -279,7 +280,7 @@ export class CertificatesService { const certificate = await this.certificateRepository.findOne({ where: { id }, relations: { - cohort: true, + cohort: { weeks: true }, user: true, }, }); @@ -310,7 +311,7 @@ export class CertificatesService { }> { const certificates = await this.certificateRepository.find({ where: { cohort: { id: cohortId } }, - relations: { cohort: true, user: true }, + relations: { cohort: { weeks: true }, user: true }, }); if (certificates.length === 0) { diff --git a/src/cohorts/cohort-reminder.service.ts b/src/cohorts/cohort-reminder.service.ts index 17183f6..481a513 100644 --- a/src/cohorts/cohort-reminder.service.ts +++ b/src/cohorts/cohort-reminder.service.ts @@ -185,7 +185,7 @@ export class CohortReminderService { const cohort = await this.cohortRepository.findOne({ where: { id: cohortId }, - relations: { users: true }, + relations: { users: true, weeks: true }, }); if (!cohort) { @@ -271,7 +271,7 @@ export class CohortReminderService { const nextExecuteOnTime = new Date(task.executeOnTime); nextExecuteOnTime.setUTCDate(nextExecuteOnTime.getUTCDate() + 7); - const cutoffDate = new Date(cohort.endDate); + const cutoffDate = new Date(cohort.getEndDate()); cutoffDate.setUTCDate(cutoffDate.getUTCDate() + 7); if (nextExecuteOnTime <= cutoffDate) { diff --git a/src/cohorts/cohorts.response.dto.ts b/src/cohorts/cohorts.response.dto.ts index 896a7a7..f0827c6 100644 --- a/src/cohorts/cohorts.response.dto.ts +++ b/src/cohorts/cohorts.response.dto.ts @@ -56,7 +56,7 @@ export class GetCohortResponseDto { type: cohort.type, season: cohort.season, startDate: cohort.startDate.toISOString(), - endDate: cohort.endDate.toISOString(), + endDate: cohort.getEndDate().toISOString(), registrationDeadline: cohort.registrationDeadline.toISOString(), hasExercises: cohort.hasExercises, classroomId: cohort.classroomId ?? null, diff --git a/src/cohorts/cohorts.service.ts b/src/cohorts/cohorts.service.ts index d5d70cb..cfa7726 100644 --- a/src/cohorts/cohorts.service.ts +++ b/src/cohorts/cohorts.service.ts @@ -161,7 +161,7 @@ export class CohortsService { type: cohort.type, season: cohort.season, startDate: cohort.startDate.toISOString(), - endDate: cohort.endDate.toISOString(), + endDate: cohort.getEndDate().toISOString(), registrationDeadline: cohort.registrationDeadline.toISOString(), }), @@ -189,6 +189,7 @@ export class CohortsService { async listPublicCohorts(): Promise { const latestCohorts = await this.cohortRepository .createQueryBuilder('c') + .leftJoinAndSelect('c.weeks', 'weeks') .distinctOn(['c.type']) .orderBy('c.type', 'ASC') .addOrderBy('c.season', 'DESC') @@ -303,7 +304,6 @@ export class CohortsService { cohort.type = cohortData.type; cohort.season = season; cohort.startDate = startDate; - cohort.endDate = endDate; cohort.registrationDeadline = registrationDeadline; cohort.hasExercises = hasExercises; cohort.weeks = []; @@ -411,12 +411,6 @@ export class CohortsService { const startDate = new Date(cohortData.startDate); startDate.setUTCHours(0, 0, 0, 0); cohort.startDate = startDate; - - const config = this.cohortConfigService.getConfig(cohort.type); - const totalWeeks = config.gdSessions + 2; - const endDate = new Date(startDate); - endDate.setUTCDate(endDate.getUTCDate() + totalWeeks * 7); - cohort.endDate = endDate; } if (cohortData.registrationDeadline) { const registrationDeadline = new Date( diff --git a/src/entities/cohort.entity.ts b/src/entities/cohort.entity.ts index d471d02..37c7895 100644 --- a/src/entities/cohort.entity.ts +++ b/src/entities/cohort.entity.ts @@ -34,15 +34,20 @@ export class Cohort extends BaseEntity { @Column('timestamptz') startDate!: Date; - @Column('timestamptz') - endDate!: Date; - @Column('boolean') hasExercises!: boolean; @Column('text', { nullable: true }) classroomId!: string | null; + getEndDate(): Date { + return this.weeks.reduce( + (max, week) => + week.scheduledDate > max ? week.scheduledDate : max, + this.weeks[0].scheduledDate, + ); + } + @ManyToMany(() => User, (u) => u.cohorts) @JoinTable() users!: User[]; diff --git a/src/github-classroom/github-classroom.service.ts b/src/github-classroom/github-classroom.service.ts index baad891..11c65dd 100644 --- a/src/github-classroom/github-classroom.service.ts +++ b/src/github-classroom/github-classroom.service.ts @@ -84,7 +84,7 @@ export class GitHubClassroomService { } const endDatePlusBuffer = new Date( - cohort.endDate.getTime() + TWENTY_FOUR_HOURS_MS, + cohort.getEndDate().getTime() + TWENTY_FOUR_HOURS_MS, ); const hasCohortEnded = endDatePlusBuffer < new Date();