Skip to content

Commit

Permalink
Feat(Module, Enrollment, Route): Fetch enrollment completed modules r…
Browse files Browse the repository at this point in the history
…oute
  • Loading branch information
Artur-Poffo committed Feb 24, 2024
1 parent 786f328 commit 137f60c
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 5 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@

### Courses
- [x] GET /courses/:courseId - Get course details
- [ ] GET /courses/:courseId/enrollments/:enrollmentId/progress - Get course details with student progress
- [x] GET /courses/:courseId/stats - Get course statistics, like duration and number of classes
- [x] GET /courses/:courseId/metrics - Get course metrics for a dashboard
- [x] GET /courses - Get recent courses with instructor and evaluation average
Expand Down Expand Up @@ -249,9 +248,10 @@
- [x] POST /enrollments/:enrollmentId/modules/:moduleId/complete - Mark module as completed
- [x] POST /enrollments/:enrollmentId/classes/:classId/complete - Mark class as completed
- [x] POST /enrollments/:enrollmentId/complete - Mark enrollment as completed
- [ ] GET /enrollments/:enrollmentId/progress - Get student enrollment progress
- [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] 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
@@ -0,0 +1,112 @@
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
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 { FetchEnrollmentCompletedModulesUseCase } from './fetch-enrollment-completed-modules'

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

describe('Fetch enrollment completed modules 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 FetchEnrollmentCompletedModulesUseCase(
inMemoryEnrollmentsRepository,
inMemoryEnrollmentCompletedItemsRepository
)
})

