Skip to content

Commit

Permalink
Feat(Class, Enrollment, Course): Mark class as completed use case
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur-Poffo committed Feb 8, 2024
1 parent 3bcbd3d commit 3ec0858
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- [x] Students can "enroll" to participate in the course.
- [x] Return information about a course with its students.
- [ ] Return information about a course with student progress in your modules and classes.
- [ ] Students can mark classes as completed.
- [x] Students can mark classes as completed.
- [ ] Mark modules as completed after the student views all its classes.
- [ ] Return information about a student with the courses they are enrolled in.
- [ ] After completing all modules of a course, that course for a student should be marked as completed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface EnrollmentsRepository {
findByStudentIdAndCourseId: (studentId: string, courseId: string) => Promise<Enrollment | null>
findManyByCourseId: (courseId: string) => Promise<Enrollment[]>
create: (enrollment: Enrollment) => Promise<Enrollment>
save: (classToSave: Enrollment) => Promise<void>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type ModuleWithClassesDTO } from './../../enterprise/entities/dtos/modu
export interface ModulesRepository {
findById: (id: string) => Promise<Module | null>
findManyByCourseId: (id: string) => Promise<Module[]>
findManyClassesByCourseId: (courseId: string) => Promise<Class[] | null>
findManyClassesByCourseId: (courseId: string) => Promise<Class[]>
findModuleWithClassesById: (id: string) => Promise<ModuleWithClassesDTO | null>
create: (module: Module) => Promise<Module>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { NotAllowedError } from '@/core/errors/errors/not-allowed-error'
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 { 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 { InMemoryCoursesRepository } from '../../../../../test/repositories/in-memory-courses-repository'
import { InMemoryEnrollmentsRepository } from '../../../../../test/repositories/in-memory-enrollments-repository'
import { InMemoryStudentsRepository } from '../../../../../test/repositories/in-memory-students-repository'
import { InMemoryInstructorRepository } from './../../../../../test/repositories/in-memory-instructors-repository'
import { InMemoryModulesRepository } from './../../../../../test/repositories/in-memory-modules-repository'
import { MarkClassAsCompletedUseCase } from './mark-class-as-completed'

let inMemoryEnrollmentsRepository: InMemoryEnrollmentsRepository
let inMemoryStudentsRepository: InMemoryStudentsRepository
let inMemoryClassesRepository: InMemoryClassesRepository
let inMemoryInstructorsRepository: InMemoryInstructorRepository
let inMemoryModulesRepository: InMemoryModulesRepository
let inMemoryCoursesRepository: InMemoryCoursesRepository
let sut: MarkClassAsCompletedUseCase

describe('Mark class as completed use case', () => {
beforeEach(() => {
inMemoryStudentsRepository = new InMemoryStudentsRepository()
inMemoryEnrollmentsRepository = new InMemoryEnrollmentsRepository()
inMemoryClassesRepository = new InMemoryClassesRepository()
inMemoryInstructorsRepository = new InMemoryInstructorRepository()

inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)

inMemoryCoursesRepository = new InMemoryCoursesRepository(
inMemoryModulesRepository, inMemoryInstructorsRepository, inMemoryEnrollmentsRepository, inMemoryStudentsRepository
)

sut = new MarkClassAsCompletedUseCase(
inMemoryEnrollmentsRepository, inMemoryCoursesRepository, inMemoryModulesRepository, inMemoryClassesRepository, inMemoryStudentsRepository
)
})

it('should be able to mark a class of a enrollment as completed', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
})
await inMemoryModulesRepository.create(module)

const classToMarkAsCompleted = makeClass({ name: 'John Doe Class', moduleId: module.id, classNumber: 1 })
await inMemoryClassesRepository.create(classToMarkAsCompleted)

const student = makeStudent()
await inMemoryStudentsRepository.create(student)

const enrollment = makeEnrollment({ studentId: student.id, courseId: course.id })
await inMemoryEnrollmentsRepository.create(enrollment)

const result = await sut.exec({
enrollmentId: enrollment.id.toString(),
classId: classToMarkAsCompleted.id.toString(),
studentId: student.id.toString()
})

expect(result.isRight()).toBe(true)
expect(result.value).toMatchObject({
class: expect.objectContaining({
name: 'John Doe Class'
})
})
})

it('should not be able to mark a inexistent class as completed', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
})
await inMemoryModulesRepository.create(module)

const student = makeStudent()
await inMemoryStudentsRepository.create(student)

const enrollment = makeEnrollment({ studentId: student.id, courseId: course.id })
await inMemoryEnrollmentsRepository.create(enrollment)

const result = await sut.exec({
enrollmentId: enrollment.id.toString(),
classId: 'inexistentClassId',
studentId: student.id.toString()
})

expect(result.isLeft()).toBe(true)
expect(result.value).toBeInstanceOf(ResourceNotFoundError)
})

