Skip to content

Commit

Permalink
Feat(Enrollment, Course, Route): Get student progress in a course route
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur-Poffo committed Feb 24, 2024
1 parent 137f60c commit db70462
Show file tree
Hide file tree
Showing 18 changed files with 383 additions and 29 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@
- [x] POST /enrollments/:enrollmentId/modules/:moduleId/complete - Mark module as completed
- [x] POST /enrollments/:enrollmentId/classes/:classId/complete - Mark class as completed
- [x] POST /enrollments/:enrollmentId/complete - Mark enrollment as completed
- [ ] GET /enrollments/:enrollmentId/progress - Get student enrollment progress
- [x] GET /enrollments/:enrollmentId/progress - Get student enrollment progress
- [x] GET /courses/:courseId/students/:studentId/enrollments - Get enrollment of a student on a course
- [x] GET /enrollments/:enrollmentId/classes/completed - Fetch enrollment completed classes
- [x] GET /enrollments/:enrollmentId/modules/completed - Fetch enrollment completed modules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let inMemoryModulesRepository: InMemoryModulesRepository
let inMemoryCoursesRepository: InMemoryCoursesRepository
let sut: GetInstructorWithCoursesUseCase

describe('Get instructors with their courses', () => {
describe('Get instructors with their courses use case', () => {
beforeEach(() => {
inMemoryEnrollmentCompletedItemsRepository = new InMemoryEnrollmentCompletedItemsRepository()
inMemoryClassesRepository = new InMemoryClassesRepository()
Expand Down
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)
})
})
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
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ let inMemoryModulesRepository: InMemoryModulesRepository
let inMemoryCoursesRepository: InMemoryCoursesRepository
let sut: GetStudentWithCoursesUseCase

describe('Get student with their courses', () => {
describe('Get student with their courses use case', () => {
beforeEach(() => {
inMemoryEnrollmentCompletedItemsRepository = new InMemoryEnrollmentCompletedItemsRepository()
inMemoryClassesRepository = new InMemoryClassesRepository()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ export class IssueCertificateUseCase implements UseCase<IssueCertificateUseCaseR
return left(new CertificateHasAlreadyBeenIssued())
}

// FIXME: Fix this after completing the mark classes and modules as completed logic
const courseIsCompleted = !!enrollment.completedAt
const courseIsCompleted = enrollment.completedAt

if (!courseIsCompleted) {
return left(new CompleteTheCourseBeforeTheCertificateIIsIssuedError())
Expand Down
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 src/domain/course-management/enterprise/entities/dtos/class.ts
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
}

This file was deleted.

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
}
}
}
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
}
}
}

This file was deleted.

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
}
43 changes: 43 additions & 0 deletions src/infra/http/controllers/get-student-progress.ts
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))
})
}
Loading

0 comments on commit db70462

Please sign in to comment.