Skip to content

Commit

Permalink
Feat(Class, Enrollment, Route): Fetch enrollment completed classes route
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur-Poffo committed Feb 24, 2024
1 parent a079985 commit 786f328
Show file tree
Hide file tree
Showing 25 changed files with 495 additions and 65 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@
- [x] POST /enrollments/:enrollmentId/classes/:classId/complete - Mark class as completed
- [x] POST /enrollments/:enrollmentId/complete - Mark enrollment as completed
- [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
- [ ] GET /enrollments/:enrollmentId/modules/completed - Fetch enrollment completed modules
- [x] DELETE /enrollments/:enrollmentId - Cancel enrollment

### Video
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { type EnrollmentCompletedItem } from '../../enterprise/entities/enrollme

export interface EnrollmentCompletedItemsRepository {
findById: (id: string) => Promise<EnrollmentCompletedItem | null>
findByEnrollmentIdAndItemId: (enrollmentId: string, itemId: string) => Promise<EnrollmentCompletedItem | null>
findManyCompletedClassesByEnrollmentId: (enrollmentId: string) => Promise<EnrollmentCompletedItem[]>
findManyCompletedModulesByEnrollmentId: (enrollmentId: string) => Promise<EnrollmentCompletedItem[]>
findAllByEnrollmentId: (enrollmentId: string) => Promise<EnrollmentCompletedItem[]>
create: (completedItem: EnrollmentCompletedItem) => Promise<EnrollmentCompletedItem>
delete: (completedItem: EnrollmentCompletedItem) => Promise<void>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type UseCaseError } from '@/core/errors/use-case-error'

export class ItemAlreadyCompletedError extends Error implements UseCaseError {
constructor() {
super('Item already marked as completed.')
}
}
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 { 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 { FetchEnrollmentCompletedClassesUseCase } from './fetch-enrollment-completed-classes'

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: FetchEnrollmentCompletedClassesUseCase

describe('Fetch enrollment completed classes use case', () => {
beforeEach(() => {
inMemoryStudentsRepository = new InMemoryStudentsRepository()
inMemoryEnrollmentCompletedItemsRepository = new InMemoryEnrollmentCompletedItemsRepository()
inMemoryCourseTagsRepository = new InMemoryCourseTagsRepository()
inMemoryClassesRepository = new InMemoryClassesRepository()
inMemoryInstructorsRepository = new InMemoryInstructorRepository()

inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)

inMemoryEnrollmentsRepository = new InMemoryEnrollmentsRepository(
inMemoryStudentsRepository, inMemoryEnrollmentCompletedItemsRepository
)
inMemoryCoursesRepository = new InMemoryCoursesRepository(
inMemoryModulesRepository, inMemoryInstructorsRepository, inMemoryEnrollmentsRepository, inMemoryStudentsRepository, inMemoryCourseTagsRepository
)

sut = new FetchEnrollmentCompletedClassesUseCase(
inMemoryEnrollmentsRepository,
inMemoryEnrollmentCompletedItemsRepository
)
})

it('should be able to fetch enrollment completed classes', 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 firstClassToMarkAsCompleted = makeClass({ name: 'John Doe Class 1', moduleId: module.id, classNumber: 1 })
const secondClassToMarkAsCompleted = makeClass({ name: 'John Doe Class 2', moduleId: module.id, classNumber: 2 })

await Promise.all([
inMemoryClassesRepository.create(firstClassToMarkAsCompleted),
inMemoryClassesRepository.create(secondClassToMarkAsCompleted)
])

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

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

const firstCompletedItem = makeEnrollmentCompletedItem({ itemId: firstClassToMarkAsCompleted.id, enrollmentId: enrollment.id, type: 'CLASS' })
const secondCompletedItem = makeEnrollmentCompletedItem({ itemId: secondClassToMarkAsCompleted.id, enrollmentId: enrollment.id, type: 'CLASS' })

await Promise.all([
inMemoryEnrollmentCompletedItemsRepository.create(firstCompletedItem),
inMemoryEnrollmentCompletedItemsRepository.create(secondCompletedItem)
])

const result = await sut.exec({
enrollmentId: enrollment.id.toString()
})

expect(result.isRight()).toBe(true)
expect(result.value).toMatchObject({
completedClasses: expect.arrayContaining([
expect.objectContaining({
itemId: firstCompletedItem.itemId
}),
expect.objectContaining({
itemId: secondCompletedItem.itemId
})
])
})
expect(inMemoryEnrollmentCompletedItemsRepository.items).toHaveLength(2)
})

it('should not be able to fetch enrollment completed classes from a inexistent enrollment', async () => {
const result = await sut.exec({
enrollmentId: 'inexistentEnrollmentId'
})

expect(result.isLeft()).toBe(true)
expect(result.value).toBeInstanceOf(ResourceNotFoundError)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { left, right, type Either } from '@/core/either'
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
import { type UseCase } from '@/core/use-cases/use-case'
import { type EnrollmentCompletedItem } from '../../enterprise/entities/enrollment-completed-item'
import { type EnrollmentCompletedItemsRepository } from '../repositories/enrollment-completed-items-repository'
import { type EnrollmentsRepository } from '../repositories/enrollments-repository'

interface FetchEnrollmentCompletedClassesUseCaseRequest {
enrollmentId: string
}

type FetchEnrollmentCompletedClassesUseCaseResponse = Either<
ResourceNotFoundError,
{
completedClasses: EnrollmentCompletedItem[]
}
>

export class FetchEnrollmentCompletedClassesUseCase implements UseCase<FetchEnrollmentCompletedClassesUseCaseRequest, FetchEnrollmentCompletedClassesUseCaseResponse> {
constructor(
private readonly enrollmentsRepository: EnrollmentsRepository,
private readonly enrollmentCompletedItemsRepository: EnrollmentCompletedItemsRepository
) { }

async exec({
enrollmentId
}: FetchEnrollmentCompletedClassesUseCaseRequest): Promise<FetchEnrollmentCompletedClassesUseCaseResponse> {
const enrollment = await this.enrollmentsRepository.findById(enrollmentId)

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

const enrollmentCompletedClasses = await this.enrollmentCompletedItemsRepository.findManyCompletedClassesByEnrollmentId(
enrollmentId
)

return right({
completedClasses: enrollmentCompletedClasses
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { InMemoryStudentsRepository } from '../../../../../test/repositories/in-
import { InMemoryInstructorRepository } from './../../../../../test/repositories/in-memory-instructors-repository'
import { InMemoryModulesRepository } from './../../../../../test/repositories/in-memory-modules-repository'
import { AllModulesInTheCourseMustBeMarkedAsCompleted } from './errors/all-modules-in-the-course-must-be-marked-as-completed'
import { ItemAlreadyCompletedError } from './errors/item-already-completed-error'
import { MarkCourseAsCompletedUseCase } from './mark-course-as-completed'

let inMemoryEnrollmentCompletedItemsRepository: InMemoryEnrollmentCompletedItemsRepository
Expand Down Expand Up @@ -90,6 +91,50 @@ describe('Mark course as completed use case', () => {
expect(inMemoryEnrollmentCompletedItemsRepository.items).toHaveLength(2)
})

it('should not be able to mark a enrollment of a student as completed twice', 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 firstCompletedItem = makeEnrollmentCompletedItem({ enrollmentId: enrollment.id, itemId: classToMarkAsCompleted.id, type: 'CLASS' })
const secondCompletedItem = makeEnrollmentCompletedItem({ enrollmentId: enrollment.id, itemId: module.id, type: 'MODULE' })

await Promise.all([
inMemoryEnrollmentCompletedItemsRepository.create(firstCompletedItem),
inMemoryEnrollmentCompletedItemsRepository.create(secondCompletedItem)
])

await sut.exec({
enrollmentId: enrollment.id.toString(),
studentId: student.id.toString()
})

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

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

it('should not be able to mark a inexistent enrollment of a student as completed', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { type EnrollmentsRepository } from '../repositories/enrollments-reposito
import { type StudentsRepository } from '../repositories/students-repository'
import { type ModulesRepository } from './../repositories/modules-repository'
import { AllModulesInTheCourseMustBeMarkedAsCompleted } from './errors/all-modules-in-the-course-must-be-marked-as-completed'
import { ItemAlreadyCompletedError } from './errors/item-already-completed-error'

interface MarkCourseAsCompletedUseCaseRequest {
enrollmentId: string
studentId: string
}

type MarkCourseAsCompletedUseCaseResponse = Either<
ResourceNotFoundError | NotAllowedError | AllModulesInTheCourseMustBeMarkedAsCompleted,
ResourceNotFoundError | NotAllowedError | AllModulesInTheCourseMustBeMarkedAsCompleted | ItemAlreadyCompletedError,
{
course: CompleteCourseDTO
}
Expand Down Expand Up @@ -51,6 +52,12 @@ export class MarkCourseAsCompletedUseCase implements UseCase<MarkCourseAsComplet
return left(new NotAllowedError())
}

const courseAlreadyMarkedAsCompleted = enrollment.completedAt

if (courseAlreadyMarkedAsCompleted) {
return left(new ItemAlreadyCompletedError())
}

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

if (!completeCourse) {
Expand All @@ -75,7 +82,7 @@ export class MarkCourseAsCompletedUseCase implements UseCase<MarkCourseAsComplet
)

const allModulesOfThisCourseIsCompleted = courseModules.every(moduleToCompare => {
return completedModules.includes(moduleToCompare)
return completedModules.some(completedModule => completedModule.id.toString() === moduleToCompare.id.toString())
})

if (!allModulesOfThisCourseIsCompleted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { InMemoryEnrollmentsRepository } from '../../../../../test/repositories/
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'
import { ToggleMarkClassAsCompletedUseCase } from './toggle-mark-class-as-completed'

let inMemoryEnrollmentCompletedItemsRepository: InMemoryEnrollmentCompletedItemsRepository
let inMemoryEnrollmentsRepository: InMemoryEnrollmentsRepository
Expand All @@ -24,9 +24,9 @@ let inMemoryClassesRepository: InMemoryClassesRepository
let inMemoryInstructorsRepository: InMemoryInstructorRepository
let inMemoryModulesRepository: InMemoryModulesRepository
let inMemoryCoursesRepository: InMemoryCoursesRepository
let sut: MarkClassAsCompletedUseCase
let sut: ToggleMarkClassAsCompletedUseCase

describe('Mark class as completed use case', () => {
describe('Toggle mark class as completed use case', () => {
beforeEach(() => {
inMemoryStudentsRepository = new InMemoryStudentsRepository()
inMemoryEnrollmentCompletedItemsRepository = new InMemoryEnrollmentCompletedItemsRepository()
Expand All @@ -43,7 +43,7 @@ describe('Mark class as completed use case', () => {
inMemoryModulesRepository, inMemoryInstructorsRepository, inMemoryEnrollmentsRepository, inMemoryStudentsRepository, inMemoryCourseTagsRepository
)

sut = new MarkClassAsCompletedUseCase(
sut = new ToggleMarkClassAsCompletedUseCase(
inMemoryEnrollmentsRepository, inMemoryCoursesRepository, inMemoryModulesRepository, inMemoryClassesRepository, inMemoryStudentsRepository, inMemoryEnrollmentCompletedItemsRepository
)
})
Expand Down Expand Up @@ -85,6 +85,44 @@ describe('Mark class as completed use case', () => {
expect(inMemoryEnrollmentCompletedItemsRepository.items).toHaveLength(1)
})

it('should be able to toggle mark a class of a enrollment as completed if it already marked 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)

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

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

expect(result.isRight()).toBe(true)
expect(inMemoryEnrollmentCompletedItemsRepository.items).toHaveLength(0)
})

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

0 comments on commit 786f328

Please sign in to comment.