it('should not be able to mark a class of a enrollment as completed if the student not is the owner of the enrollment', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
})
await inMemoryModulesRepository.create(module)

const classToMarkAsCompleted = makeClass({ name: 'John Doe Class', moduleId: module.id, classNumber: 1 })
await inMemoryClassesRepository.create(classToMarkAsCompleted)

const correctStudent = makeStudent()
const wrongStudent = makeStudent()

await Promise.all([
inMemoryStudentsRepository.create(correctStudent),
inMemoryStudentsRepository.create(wrongStudent)
])

const enrollment = makeEnrollment({ studentId: correctStudent.id, courseId: course.id })
await inMemoryEnrollmentsRepository.create(enrollment)

const result = await sut.exec({
enrollmentId: enrollment.id.toString(),
classId: classToMarkAsCompleted.id.toString(),
studentId: wrongStudent.id.toString()
})

expect(result.isLeft()).toBe(true)
expect(result.value).toBeInstanceOf(NotAllowedError)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 Class } from '../../enterprise/entities/class'
import { type CoursesRepository } from '../repositories/courses-repository'
import { type EnrollmentsRepository } from '../repositories/enrollments-repository'
import { type StudentsRepository } from '../repositories/students-repository'
import { type ClassesRepository } from './../repositories/classes-repository'
import { type ModulesRepository } from './../repositories/modules-repository'

interface MarkClassAsCompletedUseCaseRequest {
enrollmentId: string
studentId: string
classId: string
}

type MarkClassAsCompletedUseCaseResponse = Either<
ResourceNotFoundError | NotAllowedError,
{
class: Class
}
>

export class MarkClassAsCompletedUseCase implements UseCase<MarkClassAsCompletedUseCaseRequest, MarkClassAsCompletedUseCaseResponse> {
constructor(
private readonly enrollmentsRepository: EnrollmentsRepository,
private readonly coursesRepository: CoursesRepository,
private readonly modulesRepository: ModulesRepository,
private readonly classesRepository: ClassesRepository,
private readonly studentsRepository: StudentsRepository
) { }

async exec({
enrollmentId,
studentId,
classId
}: MarkClassAsCompletedUseCaseRequest): Promise<MarkClassAsCompletedUseCaseResponse> {
const [enrollment, student, classToMarkAsCompleted] = await Promise.all([
this.enrollmentsRepository.findById(enrollmentId),
this.studentsRepository.findById(studentId),
this.classesRepository.findById(classId)
])

if (!enrollment || !student || !classToMarkAsCompleted) {
return left(new ResourceNotFoundError())
}

const studentIsTheEnrollmentOwner = enrollment.studentId.toString() === studentId

if (!studentIsTheEnrollmentOwner) {
return left(new NotAllowedError())
}

const completeCourse = await this.coursesRepository.findCompleteCourseEntityById(enrollment.courseId.toString())

if (!completeCourse) {
return left(new ResourceNotFoundError())
}

const courseClasses = await this.modulesRepository.findManyClassesByCourseId(completeCourse.course.id.toString())
const courseClassesIds = courseClasses.map(classToMap => classToMap.id.toString())

const classExistInThisCourse = courseClassesIds.includes(classId)

if (!classExistInThisCourse) {
return left(new ResourceNotFoundError())
}

enrollment.completedClasses.push(classToMarkAsCompleted.id)
await this.enrollmentsRepository.save(enrollment)

return right({
class: classToMarkAsCompleted
})
}
}
7 changes: 6 additions & 1 deletion test/repositories/in-memory-enrollments-repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type EnrollmentsRepository } from '@/domain/course-management/application/repositories/enrollments'
import { type EnrollmentsRepository } from '@/domain/course-management/application/repositories/enrollments-repository'
import { type Enrollment } from '@/domain/course-management/enterprise/entities/enrollment'

export class InMemoryEnrollmentsRepository implements EnrollmentsRepository {
Expand Down Expand Up @@ -38,4 +38,9 @@ export class InMemoryEnrollmentsRepository implements EnrollmentsRepository {
this.items.push(enrollment)
return enrollment
}

async save(enrollment: Enrollment): Promise<void> {
const enrollmentIndex = this.items.indexOf(enrollment)
this.items[enrollmentIndex] = enrollment
}
}
2 changes: 1 addition & 1 deletion test/repositories/in-memory-modules-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class InMemoryModulesRepository implements ModulesRepository {
return this.items.filter(moduleToCompare => moduleToCompare.courseId.toString() === courseId)
}

async findManyClassesByCourseId(courseId: string): Promise<Class[] | null> {
async findManyClassesByCourseId(courseId: string): Promise<Class[]> {
const courseModules = this.items.filter(moduleToCompare => moduleToCompare.courseId.toString() === courseId)
const courseModulesWithClasses = await Promise.all(courseModules.map(async moduleToMap => await this.findModuleWithClassesById(moduleToMap.id.toString())))

Expand Down

0 comments on commit 3ec0858

Please sign in to comment.