Skip to content

Commit

Permalink
Feat(Student, Course, Enrollment): Enroll student to course use case
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur-Poffo committed Feb 6, 2024
1 parent 728cf59 commit 6aae7c3
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 6 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
- [x] Return information about a course.
- [x] Return information about a course with its modules and classes.
- [x] Classes and modules must have a field informing their position, e.g. this class is number one, so this is the first class of the course.
- [ ] Video streaming to watch the classes.

- [ ] Students can "enroll" to participate in the course.
- [x] Students can "enroll" to participate in the course.
- [ ] Return information about a course with its students. - Make it work after creating Enrollment entity
- [ ] Students can mark classes as completed.
- [ ] Return information about a student with the courses they are enrolled in.
Expand All @@ -34,12 +33,13 @@
- [ ] An instructor should be able to retrieve traffic data for one of their courses.

- [ ] CRUDs for all main entities: course, module, class, user, etc.
- [ ] Video streaming to watch the classes.

## Business Rules

- [x] User should not be able to register with the same email.
- [x] User should not be able to register with the same CPF.
- [ ] Only students can enroll for a course.
- [x] Only students can enroll for a course.
- [ ] Only students can rate courses and classes.
- [x] Only instructors can register a course.
- [x] A specific instructor cannot register a course with the same name.
Expand All @@ -49,7 +49,7 @@
- [x] Should not be able to add a class to a module with the same name twice.
- [x] Should not be able to register a module to a specific course with same name twice.
- [ ] A student can only issue one certificate per course.
- [ ] A student can only enroll for a particular course once.
- [x] A student can only enroll for a particular course once.
- [x] There should not be repeated tags in a course.

## Non-Functional Requirements
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type Enrollment } from '../../enterprise/entities/enrollment'

export interface EnrollmentsRepository {
findById: (id: string) => Promise<Enrollment | null>
findByStudentIdAndCourseId: (studentId: string, courseId: string) => Promise<Enrollment | null>
create: (enrollment: Enrollment) => Promise<Enrollment>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Student } from '../../enterprise/entities/student'

