Skip to content

Commit

Permalink
feat: Add list progress tracking service and endpoint (#115)
Browse files Browse the repository at this point in the history
* feat: Add list progress tracking service and endpoint

* docs: Update CHANGELOG with list progress endpoint
  • Loading branch information
lui7henrique authored Feb 5, 2025
1 parent 5cce951 commit 1435240
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## [Unreleased]

- Add list progress endpoint [(#115)](https://github.com/plotwist-app/plotwist-backend/pull/115)

### Added

- Add hasBanner query param to get lists [(#114)](https://github.com/plotwist-app/plotwist-backend/pull/114)
Expand Down
128 changes: 128 additions & 0 deletions src/domain/services/lists/get-list-progress.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, expect, it } from 'vitest'
import { makeUser } from '@/test/factories/make-user'
import { makeList } from '@/test/factories/make-list'
import { makeListItem } from '@/test/factories/make-list-item'
import { makeUserItem } from '@/test/factories/make-user-item'
import { getListProgressService } from './get-list-progress'

describe('get list progress', () => {
it('should return zero progress when list has no items', async () => {
const user = await makeUser()
const list = await makeList({ userId: user.id })

const sut = await getListProgressService({
id: list.id,
authenticatedUserId: user.id,
})

expect(sut).toEqual({
total: 0,
completed: 0,
percentage: 0,
})
})

it('should calculate correct progress when user has watched some items', async () => {
const user = await makeUser()
const list = await makeList({ userId: user.id })

const listItem1 = await makeListItem({
listId: list.id,
tmdbId: 123,
mediaType: 'MOVIE',
})

await makeListItem({
listId: list.id,
tmdbId: 456,
mediaType: 'MOVIE',
})

await makeUserItem({
userId: user.id,
tmdbId: listItem1.tmdbId,
mediaType: listItem1.mediaType,
status: 'WATCHED',
})

const sut = await getListProgressService({
id: list.id,
authenticatedUserId: user.id,
})

expect(sut).toEqual({
total: 2,
completed: 1,
percentage: 50,
})
})

it('should calculate 100% progress when user has watched all items', async () => {
const user = await makeUser()
const list = await makeList({ userId: user.id })

const listItem1 = await makeListItem({
listId: list.id,
tmdbId: 123,
mediaType: 'MOVIE',
})
const listItem2 = await makeListItem({
listId: list.id,
tmdbId: 456,
mediaType: 'MOVIE',
})

await makeUserItem({
userId: user.id,
tmdbId: listItem1.tmdbId,
mediaType: listItem1.mediaType,
status: 'WATCHED',
})
await makeUserItem({
userId: user.id,
tmdbId: listItem2.tmdbId,
mediaType: listItem2.mediaType,
status: 'WATCHED',
})

const sut = await getListProgressService({
id: list.id,
authenticatedUserId: user.id,
})

expect(sut).toEqual({
total: 2,
completed: 2,
percentage: 100,
})
})

it('should ignore user items with different media type', async () => {
const user = await makeUser()
const list = await makeList({ userId: user.id })

const listItem = await makeListItem({
listId: list.id,
tmdbId: 123,
mediaType: 'MOVIE',
})

await makeUserItem({
userId: user.id,
tmdbId: listItem.tmdbId,
mediaType: 'TV_SHOW',
status: 'WATCHED',
})

const sut = await getListProgressService({
id: list.id,
authenticatedUserId: user.id,
})

expect(sut).toEqual({
total: 1,
completed: 0,
percentage: 0,
})
})
})
42 changes: 42 additions & 0 deletions src/domain/services/lists/get-list-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { selectListItems } from '@/db/repositories/list-item-repository'
import { selectAllUserItemsByStatus } from '@/db/repositories/user-item-repository'

type GetListProgressServiceParams = {
id: string
authenticatedUserId: string
}

export async function getListProgressService({
id,
authenticatedUserId,
}: GetListProgressServiceParams) {
const listItems = await selectListItems(id)
if (listItems.length === 0) {
return {
total: 0,
completed: 0,
percentage: 0,
}
}

const userItems = await selectAllUserItemsByStatus({
userId: authenticatedUserId,
status: 'WATCHED',
})

const watchedItems = listItems.filter(listItem =>
userItems.some(
userItem =>
userItem.tmdbId === listItem.tmdbId &&
userItem.mediaType === listItem.mediaType
)
)

const percentage = Math.round((watchedItems.length / listItems.length) * 100)

return {
total: listItems.length,
completed: watchedItems.length,
percentage,
}
}
19 changes: 19 additions & 0 deletions src/http/controllers/list-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
updateListBodySchema,
updateListParamsSchema,
} from '../schemas/lists'
import { getListProgressService } from '@/domain/services/lists/get-list-progress'

export async function createListController(
request: FastifyRequest,
Expand Down Expand Up @@ -139,3 +140,21 @@ export async function updateListBannerController(

return reply.status(200).send({ list: result.list })
}

export async function getListProgressController(
request: FastifyRequest,
reply: FastifyReply
) {
const { id } = getListParamsSchema.parse(request.params)

const result = await getListProgressService({
id: id,
authenticatedUserId: request.user?.id,
})

if (result instanceof DomainError) {
return reply.status(result.status).send({ message: result.message })
}

return reply.status(200).send(result)
}
23 changes: 23 additions & 0 deletions src/http/routes/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createListController,
deleteListController,
getListController,
getListProgressController,
getListsController,
updateListBannerController,
updateListController,
Expand All @@ -16,6 +17,7 @@ import {
deleteListParamsSchema,
deleteListResponseSchema,
getListParamsSchema,
getListProgressResponseSchema,
getListResponseSchema,
getListsQuerySchema,
getListsResponseSchema,
Expand Down Expand Up @@ -147,4 +149,25 @@ export async function listsRoute(app: FastifyInstance) {
handler: updateListBannerController,
})
)

app.after(() =>
app.withTypeProvider<ZodTypeProvider>().route({
method: 'GET',
url: '/list/:id/progress',
onRequest: [verifyJwt],
schema: {
description: 'Get list progress',
tags: ['List'],
params: getListParamsSchema,
response: getListProgressResponseSchema,
operationId: 'getListProgress',
security: [
{
bearerAuth: [],
},
],
},
handler: getListProgressController,
})
)
}
8 changes: 8 additions & 0 deletions src/http/schemas/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,11 @@ export const updateListBannerResponseSchema = {
list: createSelectSchema(schema.lists),
}),
}

export const getListProgressResponseSchema = {
200: z.object({
total: z.number(),
completed: z.number(),
percentage: z.number(),
}),
}

0 comments on commit 1435240

Please # to comment.