-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat(StudentCertificate, Certificate, Enrollment): Issue certificate …
…use case
- Loading branch information
1 parent
9f564a8
commit e2fd604
Showing
8 changed files
with
312 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
src/domain/course-management/application/repositories/student-certificates-repository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
7 changes: 7 additions & 0 deletions
7
...urse-management/application/use-cases/errors/certificate-has-already-been-issued-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.') | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
...pplication/use-cases/errors/complete-the-course-before-the-certificate-is-issued-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} | ||
} |
158 changes: 158 additions & 0 deletions
158
src/domain/course-management/application/use-cases/issue-certificate.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
77 changes: 77 additions & 0 deletions
77
src/domain/course-management/application/use-cases/issue-certificate.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
37
test/repositories/in-memory-student-certificates-repository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |