From 336f904313c1668282fb2c85509c414a464de446 Mon Sep 17 00:00:00 2001 From: Segun Date: Sun, 27 Jul 2025 20:18:35 +0100 Subject: [PATCH] Feat: student term result --- .../StudentTermResultRead.controller.ts | 43 +++++++++++++++++ .../StudentTermResultCreateProvider.ts | 4 +- .../StudentTermResultRead.provider.ts | 37 ++++++++++++-- .../services/StudentTermResultRead.service.ts | 46 ++++++++++++++++++ src/api/modules/student/types/StudentTypes.ts | 6 ++- .../services/SubjectGradingCreate.service.ts | 48 ++++++++++++++----- .../migration.sql | 11 +++++ .../migration.sql | 9 ++++ .../migration.sql | 22 +++++++++ .../internal/database/schema.prisma | 25 +++++----- 10 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 src/api/modules/student/controllers/StudentTermResultRead.controller.ts create mode 100644 src/api/modules/student/services/StudentTermResultRead.service.ts create mode 100644 src/infrastructure/internal/database/migrations/20250727154404_migration_1753631040/migration.sql create mode 100644 src/infrastructure/internal/database/migrations/20250727162331_migration_1753633408/migration.sql create mode 100644 src/infrastructure/internal/database/migrations/20250727162438_migration_1753633476/migration.sql diff --git a/src/api/modules/student/controllers/StudentTermResultRead.controller.ts b/src/api/modules/student/controllers/StudentTermResultRead.controller.ts new file mode 100644 index 00000000..1db99e06 --- /dev/null +++ b/src/api/modules/student/controllers/StudentTermResultRead.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 StudentTermResultReadService from "../services/StudentTermResultRead.service"; +import ApplicationStatusEnum from "~/api/shared/helpers/enums/ApplicationStatus.enum"; +import { HttpContentTypeEnum } from "~/api/shared/helpers/enums/HttpContentType.enum"; +import { EntryPointHandler, INextFunction, IRequest, IResponse, IRouter } from "~/infrastructure/internal/types"; + +@autoInjectable() +export default class StudentTermResultReadController extends BaseController { + static controllerName: string; + private studentTermResultReadService: StudentTermResultReadService; + constructor(StudentTermResultReadService: StudentTermResultReadService) { + super(); + this.controllerName = "StudentTermResultReadController"; + this.studentTermResultReadService = StudentTermResultReadService; + } + + read: EntryPointHandler = async (req: IRequest, res: IResponse, next: INextFunction): Promise => { + return this.handleResultData(res, next, this.studentTermResultReadService.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/termresult/list", + handlers: [this.read], + produces: [ + { + applicationStatus: ApplicationStatusEnum.CREATED, + httpStatus: HttpStatusCodeEnum.CREATED, + }, + ], + description: "List of Student Term Results", + }); + } +} diff --git a/src/api/modules/student/providers/StudentTermResultCreateProvider.ts b/src/api/modules/student/providers/StudentTermResultCreateProvider.ts index ed56c6ca..af35693b 100644 --- a/src/api/modules/student/providers/StudentTermResultCreateProvider.ts +++ b/src/api/modules/student/providers/StudentTermResultCreateProvider.ts @@ -8,16 +8,18 @@ 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 } = args; + const { studentId, termId, tenantId, totalScore, averageScore, subjectCountGraded, subjectCountOffered, finalized, classId, classDivisionId } = args; const result = await dbClient.studentTermResult.create({ data: { termId, + classId, tenantId, studentId, finalized, totalScore, 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 81037207..5d6cc81f 100644 --- a/src/api/modules/student/providers/StudentTermResultRead.provider.ts +++ b/src/api/modules/student/providers/StudentTermResultRead.provider.ts @@ -1,5 +1,6 @@ import { StudentTermResult } from "@prisma/client"; import { StudentTermResultReadType } 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"; @@ -8,13 +9,32 @@ import { InternalServerError } from "~/infrastructure/internal/exceptions/Intern export default class StudentTermResultReadProvider { public async getByCriteria(criteria: StudentTermResultReadType, dbClient: PrismaTransactionClient = DbClient): Promise { try { - const { studentId, termId, tenantId } = criteria; + const { studentId, termId, tenantId, classId, classDivisionId } = criteria; const results = await dbClient.studentTermResult.findMany({ where: { - ...(studentId && { studentId }), ...(termId && { termId }), + ...(classId && { classId }), ...(tenantId && { tenantId }), + ...(studentId && { studentId }), + ...(classDivisionId && { classDivisionId }), + }, + include: { + student: { + include: { + user: { select: userObjectWithoutPassword }, + subjectGrades: { + include: { + subject: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }, }, }); @@ -26,13 +46,22 @@ export default class StudentTermResultReadProvider { public async getOneByCriteria(criteria: StudentTermResultReadType, dbClient: PrismaTransactionClient = DbClient): Promise { try { - const { studentId, termId, tenantId } = criteria; + const { studentId, termId, tenantId, classId, classDivisionId } = criteria; const result = await dbClient.studentTermResult.findFirst({ where: { - ...(studentId && { studentId }), ...(termId && { termId }), + ...(classId && { classId }), ...(tenantId && { tenantId }), + ...(studentId && { studentId }), + ...(classDivisionId && { classDivisionId }), + }, + include: { + student: { + include: { + user: { select: userObjectWithoutPassword }, + }, + }, }, }); diff --git a/src/api/modules/student/services/StudentTermResultRead.service.ts b/src/api/modules/student/services/StudentTermResultRead.service.ts new file mode 100644 index 00000000..47c71a46 --- /dev/null +++ b/src/api/modules/student/services/StudentTermResultRead.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 StudentTermResultReadProvider from "../providers/StudentTermResultRead.provider"; +import { SUCCESS, STUDENT_RESOURCE } from "~/api/shared/helpers/messages/SystemMessages"; +import { LoggingProviderFactory } from "~/infrastructure/internal/logger/LoggingProviderFactory"; +import { RESOURCE_FETCHED_SUCCESSFULLY } from "~/api/shared/helpers/messages/SystemMessagesFunction"; + +@autoInjectable() +export default class StudentTermResultReadService extends BaseService { + static serviceName = "StudentTermResultReadService"; + loggingProvider: ILoggingDriver; + studentTermResultReadProvider: StudentTermResultReadProvider; + + constructor(studentTermResultReadProvider: StudentTermResultReadProvider) { + super(StudentTermResultReadService.serviceName); + this.studentTermResultReadProvider = studentTermResultReadProvider; + this.loggingProvider = LoggingProviderFactory.build(); + } + + public async execute(trace: ServiceTrace, args: IRequest): Promise { + try { + this.initializeServiceTrace(trace, args.query); + + const { tenantId } = args.body; + const { termId, classId, classDivisionId } = args.query; + + const termResults = await this.studentTermResultReadProvider.getByCriteria({ tenantId: Number(tenantId), termId: Number(termId), classId: Number(classId), classDivisionId: Number(classDivisionId) }); + + trace.setSuccessful(); + + this.result.setData(SUCCESS, HttpStatusCodeEnum.SUCCESS, RESOURCE_FETCHED_SUCCESSFULLY(STUDENT_RESOURCE), termResults); + + 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/student/types/StudentTypes.ts b/src/api/modules/student/types/StudentTypes.ts index 5ee229ae..075624cb 100644 --- a/src/api/modules/student/types/StudentTypes.ts +++ b/src/api/modules/student/types/StudentTypes.ts @@ -147,8 +147,10 @@ export interface StudentTermResultCreateType { totalScore?: number; finalized?: boolean; averageScore?: number; + classId: number; subjectCountGraded?: number; subjectCountOffered?: number; + classDivisionId: number; } export interface StudentTermResultUpdateType { @@ -163,7 +165,9 @@ export interface StudentTermResultUpdateType { } export interface StudentTermResultReadType { - studentId?: number; termId?: 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 78e0bd90..34155986 100644 --- a/src/api/modules/subject/services/SubjectGradingCreate.service.ts +++ b/src/api/modules/subject/services/SubjectGradingCreate.service.ts @@ -86,6 +86,8 @@ export default class SubjectGradingCreateService extends BaseService { // Update student's term results await this.createOrUpdateStudentTermResult({ studentId, + classId: student?.classId, + classDivisionId: student?.classDivisionId, tenantId, calendarId, termId, @@ -149,6 +151,8 @@ export default class SubjectGradingCreateService extends BaseService { this.createOrUpdateStudentTermResult({ termId, calendarId, + classId: input.student?.classId, + classDivisionId: input.student?.classDivisionId, tenantId, subjectId, studentId: input.subjectId, @@ -362,12 +366,30 @@ export default class SubjectGradingCreateService extends BaseService { } } - private async createOrUpdateStudentTermResult({ studentId, tenantId, calendarId, termId, subjectId, newlyAddedScore }: { studentId: number; tenantId: number; calendarId: number; termId: number; subjectId: number; newlyAddedScore: number }): Promise { + private async createOrUpdateStudentTermResult({ + studentId, + tenantId, + calendarId, + termId, + subjectId, + newlyAddedScore, + classId, + classDivisionId, + }: { + studentId: number; + tenantId: number; + calendarId: number; + termId: number; + subjectId: number; + newlyAddedScore: number; + classId: number | null; + classDivisionId: number | null; + }): Promise { try { - // fetch existing term result - const existing = await this.studentTermResultReadProvider.getOneByCriteria({ studentId, termId, tenantId }); + // fetch existingTermResult term result + const existingTermResult = await this.studentTermResultReadProvider.getOneByCriteria({ studentId, termId, tenantId }); - if (existing?.finalized) { + if (existingTermResult?.finalized) { throw new BadRequestError("Cannot update this student's result as it has already been finalized."); } @@ -375,9 +397,11 @@ export default class SubjectGradingCreateService extends BaseService { const prior = await this.subjectGradingReadProvider.getOneByCriteria({ studentId, subjectId, calendarId, termId, tenantId }); const incrementCount = prior ? 0 : 1; - if (existing) { - const newTotal = existing.totalScore + newlyAddedScore; - const newCount = existing.subjectCountGraded + incrementCount; + console.log("existingTermResult", !!existingTermResult, "newlyAddedScore", newlyAddedScore, "incrementCount", incrementCount, "studentId", studentId, "termId", termId, "tenantId", tenantId); + + if (existingTermResult) { + const newTotal = existingTermResult.totalScore + newlyAddedScore; + const newCount = existingTermResult.subjectCountGraded + incrementCount; const newAvg = newCount > 0 ? newTotal / newCount : 0; await this.studentTermResultUpdateProvider.update({ @@ -401,16 +425,18 @@ export default class SubjectGradingCreateService extends BaseService { studentId, termId, tenantId, - totalScore: newlyAddedScore, - averageScore: newlyAddedScore, + finalized: false, + classId: classId!, subjectCountGraded: 1, + totalScore: newlyAddedScore, subjectCountOffered: offered, - finalized: false, + averageScore: newlyAddedScore, + classDivisionId: classDivisionId!, }); } } catch (error: any) { this.loggingProvider.error(error); - throw new NormalizedAppError(error.message); + throw new NormalizedAppError(error); } } } diff --git a/src/infrastructure/internal/database/migrations/20250727154404_migration_1753631040/migration.sql b/src/infrastructure/internal/database/migrations/20250727154404_migration_1753631040/migration.sql new file mode 100644 index 00000000..1b622187 --- /dev/null +++ b/src/infrastructure/internal/database/migrations/20250727154404_migration_1753631040/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `schoolCalendarId` on the `StudentTermResult` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "StudentTermResult" DROP CONSTRAINT "StudentTermResult_schoolCalendarId_fkey"; + +-- AlterTable +ALTER TABLE "StudentTermResult" DROP COLUMN "schoolCalendarId"; diff --git a/src/infrastructure/internal/database/migrations/20250727162331_migration_1753633408/migration.sql b/src/infrastructure/internal/database/migrations/20250727162331_migration_1753633408/migration.sql new file mode 100644 index 00000000..6a83a9da --- /dev/null +++ b/src/infrastructure/internal/database/migrations/20250727162331_migration_1753633408/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "StudentTermResult" ADD COLUMN "classDivisionId" INTEGER, +ADD COLUMN "classId" INTEGER; + +-- AddForeignKey +ALTER TABLE "StudentTermResult" ADD CONSTRAINT "StudentTermResult_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StudentTermResult" ADD CONSTRAINT "StudentTermResult_classDivisionId_fkey" FOREIGN KEY ("classDivisionId") REFERENCES "ClassDivision"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/infrastructure/internal/database/migrations/20250727162438_migration_1753633476/migration.sql b/src/infrastructure/internal/database/migrations/20250727162438_migration_1753633476/migration.sql new file mode 100644 index 00000000..6419cf44 --- /dev/null +++ b/src/infrastructure/internal/database/migrations/20250727162438_migration_1753633476/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - Made the column `classDivisionId` on table `StudentTermResult` required. This step will fail if there are existing NULL values in that column. + - Made the column `classId` on table `StudentTermResult` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropForeignKey +ALTER TABLE "StudentTermResult" DROP CONSTRAINT "StudentTermResult_classDivisionId_fkey"; + +-- DropForeignKey +ALTER TABLE "StudentTermResult" DROP CONSTRAINT "StudentTermResult_classId_fkey"; + +-- AlterTable +ALTER TABLE "StudentTermResult" ALTER COLUMN "classDivisionId" SET NOT NULL, +ALTER COLUMN "classId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "StudentTermResult" ADD CONSTRAINT "StudentTermResult_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StudentTermResult" ADD CONSTRAINT "StudentTermResult_classDivisionId_fkey" FOREIGN KEY ("classDivisionId") REFERENCES "ClassDivision"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/infrastructure/internal/database/schema.prisma b/src/infrastructure/internal/database/schema.prisma index 41112816..6608f339 100644 --- a/src/infrastructure/internal/database/schema.prisma +++ b/src/infrastructure/internal/database/schema.prisma @@ -180,6 +180,7 @@ model Class { subjectsRegistered SubjectRegistration[] fromPromotions ClassPromotion[] @relation("FromClass") toPromotions ClassPromotion[] @relation("ToClass") + studentTermResult StudentTermResult[] } enum ClassList { @@ -205,8 +206,9 @@ model ClassDivision { subjectGrading SubjectGrading[] subjectsRegistered SubjectRegistration[] - fromPromotions ClassPromotion[] @relation("FromDivision") - toPromotions ClassPromotion[] @relation("ToDivision") + fromPromotions ClassPromotion[] @relation("FromDivision") + toPromotions ClassPromotion[] @relation("ToDivision") + studentTermResult StudentTermResult[] } model Subject { @@ -280,7 +282,6 @@ model SchoolCalendar { subjectsRegistered SubjectRegistration[] classPromotions ClassPromotion[] studentCalendarResult StudentCalendarResult[] - StudentTermResult StudentTermResult[] @@unique([year, tenantId]) } @@ -297,7 +298,7 @@ model Term { tenant Tenant @relation(fields: [tenantId], references: [id]) timetable Timetable[] subjectGrade SubjectGrading[] - StudentTermResult StudentTermResult[] + studentTermResult StudentTermResult[] } model BreakPeriod { @@ -558,6 +559,8 @@ model StudentCalendarResult { model StudentTermResult { id Int @id @default(autoincrement()) studentId Int + classId Int + classDivisionId Int termId Int tenantId Int totalScore Float @default(0) @@ -566,14 +569,14 @@ model StudentTermResult { subjectCountGraded Int @default(0) finalized Boolean @default(false) - student Student @relation(fields: [studentId], references: [id]) - term Term @relation(fields: [termId], references: [id]) - tenant Tenant @relation(fields: [tenantId], references: [id]) + student Student @relation(fields: [studentId], references: [id]) + term Term @relation(fields: [termId], references: [id]) + class Class @relation(fields: [classId], references: [id]) + classDivision ClassDivision @relation(fields: [classDivisionId], references: [id]) + tenant Tenant @relation(fields: [tenantId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - SchoolCalendar SchoolCalendar? @relation(fields: [schoolCalendarId], references: [id]) - schoolCalendarId Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([studentId, termId, tenantId]) }