Skip to content

Commit

Permalink
Feat(StudentCertificate, Certificate, Enrollment): Issue certificate …
Browse files Browse the repository at this point in the history
…use case
  • Loading branch information
Artur-Poffo committed Feb 8, 2024
1 parent 9f564a8 commit e2fd604
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
- [x] Mark modules as completed after the student views all its classes.
- [x] After completing all modules of a course, that course for a student should be marked as completed.
- [x] Return information about a student with the courses they are enrolled in.
- [ ] After completing a course, the student can issue a certificate.
- [x] After completing a course, the student can issue a certificate.

- [ ] It should be possible to filter courses by name or "tags."
- [ ] Students can evaluate a particular course they are taking with a rating from 1 to 5 to later have an average rating for each course.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type StudentCertificate } from '../../enterprise/entities/student-certificate'

export interface StudentCertificatesRepository {
findById: (id: string) => Promise<StudentCertificate | null>
findByStudentIdAndCertificateId: (studentId: string, certificateId: string) => Promise<StudentCertificate | null>
create: (studentCertificate: StudentCertificate) => Promise<StudentCertificate>
}
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 CertificateHasAlreadyBeenIssued extends Error implements UseCaseError {
constructor() {
super('certificate has already been issued.')
}
}
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 CompleteTheCourseBeforeTheCertificateIIsIssuedError extends Error implements UseCaseError {
constructor() {
super('Complete the course before the certificate is issued')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
import { makeCertificate } from '../../../../../test/factories/make-certificate'
import { makeCourse } from '../../../../../test/factories/make-course'
import { makeEnrollment } from '../../../../../test/factories/make-enrollment'
import { makeInstructor } from '../../../../../test/factories/make-instructor'
import { makeStudent } from '../../../../../test/factories/make-student'
import { InMemoryCertificatesRepository } from '../../../../../test/repositories/in-memory-certificates-repository'
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 { 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 { InMemoryStudentCertificatesRepository } from './../../../../../test/repositories/in-memory-student-certificates-repository'
import { CertificateHasAlreadyBeenIssued } from './errors/certificate-has-already-been-issued-error'
import { IssueCertificateUseCase } from './issue-certificate'

let inMemoryCertificatesRepository: InMemoryCertificatesRepository
let inMemoryStudentCertificatesRepository: InMemoryStudentCertificatesRepository
let inMemoryEnrollmentsRepository: InMemoryEnrollmentsRepository
let inMemoryStudentsRepository: InMemoryStudentsRepository
let inMemoryClassesRepository: InMemoryClassesRepository
let inMemoryInstructorsRepository: InMemoryInstructorRepository
let inMemoryModulesRepository: InMemoryModulesRepository
let inMemoryCoursesRepository: InMemoryCoursesRepository
let sut: IssueCertificateUseCase

describe('Issue certificate use case', () => {
beforeEach(() => {
inMemoryCertificatesRepository = new InMemoryCertificatesRepository()
inMemoryStudentCertificatesRepository = new InMemoryStudentCertificatesRepository()
inMemoryClassesRepository = new InMemoryClassesRepository()
inMemoryInstructorsRepository = new InMemoryInstructorRepository()
inMemoryStudentsRepository = new InMemoryStudentsRepository()

inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)

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

sut = new IssueCertificateUseCase(
inMemoryCertificatesRepository,
inMemoryStudentCertificatesRepository,
inMemoryEnrollmentsRepository,
inMemoryStudentsRepository
)
})

it('should be able to issue a certificate of a course', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ name: 'John Doe Course', instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const certificate = makeCertificate({ courseId: course.id })
await inMemoryCertificatesRepository.create(certificate)

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

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

// Mark enrollment as completed
await inMemoryEnrollmentsRepository.markAsCompleted(enrollment)

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

expect(result.isRight()).toBe(true)
expect(inMemoryEnrollmentsRepository.items).toHaveLength(1)
})

it('should not be able to issue a certificate from a inexistent enrollment', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ name: 'John Doe Course', instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const certificate = makeCertificate({ courseId: course.id })
await inMemoryCertificatesRepository.create(certificate)

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

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

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

it('should not be able to issue a inexistent certificate of a course', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ name: 'John Doe Course', instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

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

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

// Mark enrollment as completed
await inMemoryEnrollmentsRepository.markAsCompleted(enrollment)

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

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

it('should not be able to issue a same certificate from a enrollment twice', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ name: 'John Doe Course', instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

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

const certificate = makeCertificate({ courseId: course.id })
await inMemoryCertificatesRepository.create(certificate)

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

// Mark enrollment as completed
await inMemoryEnrollmentsRepository.markAsCompleted(enrollment)

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(CertificateHasAlreadyBeenIssued)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 { StudentCertificate } from '../../enterprise/entities/student-certificate'
import { type CertificatesRepository } from '../repositories/certificates-repository'
import { type StudentsRepository } from '../repositories/students-repository'
import { type EnrollmentsRepository } from './../repositories/enrollments-repository'
import { type StudentCertificatesRepository } from './../repositories/student-certificates-repository'
import { CertificateHasAlreadyBeenIssued } from './errors/certificate-has-already-been-issued-error'
import { CompleteTheCourseBeforeTheCertificateIIsIssuedError } from './errors/complete-the-course-before-the-certificate-is-issued-error'

interface IssueCertificateUseCaseRequest {
enrollmentId: string
studentId: string
}

type IssueCertificateUseCaseResponse = Either<
ResourceNotFoundError | CertificateHasAlreadyBeenIssued | CompleteTheCourseBeforeTheCertificateIIsIssuedError,
{
issuedCertificate: StudentCertificate
}
>

export class IssueCertificateUseCase implements UseCase<IssueCertificateUseCaseRequest, IssueCertificateUseCaseResponse> {
constructor(
private readonly certificatesRepository: CertificatesRepository,
private readonly studentCertificatesRepository: StudentCertificatesRepository,
private readonly enrollmentsRepository: EnrollmentsRepository,
private readonly studentsRepository: StudentsRepository
) { }

async exec({
enrollmentId,
studentId
}: IssueCertificateUseCaseRequest): Promise<IssueCertificateUseCaseResponse> {
const [enrollment, student] = await Promise.all([
this.enrollmentsRepository.findById(enrollmentId),
this.studentsRepository.findById(studentId)
])

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

const courseCertificate = await this.certificatesRepository.findByCourseId(enrollment.courseId.toString())

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

const certificateHasAlreadyBeenIssued = await this.studentCertificatesRepository.findByStudentIdAndCertificateId(
student.id.toString(),
courseCertificate.id.toString()
)

if (certificateHasAlreadyBeenIssued) {
return left(new CertificateHasAlreadyBeenIssued())
}

const courseIsCompleted = !!enrollment.completedAt

if (!courseIsCompleted) {
return left(new CompleteTheCourseBeforeTheCertificateIIsIssuedError())
}

const issuedCertificate = StudentCertificate.create({
studentId: student.id,
certificateId: courseCertificate.id
})

await this.studentCertificatesRepository.create(issuedCertificate)

return right({
issuedCertificate
})
}
}
18 changes: 18 additions & 0 deletions test/factories/make-certificate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { UniqueEntityID } from '@/core/entities/unique-entity-id'
import { Certificate, type CertificateProps } from '@/domain/course-management/enterprise/entities/certificate'

export function makeCertificate(
override: Partial<CertificateProps> = {},
id?: UniqueEntityID
) {
const certificate = Certificate.create(
{
courseId: override.courseId ?? new UniqueEntityID(),
imageId: override.imageId ?? new UniqueEntityID(),
...override
},
id
)

return certificate
}
37 changes: 37 additions & 0 deletions test/repositories/in-memory-student-certificates-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type StudentCertificatesRepository } from '@/domain/course-management/application/repositories/student-certificates-repository'
import { type StudentCertificate } from '@/domain/course-management/enterprise/entities/student-certificate'

export class InMemoryStudentCertificatesRepository implements StudentCertificatesRepository {
public items: StudentCertificate[] = []

async findById(id: string): Promise<StudentCertificate | null> {
const studentCertificate = this.items.find(studentCertificateToFind => studentCertificateToFind.id.toString() === id)

if (!studentCertificate) {
return null
}

return studentCertificate
}

async findByStudentIdAndCertificateId(studentId: string, certificateId: string): Promise<StudentCertificate | null> {
const studentCertificate = this.items.find(studentCertificateToFind => {
if (studentCertificateToFind.studentId.toString() === studentId && studentCertificateToFind.certificateId.toString() === certificateId) {
return studentCertificateToFind
}

return null
})

if (!studentCertificate) {
return null
}

return studentCertificate
}

async create(studentCertificate: StudentCertificate): Promise<StudentCertificate> {
this.items.push(studentCertificate)
return studentCertificate
}
}

0 comments on commit e2fd604

Please sign in to comment.