it('should be able to fetch enrollment completed modules', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

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

const firstModule = makeModule({
courseId: course.id,
moduleNumber: 1
})
const secondModule = makeModule({
courseId: course.id,
moduleNumber: 2
})

await Promise.all([
inMemoryModulesRepository.create(firstModule),
inMemoryModulesRepository.create(secondModule)
])

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

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

const firstCompletedItem = makeEnrollmentCompletedItem({ itemId: firstModule.id, enrollmentId: enrollment.id, type: 'MODULE' })
const secondCompletedItem = makeEnrollmentCompletedItem({ itemId: secondModule.id, enrollmentId: enrollment.id, type: 'MODULE' })

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({
completedModules: 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 modules 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 FetchEnrollmentCompletedModulesUseCaseRequest {
enrollmentId: string
}

type FetchEnrollmentCompletedModulesUseCaseResponse = Either<
ResourceNotFoundError,
{
completedModules: EnrollmentCompletedItem[]
}
>

export class FetchEnrollmentCompletedModulesUseCase implements UseCase<FetchEnrollmentCompletedModulesUseCaseRequest, FetchEnrollmentCompletedModulesUseCaseResponse> {
constructor(
private readonly enrollmentsRepository: EnrollmentsRepository,
private readonly enrollmentCompletedItemsRepository: EnrollmentCompletedItemsRepository
) { }

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

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

const enrollmentCompletedModules = await this.enrollmentCompletedItemsRepository.findManyCompletedModulesByEnrollmentId(
enrollmentId
)

return right({
completedModules: enrollmentCompletedModules
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { z } from 'zod'
import { EnrollmentCompletedItemPresenter } from '../presenters/enrollment-completed-item-presenter'

const fetchEnrollmentCompletedClassesParamsSchema = z.object({
enrollmentId: z.string().uuid(),
classId: z.string().uuid()
enrollmentId: z.string().uuid()
})

export async function fetchEnrollmentCompletedClassesController(request: FastifyRequest, reply: FastifyReply) {
Expand Down
43 changes: 43 additions & 0 deletions src/infra/http/controllers/fetch-enrollment-completed-modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
import { EnrollmentCompletedItemMapper } from '@/infra/database/prisma/mappers/enrollment-completed-item-mapper'
import { makeFetchEnrollmentCompletedModulesUseCase } from '@/infra/use-cases/factories/make-fetch-enrollment-completed-modules-use-case'
import { type FastifyReply, type FastifyRequest } from 'fastify'
import { z } from 'zod'
import { EnrollmentCompletedItemPresenter } from '../presenters/enrollment-completed-item-presenter'

const fetchEnrollmentCompletedModulesParamsSchema = z.object({
enrollmentId: z.string().uuid()
})

export async function fetchEnrollmentCompletedModulesController(request: FastifyRequest, reply: FastifyReply) {
const { enrollmentId } = fetchEnrollmentCompletedModulesParamsSchema.parse(request.params)

const fetchEnrollmentCompletedModulesUseCase = makeFetchEnrollmentCompletedModulesUseCase()

const result = await fetchEnrollmentCompletedModulesUseCase.exec({
enrollmentId
})

if (result.isLeft()) {
const error = result.value

switch (error.constructor) {
case ResourceNotFoundError:
return await reply.status(404).send({ message: error.message })
default:
return await reply.status(500).send({ message: error.message })
}
}

const completedModules = result.value.completedModules

const infraCompletedModules = completedModules.map(completedModule => {
return EnrollmentCompletedItemMapper.toPrisma(completedModule)
})

return await reply.status(200).send({
completedModules: infraCompletedModules.map(infraCompletedModule => {
return EnrollmentCompletedItemPresenter.toHTTP(infraCompletedModule)
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class EnrollmentCompletedItemPresenter {
return {
id: enrollmentCompletedItem.id,
enrollmentId: enrollmentCompletedItem.enrollmentId,
itemUd: enrollmentCompletedItem.itemId,
itemId: enrollmentCompletedItem.itemId,
itemType: enrollmentCompletedItem.itemType
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/infra/http/routes/enrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type FastifyInstance } from 'fastify'
import { cancelEnrollmentController } from '../controllers/cancel-enrollment'
import { enrollToCourseController } from '../controllers/enroll-to-course'
import { fetchEnrollmentCompletedClassesController } from '../controllers/fetch-enrollment-completed-classes'
import { fetchEnrollmentCompletedModulesController } from '../controllers/fetch-enrollment-completed-modules'
import { getEnrollmentDetailsController } from '../controllers/get-enrollment-details'
import { markCourseAsCompletedController } from '../controllers/mark-course-as-completed'
import { toggleMarkClassAsCompletedController } from '../controllers/toggle-mark-class-as-completed'
Expand All @@ -12,6 +13,7 @@ import { verifyUserRole } from '../middlewares/verify-user-role'
export async function enrollmentRoutes(app: FastifyInstance) {
app.get('/courses/:courseId/students/:studentId/enrollments', { onRequest: [verifyJwt] }, getEnrollmentDetailsController)
app.get('/enrollments/:enrollmentId/classes/completed', { onRequest: [verifyJwt] }, fetchEnrollmentCompletedClassesController)
app.get('/enrollments/:enrollmentId/modules/completed', { onRequest: [verifyJwt] }, fetchEnrollmentCompletedModulesController)

app.post('/courses/:courseId/enroll', { onRequest: [verifyJwt, verifyUserRole('STUDENT')] }, enrollToCourseController)
app.post('/enrollments/:enrollmentId/classes/:classId/completed', { onRequest: [verifyJwt, verifyUserRole('STUDENT')] }, toggleMarkClassAsCompletedController)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FetchEnrollmentCompletedModulesUseCase } from '@/domain/course-management/application/use-cases/fetch-enrollment-completed-modules'
import { makePrismaEnrollmentsRepository } from '@/infra/database/prisma/repositories/factories/make-prisma-enrollments-repository'
import { PrismaEnrollmentCompleteItemsRepository } from '@/infra/database/prisma/repositories/prisma-enrollment-completed-items-repository'

export function makeFetchEnrollmentCompletedModulesUseCase() {
const prismaEnrollmentsRepository = makePrismaEnrollmentsRepository()
const prismaEnrollmentCompletedItemsRepository = new PrismaEnrollmentCompleteItemsRepository()

const fetchEnrollmentCompletedModulesUseCase = new FetchEnrollmentCompletedModulesUseCase(
prismaEnrollmentsRepository,
prismaEnrollmentCompletedItemsRepository
)

return fetchEnrollmentCompletedModulesUseCase
}

0 comments on commit 137f60c

Please sign in to comment.