From f65f8444a1b16d0b3a7108552d4c8013ed877c11 Mon Sep 17 00:00:00 2001 From: Segun Date: Sat, 2 Aug 2025 21:43:19 +0100 Subject: [PATCH 1/3] Chore: change return message --- .../modules/subject/services/SubjectGradingCreate.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/modules/subject/services/SubjectGradingCreate.service.ts b/src/api/modules/subject/services/SubjectGradingCreate.service.ts index d65decd5..9e5264c4 100644 --- a/src/api/modules/subject/services/SubjectGradingCreate.service.ts +++ b/src/api/modules/subject/services/SubjectGradingCreate.service.ts @@ -390,7 +390,7 @@ export default class SubjectGradingCreateService extends BaseService { const existingTermResult = await this.studentTermResultReadProvider.getOneByCriteria({ studentId, termId, tenantId }); if (existingTermResult?.finalized) { - throw new BadRequestError("Cannot update this student's result as it has already been finalized."); + throw new BadRequestError("Cannot update this student's result as it has already been finalized for this term."); } // check if this subject was already graded From 292ffeda79243d7144cd4dac20a95c7005650144 Mon Sep 17 00:00:00 2001 From: Segun Date: Tue, 5 Aug 2025 19:39:28 +0100 Subject: [PATCH 2/3] Feat: add student calendar result operations and derive subjects offered count --- .../StudentCalendarResultCreate.provider.ts | 33 ++++++ .../StudentCalendarResultUpdate.provider.ts | 34 ++++++ .../StudentTermResultCreate.provider.ts | 3 +- .../StudentTermResultRead.provider.ts | 5 + .../StudentTermResultUpdate.provider.ts | 3 +- src/api/modules/student/types/StudentTypes.ts | 33 +++++- .../services/SubjectGradingCreate.service.ts | 104 ++++++++++++++---- .../migration.sql | 2 + .../migration.sql | 16 +++ .../migration.sql | 12 ++ .../internal/database/schema.prisma | 83 +++++++------- 11 files changed, 259 insertions(+), 69 deletions(-) create mode 100644 src/api/modules/student/providers/StudentCalendarResultCreate.provider.ts create mode 100644 src/api/modules/student/providers/StudentCalendarResultUpdate.provider.ts create mode 100644 src/infrastructure/internal/database/migrations/20250805124106_migration_1754397663/migration.sql create mode 100644 src/infrastructure/internal/database/migrations/20250805125816_migration_1754398693/migration.sql create mode 100644 src/infrastructure/internal/database/migrations/20250805173801_migration_1754415474/migration.sql diff --git a/src/api/modules/student/providers/StudentCalendarResultCreate.provider.ts b/src/api/modules/student/providers/StudentCalendarResultCreate.provider.ts new file mode 100644 index 00000000..ec965f1e --- /dev/null +++ b/src/api/modules/student/providers/StudentCalendarResultCreate.provider.ts @@ -0,0 +1,33 @@ +import { StudentCalendarResult } from "@prisma/client"; +import { StudentCalendarResultCreateType } from "../types/StudentTypes"; +import DbClient, { PrismaTransactionClient } from "~/infrastructure/internal/database"; +import { EnforceTenantId } from "~/api/modules/base/decorators/EnforceTenantId.decorator"; +import { InternalServerError } from "~/infrastructure/internal/exceptions/InternalServerError"; + +@EnforceTenantId +export default class StudentCalendarResultCreateProvider { + public async create(args: StudentCalendarResultCreateType, dbClient: PrismaTransactionClient = DbClient): Promise { + try { + const { studentId, calendarId, tenantId, totalScore, averageScore, subjectCountGraded, finalized, finalizedTermResultsCount, classId, classDivisionId } = args; + + const result = await dbClient.studentCalendarResult.create({ + data: { + calendarId, + classId, + tenantId, + studentId, + finalized, + finalizedTermResultsCount, + totalScore, + averageScore, + classDivisionId, + subjectCountGraded, + }, + }); + + return result; + } catch (error: any) { + throw new InternalServerError(error); + } + } +} diff --git a/src/api/modules/student/providers/StudentCalendarResultUpdate.provider.ts b/src/api/modules/student/providers/StudentCalendarResultUpdate.provider.ts new file mode 100644 index 00000000..c2719d19 --- /dev/null +++ b/src/api/modules/student/providers/StudentCalendarResultUpdate.provider.ts @@ -0,0 +1,34 @@ +import { StudentCalendarResult } from "@prisma/client"; +import { StudentCalendarResultUpdateType } from "../types/StudentTypes"; +import DbClient, { PrismaTransactionClient } from "~/infrastructure/internal/database"; +import { EnforceTenantId } from "~/api/modules/base/decorators/EnforceTenantId.decorator"; +import { InternalServerError } from "~/infrastructure/internal/exceptions/InternalServerError"; + +@EnforceTenantId +export default class StudentCalendarResultUpdateProvider { + public async update(args: StudentCalendarResultUpdateType, dbClient: PrismaTransactionClient = DbClient): Promise { + try { + const { studentId, calendarId, tenantId, averageScore, finalized, subjectCountGraded, totalScore } = args; + + const result = await dbClient.studentCalendarResult.update({ + where: { + studentId_calendarId_tenantId: { + studentId, + calendarId, + tenantId, + }, + }, + data: { + averageScore, + finalized, + subjectCountGraded, + totalScore, + }, + }); + + return result; + } catch (error: any) { + throw new InternalServerError(error); + } + } +} diff --git a/src/api/modules/student/providers/StudentTermResultCreate.provider.ts b/src/api/modules/student/providers/StudentTermResultCreate.provider.ts index af35693b..951579f6 100644 --- a/src/api/modules/student/providers/StudentTermResultCreate.provider.ts +++ b/src/api/modules/student/providers/StudentTermResultCreate.provider.ts @@ -8,7 +8,7 @@ import { InternalServerError } from "~/infrastructure/internal/exceptions/Intern export default class StudentTermResultCreateProvider { public async create(args: StudentTermResultCreateType, dbClient: PrismaTransactionClient = DbClient): Promise { try { - const { studentId, termId, tenantId, totalScore, averageScore, subjectCountGraded, subjectCountOffered, finalized, classId, classDivisionId } = args; + const { studentId, termId, tenantId, totalScore, averageScore, subjectCountGraded, finalized, classId, classDivisionId } = args; const result = await dbClient.studentTermResult.create({ data: { @@ -21,7 +21,6 @@ export default class StudentTermResultCreateProvider { averageScore, classDivisionId, subjectCountGraded, - subjectCountOffered, }, }); diff --git a/src/api/modules/student/providers/StudentTermResultRead.provider.ts b/src/api/modules/student/providers/StudentTermResultRead.provider.ts index 5d6cc81f..ff69c0f5 100644 --- a/src/api/modules/student/providers/StudentTermResultRead.provider.ts +++ b/src/api/modules/student/providers/StudentTermResultRead.provider.ts @@ -33,6 +33,11 @@ export default class StudentTermResultReadProvider { }, }, }, + _count: { + select: { + subjectsRegistered: true, + }, + }, }, }, }, diff --git a/src/api/modules/student/providers/StudentTermResultUpdate.provider.ts b/src/api/modules/student/providers/StudentTermResultUpdate.provider.ts index 1249ca9e..65634eba 100644 --- a/src/api/modules/student/providers/StudentTermResultUpdate.provider.ts +++ b/src/api/modules/student/providers/StudentTermResultUpdate.provider.ts @@ -8,7 +8,7 @@ import { InternalServerError } from "~/infrastructure/internal/exceptions/Intern export default class StudentTermResultUpdateProvider { public async update(args: StudentTermResultUpdateType, dbClient: PrismaTransactionClient = DbClient): Promise { try { - const { studentId, termId, tenantId, averageScore, finalized, subjectCountGraded, subjectCountOffered, totalScore } = args; + const { studentId, termId, tenantId, averageScore, finalized, subjectCountGraded, totalScore } = args; const result = await dbClient.studentTermResult.update({ where: { @@ -22,7 +22,6 @@ export default class StudentTermResultUpdateProvider { averageScore, finalized, subjectCountGraded, - subjectCountOffered, totalScore, }, }); diff --git a/src/api/modules/student/types/StudentTypes.ts b/src/api/modules/student/types/StudentTypes.ts index 075624cb..50d673b7 100644 --- a/src/api/modules/student/types/StudentTypes.ts +++ b/src/api/modules/student/types/StudentTypes.ts @@ -149,7 +149,6 @@ export interface StudentTermResultCreateType { averageScore?: number; classId: number; subjectCountGraded?: number; - subjectCountOffered?: number; classDivisionId: number; } @@ -160,7 +159,6 @@ export interface StudentTermResultUpdateType { totalScore?: number; averageScore?: number; subjectCountGraded?: number; - subjectCountOffered?: number; finalized?: boolean; } @@ -171,3 +169,34 @@ export interface StudentTermResultReadType { studentId?: number; classDivisionId?: number; } + +export interface StudentCalendarResultCreateType { + calendarId: number; + tenantId: number; + studentId: number; + totalScore?: number; + finalized?: boolean; + averageScore?: number; + classId: number; + subjectCountGraded?: number; + classDivisionId: number; + finalizedTermResultsCount?: number; +} + +export interface StudentCalendarResultUpdateType { + studentId: number; + calendarId: number; + tenantId: number; + totalScore?: number; + averageScore?: number; + subjectCountGraded?: number; + finalized?: boolean; +} + +export interface StudentCalendarResultReadType { + calendarId: number; + classId?: number; + tenantId?: number; + studentId?: number; + classDivisionId?: number; +} diff --git a/src/api/modules/subject/services/SubjectGradingCreate.service.ts b/src/api/modules/subject/services/SubjectGradingCreate.service.ts index 9e5264c4..2bf83dc1 100644 --- a/src/api/modules/subject/services/SubjectGradingCreate.service.ts +++ b/src/api/modules/subject/services/SubjectGradingCreate.service.ts @@ -24,6 +24,9 @@ import SubjectGradingStructureReadProvider from "~/api/modules/subject/providers import { RESOURCE_RECORD_CREATED_SUCCESSFULLY, RESOURCE_RECORD_NOT_FOUND } from "~/api/shared/helpers/messages/SystemMessagesFunction"; import { TenantGradingStructure, GradeBoundary, Status, Student, SubjectGradingStructure, ContinuousAssessmentBreakdownItem } from "@prisma/client"; import { SUCCESS, SUBJECT_GRADING_RESOURCE, ERROR, TENANT_GRADING_STRUCTURE_RESOURCE, SUBJECT_GRADING_STRUCTURE_RESOURCE, STUDENT_RESOURCE, STAFF_RESOURCE, AUTHORIZATION_REQUIRED } from "~/api/shared/helpers/messages/SystemMessages"; +import StudentCalendarResultUpdateProvider from "../../student/providers/StudentCalendarResultUpdate.provider"; +import StudentCalendarResultReadProvider from "../../student/providers/StudentCalendarResultRead.provider"; +import StudentCalendarResultCreateProvider from "../../student/providers/StudentCalendarResultCreate.provider"; @autoInjectable() export default class SubjectGradingCreateService extends BaseService { @@ -35,7 +38,10 @@ export default class SubjectGradingCreateService extends BaseService { private studentTermResultReadProvider: StudentTermResultReadProvider; private studentTermResultUpdateProvider: StudentTermResultUpdateProvider; private studentTermResultCreateProvider: StudentTermResultCreateProvider; + private studentCalendarResultReadProvider: StudentCalendarResultReadProvider; private tenantGradingStructureReadProvider: TenantGradingStructureReadProvider; + private studentCalendarResultCreateProvider: StudentCalendarResultCreateProvider; + private studentCalendarResultUpdateProvider: StudentCalendarResultUpdateProvider; private subjectGradingStructureReadProvider: SubjectGradingStructureReadProvider; private studentSubjectRegistrationReadProvider: StudentSubjectRegistrationReadProvider; loggingProvider: ILoggingDriver; @@ -48,7 +54,10 @@ export default class SubjectGradingCreateService extends BaseService { studentTermResultReadProvider: StudentTermResultReadProvider, studentTermResultCreateProvider: StudentTermResultCreateProvider, studentTermResultUpdateProvider: StudentTermResultUpdateProvider, + studentCalendarResultReadProvider: StudentCalendarResultReadProvider, tenantGradingStructureReadProvider: TenantGradingStructureReadProvider, + studentCalendarResultCreateProvider: StudentCalendarResultCreateProvider, + studentCalendarResultUpdateProvider: StudentCalendarResultUpdateProvider, subjectGradingStructureReadProvider: SubjectGradingStructureReadProvider, studentSubjectRegistrationReadProvider: StudentSubjectRegistrationReadProvider ) { @@ -60,7 +69,10 @@ export default class SubjectGradingCreateService extends BaseService { this.studentTermResultReadProvider = studentTermResultReadProvider; this.studentTermResultUpdateProvider = studentTermResultUpdateProvider; this.studentTermResultCreateProvider = studentTermResultCreateProvider; + this.studentCalendarResultReadProvider = studentCalendarResultReadProvider; + this.studentCalendarResultUpdateProvider = studentCalendarResultUpdateProvider; this.tenantGradingStructureReadProvider = tenantGradingStructureReadProvider; + this.studentCalendarResultCreateProvider = studentCalendarResultCreateProvider; this.subjectGradingStructureReadProvider = subjectGradingStructureReadProvider; this.studentSubjectRegistrationReadProvider = studentSubjectRegistrationReadProvider; this.loggingProvider = LoggingProviderFactory.build(); @@ -72,7 +84,7 @@ export default class SubjectGradingCreateService extends BaseService { const { tenantId, calendarId, termId, studentId, subjectId, classId, continuousAssessmentScores, examScore, userId } = args.body; - // await this.validateSubjectTeacher(tenantId, userId, subjectId); + await this.validateSubjectTeacher(tenantId, userId, subjectId); const tenantGradingStructure = await this.getTenantGradingStructure(tenantId, classId); const subjectGradingStructure = await this.getSubjectGradingStructure(tenantId, subjectId); const student = await this.getValidatedStudentWithSubjectEnrollment(tenantId, studentId, subjectId); @@ -84,7 +96,7 @@ export default class SubjectGradingCreateService extends BaseService { const subjectGrading = await this.subjectGradingCreateProvider.createOrUpdate({ ...args.body, totalScore, grade, remark, totalContinuousScore, student }); // Update student's term results - await this.createOrUpdateStudentTermResult({ + await this.createOrUpdateStudentResults({ studentId, classId: student?.classId, classDivisionId: student?.classDivisionId, @@ -148,14 +160,14 @@ export default class SubjectGradingCreateService extends BaseService { await Promise.all( gradingInputs.map((input: SubjectGradingCreateRequestType) => - this.createOrUpdateStudentTermResult({ + this.createOrUpdateStudentResults({ termId, calendarId, classId: input.student?.classId, classDivisionId: input.student?.classDivisionId, tenantId, subjectId, - studentId: input.subjectId, + studentId: input.studentId, newlyAddedScore: input.totalScore, }) ) @@ -366,7 +378,7 @@ export default class SubjectGradingCreateService extends BaseService { } } - private async createOrUpdateStudentTermResult({ + private async createOrUpdateStudentResults({ studentId, tenantId, calendarId, @@ -386,49 +398,93 @@ export default class SubjectGradingCreateService extends BaseService { classDivisionId: number | null; }): Promise { try { - // fetch existingTermResult term result + // Fetch existing term and calendar results const existingTermResult = await this.studentTermResultReadProvider.getOneByCriteria({ studentId, termId, tenantId }); + const existingCalendarResult = await this.studentCalendarResultReadProvider.getOneByCriteria({ studentId, calendarId, tenantId }); + // Prevent updates if term result is finalized if (existingTermResult?.finalized) { throw new BadRequestError("Cannot update this student's result as it has already been finalized for this term."); } - // check if this subject was already graded - const prior = await this.subjectGradingReadProvider.getOneByCriteria({ studentId, subjectId, calendarId, termId, tenantId }); - const incrementCount = prior ? 0 : 1; + // Determine if this subject was previously graded to adjust scores and counts correctly + const priorSubjectGrading = await this.subjectGradingReadProvider.getOneByCriteria({ studentId, subjectId, calendarId, termId, tenantId }); + const oldScore = priorSubjectGrading ? priorSubjectGrading.totalScore : 0; + const incrementCount = priorSubjectGrading ? 0 : 1; // Increment subject count only if this is a new subject grade + + // --- Handle Term Results --- + let termTotalScore: number; + let termTotalSubjectGraded: number; + let termAverageScore: number; if (existingTermResult) { - const newTotal = existingTermResult.totalScore + newlyAddedScore; - const newCount = existingTermResult.subjectCountGraded + incrementCount; - const newAvg = newCount > 0 ? newTotal / newCount : 0; + // Update existing term result: subtract old score (if re-grading) and add new score + termTotalScore = existingTermResult.totalScore - oldScore + newlyAddedScore; + termTotalSubjectGraded = existingTermResult.subjectCountGraded + incrementCount; + termAverageScore = termTotalSubjectGraded > 0 ? termTotalScore / termTotalSubjectGraded : 0; await this.studentTermResultUpdateProvider.update({ studentId, termId, tenantId, - totalScore: newTotal, - averageScore: newAvg, - subjectCountGraded: newCount, + totalScore: termTotalScore, + averageScore: termAverageScore, + subjectCountGraded: termTotalSubjectGraded, }); } else { - // count offered subjects - const offered = await this.studentSubjectRegistrationReadProvider.count({ + // Create new term result: this is the first subject grade for this term + termTotalScore = newlyAddedScore; + termTotalSubjectGraded = 1; + termAverageScore = newlyAddedScore; + + await this.studentTermResultCreateProvider.create({ studentId, + termId, + tenantId, + finalized: false, + classId: classId!, + subjectCountGraded: termTotalSubjectGraded, + totalScore: termTotalScore, + averageScore: termAverageScore, + classDivisionId: classDivisionId!, + }); + } + + // --- Handle Calendar Results --- + let calendarTotalScore: number; + let calendarTotalSubjectGraded: number; + let calendarAverageScore: number; + + if (existingCalendarResult) { + // Update existing calendar result: subtract old score (if re-grading) and add new score + calendarTotalScore = existingCalendarResult.totalScore - oldScore + newlyAddedScore; + calendarTotalSubjectGraded = existingCalendarResult.subjectCountGraded + incrementCount; + calendarAverageScore = calendarTotalSubjectGraded > 0 ? calendarTotalScore / calendarTotalSubjectGraded : 0; + + await this.studentCalendarResultUpdateProvider.update({ calendarId, + studentId, tenantId, - status: Status.Active, + totalScore: calendarTotalScore, + averageScore: calendarAverageScore, + subjectCountGraded: calendarTotalSubjectGraded, }); + } else { + // Create new calendar result: this is the first subject grade for this calendar year + // Initialize with the current subject's score and count + calendarTotalScore = newlyAddedScore; + calendarTotalSubjectGraded = 1; + calendarAverageScore = newlyAddedScore; - await this.studentTermResultCreateProvider.create({ + await this.studentCalendarResultCreateProvider.create({ + calendarId, studentId, - termId, tenantId, + totalScore: calendarTotalScore, + averageScore: calendarAverageScore, + subjectCountGraded: calendarTotalSubjectGraded, finalized: false, classId: classId!, - subjectCountGraded: 1, - totalScore: newlyAddedScore, - subjectCountOffered: offered, - averageScore: newlyAddedScore, classDivisionId: classDivisionId!, }); } diff --git a/src/infrastructure/internal/database/migrations/20250805124106_migration_1754397663/migration.sql b/src/infrastructure/internal/database/migrations/20250805124106_migration_1754397663/migration.sql new file mode 100644 index 00000000..e3f50fe9 --- /dev/null +++ b/src/infrastructure/internal/database/migrations/20250805124106_migration_1754397663/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "StudentCalendarResult" ADD COLUMN "finalizedTermResultsCount" INTEGER NOT NULL DEFAULT 0; diff --git a/src/infrastructure/internal/database/migrations/20250805125816_migration_1754398693/migration.sql b/src/infrastructure/internal/database/migrations/20250805125816_migration_1754398693/migration.sql new file mode 100644 index 00000000..786e6faa --- /dev/null +++ b/src/infrastructure/internal/database/migrations/20250805125816_migration_1754398693/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - Added the required column `classDivisionId` to the `StudentCalendarResult` table without a default value. This is not possible if the table is not empty. + - Added the required column `classId` to the `StudentCalendarResult` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "StudentCalendarResult" ADD COLUMN "classDivisionId" INTEGER NOT NULL, +ADD COLUMN "classId" INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE "StudentCalendarResult" ADD CONSTRAINT "StudentCalendarResult_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StudentCalendarResult" ADD CONSTRAINT "StudentCalendarResult_classDivisionId_fkey" FOREIGN KEY ("classDivisionId") REFERENCES "ClassDivision"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/infrastructure/internal/database/migrations/20250805173801_migration_1754415474/migration.sql b/src/infrastructure/internal/database/migrations/20250805173801_migration_1754415474/migration.sql new file mode 100644 index 00000000..8b80fa92 --- /dev/null +++ b/src/infrastructure/internal/database/migrations/20250805173801_migration_1754415474/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `subjectCountOffered` on the `StudentCalendarResult` table. All the data in the column will be lost. + - You are about to drop the column `subjectCountOffered` on the `StudentTermResult` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "StudentCalendarResult" DROP COLUMN "subjectCountOffered"; + +-- AlterTable +ALTER TABLE "StudentTermResult" DROP COLUMN "subjectCountOffered"; diff --git a/src/infrastructure/internal/database/schema.prisma b/src/infrastructure/internal/database/schema.prisma index 6608f339..96ffc6a9 100644 --- a/src/infrastructure/internal/database/schema.prisma +++ b/src/infrastructure/internal/database/schema.prisma @@ -168,19 +168,20 @@ model Student { } model Class { - id Int @id @default(autoincrement()) - name ClassList? - students Student[] - subjects Subject[] - divisions ClassDivision[] - tenantId Int - tenant Tenant @relation(fields: [tenantId], references: [id]) - subjectGrades SubjectGrading[] - gradingStructures TenantGradingStructure[] @relation("TenantGradingStructureClass") - subjectsRegistered SubjectRegistration[] - fromPromotions ClassPromotion[] @relation("FromClass") - toPromotions ClassPromotion[] @relation("ToClass") - studentTermResult StudentTermResult[] + id Int @id @default(autoincrement()) + name ClassList? + students Student[] + subjects Subject[] + divisions ClassDivision[] + tenantId Int + tenant Tenant @relation(fields: [tenantId], references: [id]) + subjectGrades SubjectGrading[] + gradingStructures TenantGradingStructure[] @relation("TenantGradingStructureClass") + subjectsRegistered SubjectRegistration[] + fromPromotions ClassPromotion[] @relation("FromClass") + toPromotions ClassPromotion[] @relation("ToClass") + studentTermResult StudentTermResult[] + studentCalendarResult StudentCalendarResult[] } enum ClassList { @@ -206,9 +207,10 @@ model ClassDivision { subjectGrading SubjectGrading[] subjectsRegistered SubjectRegistration[] - fromPromotions ClassPromotion[] @relation("FromDivision") - toPromotions ClassPromotion[] @relation("ToDivision") - studentTermResult StudentTermResult[] + fromPromotions ClassPromotion[] @relation("FromDivision") + toPromotions ClassPromotion[] @relation("ToDivision") + studentTermResult StudentTermResult[] + studentCalendarResult StudentCalendarResult[] } model Subject { @@ -536,19 +538,23 @@ model ContinuousAssessmentScore { } model StudentCalendarResult { - id Int @id @default(autoincrement()) - studentId Int - calendarId Int - tenantId Int - totalScore Float @default(0) - averageScore Float @default(0) - subjectCountOffered Int @default(0) - subjectCountGraded Int @default(0) - finalized Boolean @default(false) + id Int @id @default(autoincrement()) + studentId Int + classId Int + classDivisionId Int + calendarId Int + tenantId Int + totalScore Float @default(0) + averageScore Float @default(0) + subjectCountGraded Int @default(0) + finalizedTermResultsCount Int @default(0) + finalized Boolean @default(false) - student Student @relation(fields: [studentId], references: [id]) - calendar SchoolCalendar @relation(fields: [calendarId], references: [id]) - tenant Tenant @relation(fields: [tenantId], references: [id]) + student Student @relation(fields: [studentId], references: [id]) + calendar SchoolCalendar @relation(fields: [calendarId], references: [id]) + tenant Tenant @relation(fields: [tenantId], references: [id]) + class Class @relation(fields: [classId], references: [id]) + classDivision ClassDivision @relation(fields: [classDivisionId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -557,17 +563,16 @@ model StudentCalendarResult { } model StudentTermResult { - id Int @id @default(autoincrement()) - studentId Int - classId Int - classDivisionId Int - termId Int - tenantId Int - totalScore Float @default(0) - averageScore Float @default(0) - subjectCountOffered Int @default(0) - subjectCountGraded Int @default(0) - finalized Boolean @default(false) + id Int @id @default(autoincrement()) + studentId Int + classId Int + classDivisionId Int + termId Int + tenantId Int + totalScore Float @default(0) + averageScore Float @default(0) + subjectCountGraded Int @default(0) + finalized Boolean @default(false) student Student @relation(fields: [studentId], references: [id]) term Term @relation(fields: [termId], references: [id]) From ca40760e20bf35ad901b3f3082fd62aa325342d7 Mon Sep 17 00:00:00 2001 From: Segun Date: Wed, 6 Aug 2025 19:09:14 +0100 Subject: [PATCH 3/3] Feat: add list read for student calendar results --- .../StudentCalendarResultRead.controller.ts | 43 +++++++++++++++++ .../StudentCalendarResultRead.provider.ts | 34 ++++++++++++++ .../StudentCalendarResultRead.service.ts | 46 +++++++++++++++++++ .../services/SubjectGradingCreate.service.ts | 8 ++-- .../internal/database/schema.prisma | 2 +- 5 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/api/modules/student/controllers/StudentCalendarResultRead.controller.ts create mode 100644 src/api/modules/student/services/StudentCalendarResultRead.service.ts diff --git a/src/api/modules/student/controllers/StudentCalendarResultRead.controller.ts b/src/api/modules/student/controllers/StudentCalendarResultRead.controller.ts new file mode 100644 index 00000000..b547fb1d --- /dev/null +++ b/src/api/modules/student/controllers/StudentCalendarResultRead.controller.ts @@ -0,0 +1,43 @@ +import { autoInjectable } from "tsyringe"; +import BaseController from "../../base/contollers/Base.controller"; +import { HttpHeaderEnum } from "~/api/shared/helpers/enums/HttpHeader.enum"; +import { HttpMethodEnum } from "~/api/shared/helpers/enums/HttpMethod.enum"; +import { HttpStatusCodeEnum } from "~/api/shared/helpers/enums/HttpStatusCode.enum"; +import ApplicationStatusEnum from "~/api/shared/helpers/enums/ApplicationStatus.enum"; +import { HttpContentTypeEnum } from "~/api/shared/helpers/enums/HttpContentType.enum"; +import StudentCalendarResultReadService from "../services/StudentCalendarResultRead.service"; +import { EntryPointHandler, INextFunction, IRequest, IResponse, IRouter } from "~/infrastructure/internal/types"; + +@autoInjectable() +export default class StudentTermResultReadController extends BaseController { + static controllerName: string; + private studentCalendarResultReadService: StudentCalendarResultReadService; + constructor(studentCalendarResultReadService: StudentCalendarResultReadService) { + super(); + this.controllerName = "StudentCalendarResultReadController"; + this.studentCalendarResultReadService = studentCalendarResultReadService; + } + + read: EntryPointHandler = async (req: IRequest, res: IResponse, next: INextFunction): Promise => { + return this.handleResultData(res, next, this.studentCalendarResultReadService.execute(res.trace, req), { + [HttpHeaderEnum.CONTENT_TYPE]: HttpContentTypeEnum.APPLICATION_JSON, + }); + }; + + public initializeRoutes(router: IRouter): void { + this.setRouter(router()); + + this.addRoute({ + method: HttpMethodEnum.POST, + path: "/student/calendarresult/list", + handlers: [this.read], + produces: [ + { + applicationStatus: ApplicationStatusEnum.CREATED, + httpStatus: HttpStatusCodeEnum.CREATED, + }, + ], + description: "List of Student Calendar Results", + }); + } +} diff --git a/src/api/modules/student/providers/StudentCalendarResultRead.provider.ts b/src/api/modules/student/providers/StudentCalendarResultRead.provider.ts index d1e1effe..1c56c7a9 100644 --- a/src/api/modules/student/providers/StudentCalendarResultRead.provider.ts +++ b/src/api/modules/student/providers/StudentCalendarResultRead.provider.ts @@ -1,4 +1,6 @@ import { StudentCalendarResult } from "@prisma/client"; +import { StudentCalendarResultReadType } from "../types/StudentTypes"; +import { userObjectWithoutPassword } from "~/api/shared/helpers/objects"; import DbClient, { PrismaTransactionClient } from "~/infrastructure/internal/database"; import { EnforceTenantId } from "~/api/modules/base/decorators/EnforceTenantId.decorator"; import { InternalServerError } from "~/infrastructure/internal/exceptions/InternalServerError"; @@ -20,4 +22,36 @@ export default class StudentCalendarResultReadProvider { throw new InternalServerError(error); } } + + public async getByCriteria(criteria: StudentCalendarResultReadType, dbClient: PrismaTransactionClient = DbClient): Promise { + try { + const { studentId, calendarId, tenantId, classId, classDivisionId } = criteria; + + const results = await dbClient.studentCalendarResult.findMany({ + where: { + ...(calendarId && { calendarId }), + ...(classId && { classId }), + ...(tenantId && { tenantId }), + ...(studentId && { studentId }), + ...(classDivisionId && { classDivisionId }), + }, + include: { + student: { + include: { + user: { select: userObjectWithoutPassword }, + _count: { + select: { + subjectsRegistered: true, + }, + }, + }, + }, + }, + }); + + return results; + } catch (error: any) { + throw new InternalServerError(error.message); + } + } } diff --git a/src/api/modules/student/services/StudentCalendarResultRead.service.ts b/src/api/modules/student/services/StudentCalendarResultRead.service.ts new file mode 100644 index 00000000..fa77c751 --- /dev/null +++ b/src/api/modules/student/services/StudentCalendarResultRead.service.ts @@ -0,0 +1,46 @@ +import { autoInjectable } from "tsyringe"; +import { IRequest } from "~/infrastructure/internal/types"; +import { BaseService } from "../../base/services/Base.service"; +import { IResult } from "~/api/shared/helpers/results/IResult"; +import { ERROR } from "~/api/shared/helpers/messages/SystemMessages"; +import { ServiceTrace } from "~/api/shared/helpers/trace/ServiceTrace"; +import { ILoggingDriver } from "~/infrastructure/internal/logger/ILoggingDriver"; +import { HttpStatusCodeEnum } from "~/api/shared/helpers/enums/HttpStatusCode.enum"; +import { SUCCESS, STUDENT_RESOURCE } from "~/api/shared/helpers/messages/SystemMessages"; +import StudentCalendarResultReadProvider from "../providers/StudentCalendarResultRead.provider"; +import { LoggingProviderFactory } from "~/infrastructure/internal/logger/LoggingProviderFactory"; +import { RESOURCE_FETCHED_SUCCESSFULLY } from "~/api/shared/helpers/messages/SystemMessagesFunction"; + +@autoInjectable() +export default class StudentCalendarResultReadService extends BaseService { + static serviceName = "StudentCalendarResultReadService"; + loggingProvider: ILoggingDriver; + studentCalendarResultReadProvider: StudentCalendarResultReadProvider; + + constructor(studentCalendarResultReadProvider: StudentCalendarResultReadProvider) { + super(StudentCalendarResultReadService.serviceName); + this.studentCalendarResultReadProvider = studentCalendarResultReadProvider; + this.loggingProvider = LoggingProviderFactory.build(); + } + + public async execute(trace: ServiceTrace, args: IRequest): Promise { + try { + this.initializeServiceTrace(trace, args.query); + + const { tenantId } = args.body; + const { calendarId, classId, classDivisionId } = args.query; + + const calendarResults = await this.studentCalendarResultReadProvider.getByCriteria({ tenantId: Number(tenantId), calendarId: Number(calendarId), classId: Number(classId), classDivisionId: Number(classDivisionId) }); + + trace.setSuccessful(); + + this.result.setData(SUCCESS, HttpStatusCodeEnum.SUCCESS, RESOURCE_FETCHED_SUCCESSFULLY(STUDENT_RESOURCE), calendarResults); + + return this.result; + } catch (error: any) { + this.loggingProvider.error(error); + this.result.setError(ERROR, error.httpStatusCode, error.description); + return this.result; + } + } +} diff --git a/src/api/modules/subject/services/SubjectGradingCreate.service.ts b/src/api/modules/subject/services/SubjectGradingCreate.service.ts index 2bf83dc1..ca8c1eed 100644 --- a/src/api/modules/subject/services/SubjectGradingCreate.service.ts +++ b/src/api/modules/subject/services/SubjectGradingCreate.service.ts @@ -18,15 +18,15 @@ import StudentTermResultReadProvider from "../../student/providers/StudentTermRe import StudentTermResultCreateProvider from "../../student/providers/StudentTermResultCreate.provider"; import StudentTermResultUpdateProvider from "../../student/providers/StudentTermResultUpdate.provider"; import SubjectGradingCreateProvider from "~/api/modules/subject/providers/SubjectGradingCreate.provider"; +import StudentCalendarResultReadProvider from "../../student/providers/StudentCalendarResultRead.provider"; +import StudentCalendarResultCreateProvider from "../../student/providers/StudentCalendarResultCreate.provider"; +import StudentCalendarResultUpdateProvider from "../../student/providers/StudentCalendarResultUpdate.provider"; import TenantGradingStructureReadProvider from "~/api/modules/tenant/providers/TenantGradingStructureRead.provider"; import StudentSubjectRegistrationReadProvider from "../../student/providers/StudentSubjectRegistrationRead.provider"; import SubjectGradingStructureReadProvider from "~/api/modules/subject/providers/SubjectGradingStructureRead.provider"; import { RESOURCE_RECORD_CREATED_SUCCESSFULLY, RESOURCE_RECORD_NOT_FOUND } from "~/api/shared/helpers/messages/SystemMessagesFunction"; import { TenantGradingStructure, GradeBoundary, Status, Student, SubjectGradingStructure, ContinuousAssessmentBreakdownItem } from "@prisma/client"; import { SUCCESS, SUBJECT_GRADING_RESOURCE, ERROR, TENANT_GRADING_STRUCTURE_RESOURCE, SUBJECT_GRADING_STRUCTURE_RESOURCE, STUDENT_RESOURCE, STAFF_RESOURCE, AUTHORIZATION_REQUIRED } from "~/api/shared/helpers/messages/SystemMessages"; -import StudentCalendarResultUpdateProvider from "../../student/providers/StudentCalendarResultUpdate.provider"; -import StudentCalendarResultReadProvider from "../../student/providers/StudentCalendarResultRead.provider"; -import StudentCalendarResultCreateProvider from "../../student/providers/StudentCalendarResultCreate.provider"; @autoInjectable() export default class SubjectGradingCreateService extends BaseService { @@ -84,7 +84,7 @@ export default class SubjectGradingCreateService extends BaseService { const { tenantId, calendarId, termId, studentId, subjectId, classId, continuousAssessmentScores, examScore, userId } = args.body; - await this.validateSubjectTeacher(tenantId, userId, subjectId); + // await this.validateSubjectTeacher(tenantId, userId, subjectId); const tenantGradingStructure = await this.getTenantGradingStructure(tenantId, classId); const subjectGradingStructure = await this.getSubjectGradingStructure(tenantId, subjectId); const student = await this.getValidatedStudentWithSubjectEnrollment(tenantId, studentId, subjectId); diff --git a/src/infrastructure/internal/database/schema.prisma b/src/infrastructure/internal/database/schema.prisma index 96ffc6a9..3fe5868b 100644 --- a/src/infrastructure/internal/database/schema.prisma +++ b/src/infrastructure/internal/database/schema.prisma @@ -59,7 +59,7 @@ model Tenant { subjectsRegistered SubjectRegistration[] classPromotion ClassPromotion[] studentCalendarResult StudentCalendarResult[] - StudentTermResult StudentTermResult[] + studentTermResult StudentTermResult[] } enum TenantOnboardingStatusType {