export interface StudentsRepository {
findById: (id: string) => Promise<Student | null>
findByEmail: (email: string) => Promise<Student | null>
findByCpf: (cpf: string) => Promise<Student | null>
create: (student: Student) => Promise<Student>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
import { makeCourse } from '../../../../../test/factories/make-course'
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 { EnrollToCourseUseCase } from './enroll-to-course'
import { AlreadyEnrolledInThisCourse } from './errors/already-enrolled-in-this-course'

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

describe('Enroll to course use case', () => {
beforeEach(() => {
inMemoryEnrollmentsRepository = new InMemoryEnrollmentsRepository()
inMemoryClassesRepository = new InMemoryClassesRepository()
inMemoryInstructorsRepository = new InMemoryInstructorRepository()
inMemoryStudentsRepository = new InMemoryStudentsRepository()

inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryInstructorsRepository)

sut = new EnrollToCourseUseCase(inMemoryEnrollmentsRepository, inMemoryStudentsRepository, inMemoryCoursesRepository)
})

it('should be able to enroll to a course', async () => {
const student = makeStudent()
await inMemoryStudentsRepository.create(student)

const course = makeCourse()
await inMemoryCoursesRepository.create(course)

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

expect(result.isRight()).toBe(true)
expect(result.value).toMatchObject({
enrollment: expect.objectContaining({
studentId: student.id
})
})
})

it('should not be able to enroll to a inexistent course', async () => {
const student = makeStudent()
await inMemoryStudentsRepository.create(student)

const result = await sut.exec({
studentId: student.id.toString(),
courseId: 'inexistentCourseId'
})

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

it('an student should not be able to enroll to a course twice', async () => {
const student = makeStudent()
await inMemoryStudentsRepository.create(student)

const course = makeCourse()
await inMemoryCoursesRepository.create(course)

await sut.exec({
studentId: student.id.toString(),
courseId: course.id.toString()
})

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

expect(result.isLeft()).toBe(true)
expect(result.value).toBeInstanceOf(AlreadyEnrolledInThisCourse)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 { Enrollment } from '../../enterprise/entities/enrollment'
import { type CoursesRepository } from '../repositories/courses-repository'
import { type EnrollmentsRepository } from '../repositories/enrollments'
import { type StudentsRepository } from '../repositories/students-repository'
import { AlreadyEnrolledInThisCourse } from './errors/already-enrolled-in-this-course'

interface EnrollToCourseUseCaseRequest {
studentId: string
courseId: string
}

type EnrollToCourseUseCaseResponse = Either<
ResourceNotFoundError,
{
enrollment: Enrollment
}
>

export class EnrollToCourseUseCase implements UseCase<EnrollToCourseUseCaseRequest, EnrollToCourseUseCaseResponse> {
constructor(
private readonly enrollmentsRepository: EnrollmentsRepository,
private readonly studentsRepository: StudentsRepository,
private readonly coursesRepository: CoursesRepository
) { }

async exec({
studentId,
courseId
}: EnrollToCourseUseCaseRequest): Promise<EnrollToCourseUseCaseResponse> {
const [student, course] = await Promise.all([
this.studentsRepository.findById(studentId),
this.coursesRepository.findById(courseId)
])

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

const enrollmentAlreadyExists = await this.enrollmentsRepository.findByStudentIdAndCourseId(studentId, courseId)

if (enrollmentAlreadyExists) {
return left(new AlreadyEnrolledInThisCourse(course.name))
}

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

await this.enrollmentsRepository.create(enrollment)

return right({
enrollment
})
}
}
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 AlreadyEnrolledInThisCourse extends Error implements UseCaseError {
constructor(courseName: string) {
super(`Already Enrolled in this course: ${courseName}.`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ export class Enrollment extends Entity<EnrollmentProps> {
}

static create(
props: Optional<EnrollmentProps, 'ocurredAt' | 'completedAt'>,
props: Optional<EnrollmentProps, 'ocurredAt' | 'completedAt' | 'completedModules' | 'completedClasses'>,
id?: UniqueEntityID
) {
const enrollment = new Enrollment(
{
...props,
ocurredAt: props.ocurredAt ?? new Date(),
completedAt: null
completedAt: null,
completedModules: [],
completedClasses: []
},
id
)
Expand Down
37 changes: 37 additions & 0 deletions test/repositories/in-memory-enrollments-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type EnrollmentsRepository } from '@/domain/course-management/application/repositories/enrollments'
import { type Enrollment } from '@/domain/course-management/enterprise/entities/enrollment'

export class InMemoryEnrollmentsRepository implements EnrollmentsRepository {
public items: Enrollment[] = []

async findById(id: string): Promise<Enrollment | null> {
const enrollment = this.items.find(enrollmentToCompare => enrollmentToCompare.id.toString() === id)

if (!enrollment) {
return null
}

return enrollment
}

async findByStudentIdAndCourseId(studentId: string, courseId: string): Promise<Enrollment | null> {
const enrollment = this.items.find(enrollmentToCompare => {
if (enrollmentToCompare.studentId.toString() === studentId && enrollmentToCompare.courseId.toString() === courseId) {
return enrollmentToCompare
}

return undefined
})

if (!enrollment) {
return null
}

return enrollment
}

async create(enrollment: Enrollment): Promise<Enrollment> {
this.items.push(enrollment)
return enrollment
}
}
10 changes: 10 additions & 0 deletions test/repositories/in-memory-students-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { type Student } from '@/domain/course-management/enterprise/entities/stu
export class InMemoryStudentsRepository implements StudentsRepository {
public items: Student[] = []

async findById(id: string): Promise<Student | null> {
const student = this.items.find((studentToCompare) => studentToCompare.id.toString() === id)

if (!student) {
return null
}

return student
}

async findByEmail(email: string): Promise<Student | null> {
const student = this.items.find((studentToCompare) => studentToCompare.email === email)

Expand Down

0 comments on commit 6aae7c3

Please sign in to comment.