-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat(Enrollment, Course, Route): Get student progress in a course route
- Loading branch information
1 parent
137f60c
commit db70462
Showing
18 changed files
with
383 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
src/domain/course-management/application/use-cases/get-student-progress.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' | ||
import { makeClass } from '../../../../../test/factories/make-class' | ||
import { makeCourse } from '../../../../../test/factories/make-course' | ||
import { makeEnrollment } from '../../../../../test/factories/make-enrollment' | ||
import { makeEnrollmentCompletedItem } from '../../../../../test/factories/make-enrollment-completed-item' | ||
import { makeInstructor } from '../../../../../test/factories/make-instructor' | ||
import { makeModule } from '../../../../../test/factories/make-module' | ||
import { makeStudent } from '../../../../../test/factories/make-student' | ||
import { InMemoryClassesRepository } from '../../../../../test/repositories/in-memory-classes-repository' | ||
import { InMemoryCourseTagsRepository } from '../../../../../test/repositories/in-memory-course-tags-repository' | ||
import { InMemoryCoursesRepository } from '../../../../../test/repositories/in-memory-courses-repository' | ||
import { InMemoryEnrollmentCompletedItemsRepository } from '../../../../../test/repositories/in-memory-enrollment-completed-items-repository' | ||
import { InMemoryEnrollmentsRepository } from '../../../../../test/repositories/in-memory-enrollments-repository' | ||
import { InMemoryInstructorRepository } from '../../../../../test/repositories/in-memory-instructors-repository' | ||
import { InMemoryModulesRepository } from '../../../../../test/repositories/in-memory-modules-repository' | ||
import { InMemoryStudentsRepository } from '../../../../../test/repositories/in-memory-students-repository' | ||
import { GetStudentProgressUseCase } from './get-student-progress' | ||
|
||
let inMemoryEnrollmentCompletedItemsRepository: InMemoryEnrollmentCompletedItemsRepository | ||
let inMemoryEnrollmentsRepository: InMemoryEnrollmentsRepository | ||
let inMemoryCourseTagsRepository: InMemoryCourseTagsRepository | ||
let inMemoryStudentsRepository: InMemoryStudentsRepository | ||
let inMemoryClassesRepository: InMemoryClassesRepository | ||
let inMemoryInstructorsRepository: InMemoryInstructorRepository | ||
let inMemoryModulesRepository: InMemoryModulesRepository | ||
let inMemoryCoursesRepository: InMemoryCoursesRepository | ||
let sut: GetStudentProgressUseCase | ||
|
||
describe('Get student progress use case', () => { | ||
beforeEach(() => { | ||
inMemoryEnrollmentCompletedItemsRepository = new InMemoryEnrollmentCompletedItemsRepository() | ||
inMemoryClassesRepository = new InMemoryClassesRepository() | ||
inMemoryCourseTagsRepository = new InMemoryCourseTagsRepository() | ||
inMemoryInstructorsRepository = new InMemoryInstructorRepository() | ||
inMemoryStudentsRepository = new InMemoryStudentsRepository() | ||
|
||
inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository) | ||
|
||
inMemoryEnrollmentsRepository = new InMemoryEnrollmentsRepository( | ||
inMemoryStudentsRepository, inMemoryEnrollmentCompletedItemsRepository | ||
) | ||
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryInstructorsRepository, inMemoryEnrollmentsRepository, inMemoryStudentsRepository, inMemoryCourseTagsRepository) | ||
|
||
sut = new GetStudentProgressUseCase( | ||
inMemoryEnrollmentsRepository, | ||
inMemoryCoursesRepository, | ||
inMemoryModulesRepository, | ||
inMemoryEnrollmentCompletedItemsRepository | ||
) | ||
}) | ||
|
||
it('should be able to get student progress in a course', async () => { | ||
const instructor = makeInstructor() | ||
await inMemoryInstructorsRepository.create(instructor) | ||
|
||
const course = makeCourse({ name: 'First Course', instructorId: instructor.id }) | ||
await inMemoryCoursesRepository.create(course) | ||
|
||
const module = makeModule({ | ||
name: 'John Doe Module', | ||
courseId: course.id, | ||
moduleNumber: 1 | ||
}) | ||
await inMemoryModulesRepository.create(module) | ||
|
||
const classToAdd = makeClass({ name: 'John Doe Class', moduleId: module.id, classNumber: 1 }) | ||
await inMemoryClassesRepository.create(classToAdd) | ||
|
||
const student = makeStudent() | ||
await inMemoryStudentsRepository.create(student) | ||
|
||
const enrollment = makeEnrollment({ studentId: student.id, courseId: course.id }) | ||
await inMemoryEnrollmentsRepository.create(enrollment) | ||
|
||
const completedItem = makeEnrollmentCompletedItem({ enrollmentId: enrollment.id, itemId: classToAdd.id, type: 'CLASS' }) | ||
await inMemoryEnrollmentCompletedItemsRepository.create(completedItem) | ||
|
||
const result = await sut.exec({ | ||
enrollmentId: enrollment.id.toString(), | ||
studentId: student.id.toString() | ||
}) | ||
|
||
expect(result.isRight()).toBe(true) | ||
expect(result.value).toMatchObject({ | ||
classes: expect.arrayContaining([ | ||
expect.objectContaining({ | ||
class: expect.objectContaining({ | ||
name: 'John Doe Class' | ||
}), | ||
completed: true | ||
}) | ||
]), | ||
modules: expect.arrayContaining([ | ||
expect.objectContaining({ | ||
module: expect.objectContaining({ | ||
name: 'John Doe Module' | ||
}), | ||
completed: false | ||
}) | ||
]) | ||
}) | ||
}) | ||
|
||
it('should not be able to get a student progress from a inexistent enrollment', async () => { | ||
const result = await sut.exec({ | ||
enrollmentId: 'inexistentEnrollmentId', | ||
studentId: 'inexistentStudentId' | ||
}) | ||
|
||
expect(result.isLeft()).toBe(true) | ||
expect(result.value).toBeInstanceOf(ResourceNotFoundError) | ||
}) | ||
}) |
120 changes: 120 additions & 0 deletions
120
src/domain/course-management/application/use-cases/get-student-progress.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { left, right, type Either } from '@/core/either' | ||
import { NotAllowedError } from '@/core/errors/errors/not-allowed-error' | ||
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' | ||
import { type UseCase } from '@/core/use-cases/use-case' | ||
import { type ClassWithStudentProgressDTO } from '../../enterprise/entities/dtos/class-with-student-progress' | ||
import { ClassDtoMapper } from '../../enterprise/entities/dtos/mappers/class-dto-mapper' | ||
import { ModuleDtoMapper } from '../../enterprise/entities/dtos/mappers/module-dto-mapper' | ||
import { type ModuleWithStudentProgressDTO } from '../../enterprise/entities/dtos/module-with-student-progress' | ||
import { type CoursesRepository } from '../repositories/courses-repository' | ||
import { type EnrollmentCompletedItemsRepository } from '../repositories/enrollment-completed-items-repository' | ||
import { type EnrollmentsRepository } from '../repositories/enrollments-repository' | ||
import { type ModulesRepository } from '../repositories/modules-repository' | ||
|
||
interface GetStudentProgressUseCaseRequest { | ||
enrollmentId: string | ||
studentId: string | ||
} | ||
|
||
type GetStudentProgressUseCaseResponse = Either< | ||
ResourceNotFoundError | NotAllowedError, | ||
{ | ||
classes: ClassWithStudentProgressDTO[] | ||
modules: ModuleWithStudentProgressDTO[] | ||
} | ||
> | ||
|
||
export class GetStudentProgressUseCase implements UseCase<GetStudentProgressUseCaseRequest, GetStudentProgressUseCaseResponse> { | ||
constructor( | ||
private readonly enrollmentsRepository: EnrollmentsRepository, | ||
private readonly coursesRepository: CoursesRepository, | ||
private readonly modulesRepository: ModulesRepository, | ||
private readonly enrollmentCompletedItemsRepository: EnrollmentCompletedItemsRepository | ||
) { } | ||
|
||
async exec({ | ||
enrollmentId, | ||
studentId | ||
}: GetStudentProgressUseCaseRequest): Promise<GetStudentProgressUseCaseResponse> { | ||
const enrollment = await this.enrollmentsRepository.findById(enrollmentId) | ||
|
||
if (!enrollment) { | ||
return left(new ResourceNotFoundError()) | ||
} | ||
|
||
const studentIsTheEnrollmentOwner = enrollment.studentId.toString() === studentId | ||
|
||
if (!studentIsTheEnrollmentOwner) { | ||
return left(new NotAllowedError()) | ||
} | ||
|
||
const course = await this.coursesRepository.findById(enrollment.courseId.toString()) | ||
|
||
if (!course) { | ||
return left(new ResourceNotFoundError()) | ||
} | ||
|
||
const courseClasses = await this.modulesRepository.findManyClassesByCourseId(course.id.toString()) | ||
|
||
const completedClasses = await this.enrollmentCompletedItemsRepository.findManyCompletedClassesByEnrollmentId( | ||
enrollmentId | ||
) | ||
const completedClassIds = completedClasses.map(completedClass => completedClass.itemId.toString()) | ||
|
||
const classesProgression: ClassWithStudentProgressDTO[] = [] | ||
|
||
courseClasses.forEach(courseClass => { | ||
const isClassCompleted = completedClassIds.includes(courseClass.id.toString()) | ||
|
||
if (isClassCompleted) { | ||
const classWithProgress: ClassWithStudentProgressDTO = { | ||
class: ClassDtoMapper.toDTO(courseClass), | ||
completed: true | ||
} | ||
|
||
classesProgression.push(classWithProgress) | ||
} else { | ||
const classWithProgress: ClassWithStudentProgressDTO = { | ||
class: ClassDtoMapper.toDTO(courseClass), | ||
completed: false | ||
} | ||
|
||
classesProgression.push(classWithProgress) | ||
} | ||
}) | ||
|
||
const courseModules = await this.modulesRepository.findManyByCourseId(course.id.toString()) | ||
|
||
const completedModules = await this.enrollmentCompletedItemsRepository.findManyCompletedModulesByEnrollmentId( | ||
enrollmentId | ||
) | ||
const completedModuleIds = completedModules.map(completedModule => completedModule.id.toString()) | ||
|
||
const modulesProgression: ModuleWithStudentProgressDTO[] = [] | ||
|
||
courseModules.forEach(courseModule => { | ||
const isModuleCompleted = completedModuleIds.includes(courseModule.id.toString()) | ||
|
||
if (isModuleCompleted) { | ||
const moduleWithProgress: ModuleWithStudentProgressDTO = { | ||
module: ModuleDtoMapper.toDTO(courseModule), | ||
completed: true | ||
} | ||
|
||
modulesProgression.push(moduleWithProgress) | ||
} else { | ||
const moduleWithProgress: ModuleWithStudentProgressDTO = { | ||
module: ModuleDtoMapper.toDTO(courseModule), | ||
completed: false | ||
} | ||
|
||
modulesProgression.push(moduleWithProgress) | ||
} | ||
}) | ||
|
||
return right({ | ||
classes: classesProgression, | ||
modules: modulesProgression | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
src/domain/course-management/enterprise/entities/dtos/class-with-student-progress.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { type ClassDTO } from './class' | ||
|
||
export interface ClassWithStudentProgressDTO { | ||
class: ClassDTO | ||
completed: boolean | ||
} |
10 changes: 10 additions & 0 deletions
10
src/domain/course-management/enterprise/entities/dtos/class.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { type UniqueEntityID } from '@/core/entities/unique-entity-id' | ||
|
||
export interface ClassDTO { | ||
id: UniqueEntityID | ||
name: string | ||
description: string | ||
videoId: UniqueEntityID | ||
classNumber: number | ||
moduleId: UniqueEntityID | ||
} |
9 changes: 0 additions & 9 deletions
9
...omain/course-management/enterprise/entities/dtos/complete-course-with-student-progress.ts
This file was deleted.
Oops, something went wrong.
15 changes: 15 additions & 0 deletions
15
src/domain/course-management/enterprise/entities/dtos/mappers/class-dto-mapper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { type Class } from '../../class' | ||
import { type ClassDTO } from '../class' | ||
|
||
export class ClassDtoMapper { | ||
static toDTO(classToMap: Class): ClassDTO { | ||
return { | ||
id: classToMap.id, | ||
name: classToMap.name, | ||
description: classToMap.description, | ||
classNumber: classToMap.classNumber, | ||
moduleId: classToMap.moduleId, | ||
videoId: classToMap.videoId | ||
} | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
src/domain/course-management/enterprise/entities/dtos/mappers/module-dto-mapper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { type Module } from '../../module' | ||
import { type ModuleDTO } from '../module' | ||
|
||
export class ModuleDtoMapper { | ||
static toDTO(module: Module): ModuleDTO { | ||
return { | ||
id: module.id, | ||
name: module.name, | ||
description: module.description, | ||
moduleNumber: module.moduleNumber, | ||
courseId: module.courseId | ||
} | ||
} | ||
} |
15 changes: 0 additions & 15 deletions
15
...in/course-management/enterprise/entities/dtos/module-with-classes-and-student-progress.ts
This file was deleted.
Oops, something went wrong.
6 changes: 6 additions & 0 deletions
6
src/domain/course-management/enterprise/entities/dtos/module-with-student-progress.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { type ModuleDTO } from './module' | ||
|
||
export interface ModuleWithStudentProgressDTO { | ||
module: ModuleDTO | ||
completed: boolean | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { NotAllowedError } from '@/core/errors/errors/not-allowed-error' | ||
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' | ||
import { makeGetStudentProgressUseCase } from '@/infra/use-cases/factories/make-get-student-progress-use-case' | ||
import { type FastifyReply, type FastifyRequest } from 'fastify' | ||
import { z } from 'zod' | ||
import { ClassWithStudentProgressPresenter } from '../presenters/class-with-student-progress-presenter' | ||
import { ModuleWithStudentProgressPresenter } from '../presenters/module-with-student-progress-presenter' | ||
|
||
const getStudentProgressParamsSchema = z.object({ | ||
enrollmentId: z.string().uuid() | ||
}) | ||
|
||
export async function getStudentProgressController(request: FastifyRequest, reply: FastifyReply) { | ||
const { enrollmentId } = getStudentProgressParamsSchema.parse(request.params) | ||
const { sub: studentId } = request.user | ||
|
||
const getStudentProgressUseCase = makeGetStudentProgressUseCase() | ||
|
||
const result = await getStudentProgressUseCase.exec({ | ||
enrollmentId, | ||
studentId | ||
}) | ||
|
||
if (result.isLeft()) { | ||
const error = result.value | ||
|
||
switch (error.constructor) { | ||
case ResourceNotFoundError: | ||
return await reply.status(404).send({ message: error.message }) | ||
case NotAllowedError: | ||
return await reply.status(401).send({ message: error.message }) | ||
default: | ||
return await reply.status(500).send({ message: error.message }) | ||
} | ||
} | ||
|
||
const { modules, classes } = result.value | ||
|
||
return await reply.status(200).send({ | ||
classes: classes.map(classWithProgress => ClassWithStudentProgressPresenter.toHTTP(classWithProgress)), | ||
modules: modules.map(moduleWithProgress => ModuleWithStudentProgressPresenter.toHTTP(moduleWithProgress)) | ||
}) | ||
} |
Oops, something went wrong.