From ca4d7b028487127d897e4aadc72ee24b40cdedf4 Mon Sep 17 00:00:00 2001 From: Henry Brink Date: Fri, 14 Jun 2024 10:41:19 +0200 Subject: [PATCH] Tasks: Initial implementation (#224) * save * bump * start and stop tasks * save * fix: fitness data not awaited * enh: Added more tasks * add single task endpoint * added some tests Signed-off-by: Henry Brink * added further tests Signed-off-by: Henry Brink * fix: TaskLogs would already be considered started * make api compliant to docs * fix: enums are not correct in api doc * linter & ghas errors --------- Signed-off-by: Henry Brink --- .../migrations/20240611203212_/migration.sql | 11 ++ backend/prisma/schema.prisma | 11 ++ backend/src/app.module.ts | 2 + backend/src/app/tasks/task.controller.ts | 105 ++++++++++++ backend/src/app/tasks/task.module.ts | 12 ++ backend/src/app/tasks/task.service.spec.ts | 116 +++++++++++++ backend/src/app/tasks/task.service.ts | 159 ++++++++++++++++++ backend/src/app/tasks/tasks/static/task.1.ts | 20 +++ backend/src/app/tasks/tasks/static/task2.ts | 17 ++ backend/src/app/tasks/tasks/static/task3.ts | 17 ++ backend/src/app/tasks/tasks/task.base.ts | 20 +++ backend/src/app/tasks/tasks/task.step.ts | 16 ++ backend/src/app/tasks/tasks/task.types.ts | 23 +++ backend/src/db/prisma.module.ts | 11 +- .../src/db/repositories/fitness.repository.ts | 9 + .../src/db/repositories/task.repository.ts | 67 ++++++++ .../integration/fitness/fitness.service.ts | 18 ++ .../fitness/providers/fitbit.provider.ts | 4 +- backend/test/lib/constants.ts | 8 +- backend/test/lib/database.constants.ts | 30 +++- docs/public/api/schema.yaml | 2 +- 21 files changed, 670 insertions(+), 8 deletions(-) create mode 100644 backend/prisma/migrations/20240611203212_/migration.sql create mode 100644 backend/src/app/tasks/task.controller.ts create mode 100644 backend/src/app/tasks/task.module.ts create mode 100644 backend/src/app/tasks/task.service.spec.ts create mode 100644 backend/src/app/tasks/task.service.ts create mode 100644 backend/src/app/tasks/tasks/static/task.1.ts create mode 100644 backend/src/app/tasks/tasks/static/task2.ts create mode 100644 backend/src/app/tasks/tasks/static/task3.ts create mode 100644 backend/src/app/tasks/tasks/task.base.ts create mode 100644 backend/src/app/tasks/tasks/task.step.ts create mode 100644 backend/src/app/tasks/tasks/task.types.ts create mode 100644 backend/src/db/repositories/task.repository.ts diff --git a/backend/prisma/migrations/20240611203212_/migration.sql b/backend/prisma/migrations/20240611203212_/migration.sql new file mode 100644 index 0000000..ae19171 --- /dev/null +++ b/backend/prisma/migrations/20240611203212_/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "TaskLog" ( + "userId" TEXT NOT NULL, + "task" TEXT NOT NULL, + "start" DATETIME NOT NULL, + "end" DATETIME, + "status" TEXT NOT NULL, + "metadata" TEXT, + + PRIMARY KEY ("userId", "task") +); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index bc9560e..8f12abd 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -33,6 +33,17 @@ model FitnessProviderCredential { providerUserId String } +model TaskLog { + userId String + task String + start DateTime + end DateTime? + status String + metadata String? + + @@id([userId, task]) +} + model Points { userId String owner User @relation(fields: [userId], references: [id]) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index e398fa8..d286b60 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,6 +9,7 @@ import { ConfigModule } from '@nestjs/config'; import FitnessModule from './integration/fitness/fitness.module'; import configuration from './config/configuration'; import { NotificationModule } from './notification/notification.module'; +import { TaskModule } from './app/tasks/task.module'; import { StreakModule } from './app/streaks/streak.module'; @Module({ @@ -20,6 +21,7 @@ import { StreakModule } from './app/streaks/streak.module'; }), FitnessModule, NotificationModule, + TaskModule, StreakModule, ], controllers: [AppController, UserController], diff --git a/backend/src/app/tasks/task.controller.ts b/backend/src/app/tasks/task.controller.ts new file mode 100644 index 0000000..3813669 --- /dev/null +++ b/backend/src/app/tasks/task.controller.ts @@ -0,0 +1,105 @@ +import { + Body, + Controller, + Get, + Param, + Put, + Req, + Res, + UseGuards, +} from '@nestjs/common'; +import { + ConcurrentTaskError, + FitnessDataNotAvailable, + TaskAlreadyCompleted, + TaskNotAvailableError, + TaskService, +} from './task.service'; +import { AutoGuard } from '../../auth/auto.guard'; +import { NestRequest } from '../../types/request.type'; +import { Response } from 'express'; + +@Controller('task') +export class TaskController { + constructor(private taskService: TaskService) {} + + @Get() + @UseGuards(AutoGuard) + public async getTasks(@Req() req: NestRequest) { + return (await this.taskService.getTasks(req.user.id)).map((t) => + t.getInfo(), + ); + } + + @Put('/:id') + @UseGuards(AutoGuard) + public async putTask( + @Req() req: NestRequest, + @Param('id') id: string, + @Body('action') action: string, + @Res() response: Response, + ) { + if (action == 'start') { + return this.startTask(req, id, response); + } else if (action == 'stop') { + return this.stopTask(req, id, response); + } else { + response + .status(500) + .json({ error: 'action must be either start or stop' }); + } + } + + private async startTask(req: NestRequest, id: string, response: Response) { + try { + await this.taskService.startTask(req.user.id, id); + const task = await this.taskService.getTask(req.user.id, id); + + return response.json(task?.getInfo()); + } catch (e) { + if (e instanceof TaskNotAvailableError) { + return response.status(400).json({ error: 'Invalid task' }); + } else if (e instanceof FitnessDataNotAvailable) { + return response + .status(400) + .json({ error: 'No fitness provider connected.' }); + } + + if (e instanceof ConcurrentTaskError) { + return response + .status(400) + .json({ error: 'You already have a running task' }); + } else if (e instanceof TaskAlreadyCompleted) { + return response + .status(400) + .json({ error: 'You already completed this task' }); + } + + response.status(500).json({ error: 'Unknown error' }); + } + } + + private async stopTask(req: NestRequest, id: string, response: Response) { + await this.taskService.stopTask(req.user.id, id); + + const task = await this.taskService.getTask(req.user.id, id); + + response.status(200).json(task?.getInfo()); + } + + @Get('/:id') + @UseGuards(AutoGuard) + public async getTask( + @Req() req: NestRequest, + @Param('id') id: string, + @Res() response: Response, + ) { + const task = await this.taskService.getTask(req.user.id, id); + + if (!task) { + return response.status(404).json({ error: 'Task not found' }); + } + + return response.status(200).json(task.getInfo()); + } +} diff --git a/backend/src/app/tasks/task.module.ts b/backend/src/app/tasks/task.module.ts new file mode 100644 index 0000000..713e7d3 --- /dev/null +++ b/backend/src/app/tasks/task.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../../db/prisma.module'; +import { TaskService } from './task.service'; +import { TaskController } from './task.controller'; +import FitnessModule from '../../integration/fitness/fitness.module'; + +@Module({ + imports: [PrismaModule, FitnessModule], + providers: [TaskService], + controllers: [TaskController], +}) +export class TaskModule {} diff --git a/backend/src/app/tasks/task.service.spec.ts b/backend/src/app/tasks/task.service.spec.ts new file mode 100644 index 0000000..42ccbd1 --- /dev/null +++ b/backend/src/app/tasks/task.service.spec.ts @@ -0,0 +1,116 @@ +import { Test } from '@nestjs/testing'; +import { TaskService } from './task.service'; +import { PrismaModule } from '../../db/prisma.module'; +import { DeepMockProxy, mockDeep } from 'jest-mock-extended'; +import { TaskRepository } from '../../db/repositories/task.repository'; +import { FitnessService } from '../../integration/fitness/fitness.service'; +import FitnessModule from '../../integration/fitness/fitness.module'; +import { TestConstants } from '../../../test/lib/constants'; +import { MockProvider } from '../../integration/fitness/providers/mock.provider'; + +describe('task service tests', () => { + let taskService: TaskService; + let taskRepository: DeepMockProxy; + let fitnessService: DeepMockProxy; + + beforeAll(async () => { + const testModule = await Test.createTestingModule({ + imports: [PrismaModule, FitnessModule], + providers: [TaskService], + }) + .overrideProvider(TaskRepository) + .useValue(mockDeep()) + .overrideProvider(FitnessService) + .useValue(mockDeep()) + .compile(); + + taskService = testModule.get(TaskService); + taskRepository = testModule.get(TaskRepository); + fitnessService = testModule.get(FitnessService); + }); + + it('it should return all available tasks including user info', async () => { + const logs = [ + TestConstants.database.taskLogs.task1, + TestConstants.database.taskLogs.task2, + ]; + + taskRepository.getTaskLogsForUser.mockResolvedValue(logs); + + const tasks = await taskService.getTasks( + TestConstants.database.users.exampleUser.id, + ); + + expect(tasks.length).toBeGreaterThan(0); + + const task1Info = tasks[0].getInfo(); + expect(task1Info.id).toBe('1'); + expect(task1Info.status).toBe('pending'); + + const task2Info = tasks[1].getInfo(); + expect(task2Info.id).toBe('2'); + expect(task2Info.status).toBe('failed'); + + const task3Info = tasks[2].getInfo(); + expect(task3Info.id).toBe('3'); + expect(task3Info.status).toBe('not started'); + }); + + it('it should return all a specific including user info', async () => { + taskRepository.getTaskLog.mockResolvedValue( + TestConstants.database.taskLogs.task1, + ); + + const task = await taskService.getTask( + TestConstants.database.users.exampleUser.id, + TestConstants.database.taskLogs.task1.task, + ); + + expect(task).toBeDefined(); + + const task1Info = task!.getInfo(); + expect(task1Info.id).toBe('1'); + expect(task1Info.status).toBe('pending'); + }); + + it('should be able to start a task', async () => { + taskRepository.getStartedTasksForUser.mockResolvedValue([]); + + fitnessService.getDatasourcesForUser.mockResolvedValue([ + new MockProvider(), + ]); + + taskRepository.saveTaskLog.mockResolvedValue( + TestConstants.database.taskLogs.task3, + ); + + taskRepository.getTaskLog.mockResolvedValue(null); + + const log = await taskService.startTask( + TestConstants.database.users.exampleUser.id, + TestConstants.database.taskLogs.task3.task, + ); + + expect(log).toBeDefined(); + expect(log.task).toBe('3'); + expect(log.status).toBe('in progress'); + }); + + it('should be able to stop a running task', async () => { + taskRepository.getTaskLog.mockResolvedValue( + TestConstants.database.taskLogs.task3, + ); + + taskRepository.updateTaskLog.mockResolvedValue( + TestConstants.database.taskLogs.task3, + ); + + const log = await taskService.stopTask( + TestConstants.database.users.exampleUser.id, + TestConstants.database.taskLogs.task3.task, + ); + + expect(log).toBeDefined(); + expect(log.task).toBe('3'); + }); +}); diff --git a/backend/src/app/tasks/task.service.ts b/backend/src/app/tasks/task.service.ts new file mode 100644 index 0000000..cb1baba --- /dev/null +++ b/backend/src/app/tasks/task.service.ts @@ -0,0 +1,159 @@ +import { Injectable } from '@nestjs/common'; +import { Task1 } from './tasks/static/task.1'; +import { Task } from './tasks/task.base'; +import { TaskRepository } from '../../db/repositories/task.repository'; +import * as dayjs from 'dayjs'; +import { TaskLog } from '@prisma/client'; +import { FitnessService } from '../../integration/fitness/fitness.service'; +import { Task2 } from './tasks/static/task2'; +import { Task3 } from './tasks/static/task3'; + +export class ConcurrentTaskError extends Error {} +export class TaskNotAvailableError extends Error {} +export class TaskNotStartedError extends Error {} +export class FitnessDataNotAvailable extends Error {} +export class TaskAlreadyCompleted extends Error {} + +@Injectable() +export class TaskService { + constructor( + private taskRepository: TaskRepository, + private fitnessService: FitnessService, + ) {} + + private availableTasks = { + '1': Task1, + '2': Task2, + '3': Task3, + }; + + private getLogForTask(logs: TaskLog[], task: string): TaskLog | undefined { + return logs.find((t) => t.task == task); + } + + /** + * Returns all available tasks + * + * @returns + */ + public async getTasks(user: string): Promise { + const logs = await this.taskRepository.getTaskLogsForUser(user); + + const tasksWithLogs = Object.keys(this.availableTasks).map((t) => { + const log = this.getLogForTask(logs, t); + + return new this.availableTasks[t]( + log?.status || 'not started', + log?.start, + log?.end, + ); + }); + + return tasksWithLogs; + } + + public async getTask( + user: string, + taskId: string, + ): Promise { + if (!(taskId in this.availableTasks)) { + return; + } + + const task = this.availableTasks[taskId]; + + if (!task) { + return undefined; + } + + const log = await this.taskRepository.getTaskLog(user, taskId); + + return new task(log?.status ?? 'not started'); + } + + public async startTask(user: string, task: string): Promise { + // Only one concurrent task should be allowed + const startedTasks = await this.taskRepository.getStartedTasksForUser(user); + + if (startedTasks.length > 0) { + throw new ConcurrentTaskError('User already has a started task'); + } + + // Check whether the task was already completed + const log = await this.taskRepository.getTaskLog(user, task); + + if (log) { + // If the log is failed, delete the log and start over + if (log.status == 'failed') { + await this.taskRepository.deleteTaskLog(user, task); + } else { + throw new TaskAlreadyCompleted(); + } + } + + if (!(task in this.availableTasks)) { + throw new TaskNotAvailableError('Task not found'); + } + + const datasources = await this.fitnessService.getDatasourcesForUser(user); + + if (datasources.length < 1) { + throw new FitnessDataNotAvailable(); + } + + const fitnessData = await datasources[0].getFitnessData( + user, + dayjs().toDate(), + dayjs().toDate(), + ); + + return await this.taskRepository.saveTaskLog({ + task, + userId: user, + start: dayjs().toDate(), + status: 'in progress', + metadata: JSON.stringify(fitnessData), + }); + } + + public async stopTask(user: string, task: string) { + const log = await this.taskRepository.getTaskLog(user, task); + + if (!log || log.end != undefined || log.status != 'in progress') { + throw new TaskNotStartedError(); + } + + // Verify the task in three minutes + setTimeout(() => this.verifyTask(user, task), 3 * 1000); + + return await this.taskRepository.updateTaskLog(user, task, { + status: 'pending', + end: dayjs().toDate(), + }); + } + + public async verifyTask(user: string, task: string): Promise { + const log = await this.taskRepository.getTaskLog(user, task); + + if (!log) { + console.debug('Log to verify not found'); + throw new Error('no log found to verify'); + } + + // Retrieve the fitness data + const fitnessData = await this.fitnessService.getFitnessDataForUser(user); + + // Verify the task# + const taskValidator = new this.availableTasks[log.task](); + const result = taskValidator.validate( + JSON.parse(log.metadata!), + fitnessData, + ); + + console.debug(`Fitness Task verified: ${result}`); + + this.taskRepository.updateTaskLog(user, task, { + status: result ? 'completed' : 'failed', + }); + } +} diff --git a/backend/src/app/tasks/tasks/static/task.1.ts b/backend/src/app/tasks/tasks/static/task.1.ts new file mode 100644 index 0000000..b0521bd --- /dev/null +++ b/backend/src/app/tasks/tasks/static/task.1.ts @@ -0,0 +1,20 @@ +import * as dayjs from 'dayjs'; +import { StepTask } from '../task.step'; +import { TaskInfo } from '../task.types'; + +export class Task1 extends StepTask { + getRequiredSteps(): number { + return 100; + } + getInfo(): TaskInfo { + return { + id: '1', + title: 'Ein leichter Start', + description: 'Erreiche mindestens 100 Schritte', + conditions: [], + status: this.userStatus, + start: this.start ? dayjs(this.start).format() : undefined, + stop: this.stop ? dayjs(this.stop).format() : undefined, + }; + } +} diff --git a/backend/src/app/tasks/tasks/static/task2.ts b/backend/src/app/tasks/tasks/static/task2.ts new file mode 100644 index 0000000..7c37739 --- /dev/null +++ b/backend/src/app/tasks/tasks/static/task2.ts @@ -0,0 +1,17 @@ +import { StepTask } from '../task.step'; +import { TaskInfo } from '../task.types'; + +export class Task2 extends StepTask { + getRequiredSteps(): number { + return 1000; + } + getInfo(): TaskInfo { + return { + id: '2', + title: 'Deine Reise beginnt', + description: 'Erreiche mindestens 1.000 Schritte', + conditions: [], + status: this.userStatus, + }; + } +} diff --git a/backend/src/app/tasks/tasks/static/task3.ts b/backend/src/app/tasks/tasks/static/task3.ts new file mode 100644 index 0000000..00644de --- /dev/null +++ b/backend/src/app/tasks/tasks/static/task3.ts @@ -0,0 +1,17 @@ +import { StepTask } from '../task.step'; +import { TaskInfo } from '../task.types'; + +export class Task3 extends StepTask { + getRequiredSteps(): number { + return 2000; + } + getInfo(): TaskInfo { + return { + id: '3', + title: 'Deine Reise wird fortgesetzt', + description: 'Erreiche mindestens 2.000 Schritte', + conditions: [], + status: this.userStatus, + }; + } +} diff --git a/backend/src/app/tasks/tasks/task.base.ts b/backend/src/app/tasks/tasks/task.base.ts new file mode 100644 index 0000000..3d3b342 --- /dev/null +++ b/backend/src/app/tasks/tasks/task.base.ts @@ -0,0 +1,20 @@ +import { FitnessData } from '../../../integration/fitness/fitness.data'; +import { TaskInfo, TaskStatus } from './task.types'; + +/** + * Task + * + * Tasks are challenges for the user, which can be used to gain points. A task + * has a validate method and checks the previous and current fitness data + * from the provider of the user + */ +export abstract class Task { + constructor( + protected userStatus: TaskStatus = 'unknown', + protected start?: Date, + protected stop?: Date, + ) {} + + abstract getInfo(): TaskInfo; + abstract validate(previous: FitnessData, current: FitnessData): boolean; +} diff --git a/backend/src/app/tasks/tasks/task.step.ts b/backend/src/app/tasks/tasks/task.step.ts new file mode 100644 index 0000000..3a83644 --- /dev/null +++ b/backend/src/app/tasks/tasks/task.step.ts @@ -0,0 +1,16 @@ +import { Task } from './task.base'; +import { FitnessData } from '../../../integration/fitness/fitness.data'; + +export abstract class StepTask extends Task { + abstract getRequiredSteps(): number; + + public validate(previous: FitnessData, current: FitnessData): boolean { + const stepsPrevious = previous.goals.find((g) => g.type == 'steps'); + const stepsNow = current.goals.find((g) => g.type == 'steps'); + + // If we have no steps, the user cannot complete this task + if (!stepsPrevious || !stepsNow) throw new Error('Step data is missing'); + + return stepsPrevious.value + this.getRequiredSteps() < stepsNow.value; + } +} diff --git a/backend/src/app/tasks/tasks/task.types.ts b/backend/src/app/tasks/tasks/task.types.ts new file mode 100644 index 0000000..9e3dc8e --- /dev/null +++ b/backend/src/app/tasks/tasks/task.types.ts @@ -0,0 +1,23 @@ +export type TaskInfo = { + id: string; + title: string; + description: string; + conditions: { + name: string; + description: string; + color: string; + icon: string; + }[]; + status: TaskStatus; + start?: string; + stop?: string; +}; + +export type TaskStatus = 'available' | 'completed' | 'locked' | 'unknown'; + +export type TaskLog = { + status: 'completed' | 'in progress' | 'failed' | 'pending'; + start?: string; + end?: string; + points: number; +}; diff --git a/backend/src/db/prisma.module.ts b/backend/src/db/prisma.module.ts index 02431c2..9793b9f 100644 --- a/backend/src/db/prisma.module.ts +++ b/backend/src/db/prisma.module.ts @@ -2,15 +2,22 @@ import { Module } from '@nestjs/common'; import { UserRepository } from './repositories/user.repository'; import { PrismaService } from './prisma.service'; import { FitnessRepository } from './repositories/fitness.repository'; +import { TaskRepository } from './repositories/task.repository'; import { StreakRepository } from './repositories/streak.repository'; - @Module({ providers: [ PrismaService, UserRepository, FitnessRepository, StreakRepository, + TaskRepository, + ], + exports: [ + PrismaService, + UserRepository, + FitnessRepository, + StreakRepository, + TaskRepository, ], - exports: [PrismaService, UserRepository, FitnessRepository, StreakRepository], }) export class PrismaModule {} diff --git a/backend/src/db/repositories/fitness.repository.ts b/backend/src/db/repositories/fitness.repository.ts index 517969f..409ed42 100644 --- a/backend/src/db/repositories/fitness.repository.ts +++ b/backend/src/db/repositories/fitness.repository.ts @@ -39,6 +39,15 @@ export class FitnessRepository { return await this.prisma.fitnessProviderCredential.delete(provider); } + public async deleteProviderForUser(user: string, type: string) { + return await this.prisma.fitnessProviderCredential.deleteMany({ + where: { + userId: user, + type, + }, + }); + } + public async updateProvider( type: string, user: string, diff --git a/backend/src/db/repositories/task.repository.ts b/backend/src/db/repositories/task.repository.ts new file mode 100644 index 0000000..06ef4fe --- /dev/null +++ b/backend/src/db/repositories/task.repository.ts @@ -0,0 +1,67 @@ +import { Prisma, TaskLog } from '@prisma/client'; +import { PrismaService } from '../prisma.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TaskRepository { + constructor(private prismaService: PrismaService) {} + + async getTaskLog(userId: string, taskId: string): Promise { + return await this.prismaService.taskLog.findUnique({ + where: { + userId_task: { + userId, + task: taskId, + }, + }, + }); + } + + async getTaskLogsForUser(userId: string): Promise { + return await this.prismaService.taskLog.findMany({ + where: { + userId, + }, + }); + } + + async saveTaskLog(log: Prisma.TaskLogCreateInput): Promise { + return await this.prismaService.taskLog.create({ data: log }); + } + + async updateTaskLog( + userId: string, + taskId: string, + log: Prisma.TaskLogUpdateInput, + ): Promise { + return await this.prismaService.taskLog.update({ + where: { + userId_task: { + userId, + task: taskId, + }, + }, + data: log, + }); + } + + async deleteTaskLog(userId: string, taskId: string) { + return await this.prismaService.taskLog.delete({ + where: { + userId_task: { + userId, + task: taskId, + }, + }, + }); + } + + async getStartedTasksForUser(userId: string): Promise { + return await this.prismaService.taskLog.findMany({ + where: { + userId, + status: 'in progress', + }, + }); + } +} diff --git a/backend/src/integration/fitness/fitness.service.ts b/backend/src/integration/fitness/fitness.service.ts index 9f6cd5f..476fb20 100644 --- a/backend/src/integration/fitness/fitness.service.ts +++ b/backend/src/integration/fitness/fitness.service.ts @@ -5,6 +5,8 @@ import { FitnessProvider } from './providers/provider.interface'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { CredentialService } from '../credentials/credential.service'; import { LOGGER_SERVICE } from '../../logger/logger.service'; +import { FitnessData } from './fitness.data'; +import * as dayjs from 'dayjs'; @Injectable() export class FitnessService { @@ -89,4 +91,20 @@ export class FitnessService { return responsibleProvider ?? null; } + + public async getFitnessDataForUser( + userId: string, + ): Promise { + const providers = await this.getDatasourcesForUser(userId); + + if (providers.length < 1) { + return undefined; + } + + return await providers[0].getFitnessData( + userId, + dayjs().toDate(), + dayjs().toDate(), + ); + } } diff --git a/backend/src/integration/fitness/providers/fitbit.provider.ts b/backend/src/integration/fitness/providers/fitbit.provider.ts index 89c16a6..6caa042 100644 --- a/backend/src/integration/fitness/providers/fitbit.provider.ts +++ b/backend/src/integration/fitness/providers/fitbit.provider.ts @@ -65,9 +65,7 @@ export class FitbitProvider implements FitnessProvider { this.userStatus = 'enabled'; // First delete any configured provider for this type and user - await this.fitnessRepository.deleteProvider({ - where: { userId: user, type: this.FITBIT_TYPE }, - }); + await this.fitnessRepository.deleteProviderForUser(user, this.FITBIT_TYPE); // Save the credentials in the database await this.fitnessRepository.createProvider({ diff --git a/backend/test/lib/constants.ts b/backend/test/lib/constants.ts index b69df11..b1e13fa 100644 --- a/backend/test/lib/constants.ts +++ b/backend/test/lib/constants.ts @@ -1,9 +1,15 @@ -import { FitnessCredetials, PointEntries, Users } from './database.constants'; +import { + FitnessCredetials, + PointEntries, + TaskLogs, + Users, +} from './database.constants'; export const TestConstants = { database: { users: Users, fitnessCredentials: FitnessCredetials, points: PointEntries, + taskLogs: TaskLogs, }, }; diff --git a/backend/test/lib/database.constants.ts b/backend/test/lib/database.constants.ts index eb92b15..2f1c185 100644 --- a/backend/test/lib/database.constants.ts +++ b/backend/test/lib/database.constants.ts @@ -1,4 +1,9 @@ -import { FitnessProviderCredential, Points, User } from '@prisma/client'; +import { + FitnessProviderCredential, + Points, + TaskLog, + User, +} from '@prisma/client'; import * as dayjs from 'dayjs'; export const Users: { exampleUser: User } = { @@ -51,3 +56,26 @@ export const PointEntries = { streak: 0, }, }; + +export const TaskLogs = { + task1: { + task: '1', + userId: Users.exampleUser.id, + start: dayjs().toDate(), + status: 'pending', + } as TaskLog, + task2: { + task: '2', + userId: Users.exampleUser.id, + start: dayjs().subtract(1, 'hour').toDate(), + end: dayjs().toDate(), + status: 'failed', + } as TaskLog, + task3: { + task: '3', + userId: Users.exampleUser.id, + start: dayjs().subtract(1, 'hour').toDate(), + end: null, + status: 'in progress', + } as TaskLog, +}; diff --git a/docs/public/api/schema.yaml b/docs/public/api/schema.yaml index 3d2b8b6..462ced2 100644 --- a/docs/public/api/schema.yaml +++ b/docs/public/api/schema.yaml @@ -423,7 +423,7 @@ components: type: string status: type: string - enum: [available, unavailable, fulfilled, started] + enum: [not started, in progress, pending, completed, failed] Datasource: type: object ShopItem: